database access refactor
This commit is contained in:
parent
68dddc848f
commit
5c26461247
@ -642,7 +642,7 @@ reservesCli
|
|||||||
})
|
})
|
||||||
.action(async (args) => {
|
.action(async (args) => {
|
||||||
await withWallet(args, async (wallet) => {
|
await withWallet(args, async (wallet) => {
|
||||||
const reserves = await wallet.getReserves();
|
const reserves = await wallet.getReservesForExchange();
|
||||||
console.log(JSON.stringify(reserves, undefined, 2));
|
console.log(JSON.stringify(reserves, undefined, 2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
openDatabase,
|
openDatabase,
|
||||||
Database,
|
describeStore,
|
||||||
Store,
|
describeContents,
|
||||||
Index,
|
describeIndex,
|
||||||
AnyStoreMap,
|
DbAccess,
|
||||||
|
StoreDescriptor,
|
||||||
|
StoreWithIndexes,
|
||||||
|
IndexDescriptor,
|
||||||
} from "./util/query";
|
} from "./util/query";
|
||||||
import {
|
import {
|
||||||
IDBFactory,
|
IDBFactory,
|
||||||
IDBDatabase,
|
IDBDatabase,
|
||||||
IDBObjectStore,
|
IDBObjectStore,
|
||||||
IDBTransaction,
|
IDBTransaction,
|
||||||
IDBKeyPath,
|
IDBObjectStoreParameters,
|
||||||
} from "@gnu-taler/idb-bridge";
|
} from "@gnu-taler/idb-bridge";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
@ -55,7 +58,7 @@ export const WALLET_DB_MINOR_VERSION = 1;
|
|||||||
const logger = new Logger("db.ts");
|
const logger = new Logger("db.ts");
|
||||||
|
|
||||||
function upgradeFromStoreMap(
|
function upgradeFromStoreMap(
|
||||||
storeMap: AnyStoreMap,
|
storeMap: any,
|
||||||
db: IDBDatabase,
|
db: IDBDatabase,
|
||||||
oldVersion: number,
|
oldVersion: number,
|
||||||
newVersion: number,
|
newVersion: number,
|
||||||
@ -63,15 +66,17 @@ function upgradeFromStoreMap(
|
|||||||
): void {
|
): void {
|
||||||
if (oldVersion === 0) {
|
if (oldVersion === 0) {
|
||||||
for (const n in storeMap) {
|
for (const n in storeMap) {
|
||||||
if ((storeMap as any)[n] instanceof Store) {
|
const swi: StoreWithIndexes<StoreDescriptor<unknown>, any> = storeMap[n];
|
||||||
const si: Store<string, any> = (storeMap as any)[n];
|
const storeDesc: StoreDescriptor<unknown> = swi.store;
|
||||||
const s = db.createObjectStore(si.name, si.storeParams);
|
const s = db.createObjectStore(storeDesc.name, {
|
||||||
for (const indexName in si as any) {
|
autoIncrement: storeDesc.autoIncrement,
|
||||||
if ((si as any)[indexName] instanceof Index) {
|
keyPath: storeDesc.keyPath,
|
||||||
const ii: Index<string, string, any, any> = (si as any)[indexName];
|
});
|
||||||
s.createIndex(ii.indexName, ii.keyPath, ii.options);
|
for (const indexName in swi.indexMap as any) {
|
||||||
}
|
const indexDesc: IndexDescriptor = swi.indexMap[indexName];
|
||||||
}
|
s.createIndex(indexDesc.name, indexDesc.keyPath, {
|
||||||
|
multiEntry: indexDesc.multiEntry,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -80,30 +85,7 @@ function upgradeFromStoreMap(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.info(`upgrading database from ${oldVersion} to ${newVersion}`);
|
logger.info(`upgrading database from ${oldVersion} to ${newVersion}`);
|
||||||
for (const n in Stores) {
|
throw Error("upgrade not supported");
|
||||||
if ((Stores as any)[n] instanceof Store) {
|
|
||||||
const si: Store<string, any> = (Stores as any)[n];
|
|
||||||
let s: IDBObjectStore;
|
|
||||||
const storeVersionAdded = si.storeParams?.versionAdded ?? 1;
|
|
||||||
if (storeVersionAdded > oldVersion) {
|
|
||||||
s = db.createObjectStore(si.name, si.storeParams);
|
|
||||||
} else {
|
|
||||||
s = upgradeTransaction.objectStore(si.name);
|
|
||||||
}
|
|
||||||
for (const indexName in si as any) {
|
|
||||||
if ((si as any)[indexName] instanceof Index) {
|
|
||||||
const ii: Index<string, string, any, any> = (si as any)[indexName];
|
|
||||||
const indexVersionAdded = ii.options?.versionAdded ?? 0;
|
|
||||||
if (
|
|
||||||
indexVersionAdded > oldVersion ||
|
|
||||||
storeVersionAdded > oldVersion
|
|
||||||
) {
|
|
||||||
s.createIndex(ii.indexName, ii.keyPath, ii.options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTalerDbUpgradeNeeded(
|
function onTalerDbUpgradeNeeded(
|
||||||
@ -112,7 +94,13 @@ function onTalerDbUpgradeNeeded(
|
|||||||
newVersion: number,
|
newVersion: number,
|
||||||
upgradeTransaction: IDBTransaction,
|
upgradeTransaction: IDBTransaction,
|
||||||
) {
|
) {
|
||||||
upgradeFromStoreMap(Stores, db, oldVersion, newVersion, upgradeTransaction);
|
upgradeFromStoreMap(
|
||||||
|
WalletStoresV1,
|
||||||
|
db,
|
||||||
|
oldVersion,
|
||||||
|
newVersion,
|
||||||
|
upgradeTransaction,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMetaDbUpgradeNeeded(
|
function onMetaDbUpgradeNeeded(
|
||||||
@ -122,7 +110,7 @@ function onMetaDbUpgradeNeeded(
|
|||||||
upgradeTransaction: IDBTransaction,
|
upgradeTransaction: IDBTransaction,
|
||||||
) {
|
) {
|
||||||
upgradeFromStoreMap(
|
upgradeFromStoreMap(
|
||||||
MetaStores,
|
walletMetadataStore,
|
||||||
db,
|
db,
|
||||||
oldVersion,
|
oldVersion,
|
||||||
newVersion,
|
newVersion,
|
||||||
@ -137,7 +125,7 @@ function onMetaDbUpgradeNeeded(
|
|||||||
export async function openTalerDatabase(
|
export async function openTalerDatabase(
|
||||||
idbFactory: IDBFactory,
|
idbFactory: IDBFactory,
|
||||||
onVersionChange: () => void,
|
onVersionChange: () => void,
|
||||||
): Promise<Database<typeof Stores>> {
|
): Promise<DbAccess<typeof WalletStoresV1>> {
|
||||||
const metaDbHandle = await openDatabase(
|
const metaDbHandle = await openDatabase(
|
||||||
idbFactory,
|
idbFactory,
|
||||||
TALER_META_DB_NAME,
|
TALER_META_DB_NAME,
|
||||||
@ -146,23 +134,24 @@ export async function openTalerDatabase(
|
|||||||
onMetaDbUpgradeNeeded,
|
onMetaDbUpgradeNeeded,
|
||||||
);
|
);
|
||||||
|
|
||||||
const metaDb = new Database(metaDbHandle, MetaStores);
|
const metaDb = new DbAccess(metaDbHandle, walletMetadataStore);
|
||||||
let currentMainVersion: string | undefined;
|
let currentMainVersion: string | undefined;
|
||||||
await metaDb.runWithWriteTransaction([MetaStores.metaConfig], async (tx) => {
|
await metaDb
|
||||||
const dbVersionRecord = await tx.get(
|
.mktx((x) => ({
|
||||||
MetaStores.metaConfig,
|
metaConfig: x.metaConfig,
|
||||||
CURRENT_DB_CONFIG_KEY,
|
}))
|
||||||
);
|
.runReadWrite(async (tx) => {
|
||||||
if (!dbVersionRecord) {
|
const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY);
|
||||||
currentMainVersion = TALER_DB_NAME;
|
if (!dbVersionRecord) {
|
||||||
await tx.put(MetaStores.metaConfig, {
|
currentMainVersion = TALER_DB_NAME;
|
||||||
key: CURRENT_DB_CONFIG_KEY,
|
await tx.metaConfig.put({
|
||||||
value: TALER_DB_NAME,
|
key: CURRENT_DB_CONFIG_KEY,
|
||||||
});
|
value: TALER_DB_NAME,
|
||||||
} else {
|
});
|
||||||
currentMainVersion = dbVersionRecord.value;
|
} else {
|
||||||
}
|
currentMainVersion = dbVersionRecord.value;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (currentMainVersion !== TALER_DB_NAME) {
|
if (currentMainVersion !== TALER_DB_NAME) {
|
||||||
// In the future, the migration logic will be implemented here.
|
// In the future, the migration logic will be implemented here.
|
||||||
@ -177,11 +166,12 @@ export async function openTalerDatabase(
|
|||||||
onTalerDbUpgradeNeeded,
|
onTalerDbUpgradeNeeded,
|
||||||
);
|
);
|
||||||
|
|
||||||
return new Database(mainDbHandle, Stores);
|
return new DbAccess(mainDbHandle, WalletStoresV1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteTalerDatabase(idbFactory: IDBFactory): Promise<void> {
|
|
||||||
return Database.deleteDatabase(idbFactory, TALER_DB_NAME);
|
export function deleteTalerDatabase(idbFactory: IDBFactory): void {
|
||||||
|
idbFactory.deleteDatabase(TALER_DB_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReserveRecordStatus {
|
export enum ReserveRecordStatus {
|
||||||
@ -634,7 +624,7 @@ export interface ExchangeRecord {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of updating the info about the exchange.
|
* Status of updating the info about the exchange.
|
||||||
*
|
*
|
||||||
* FIXME: Adapt this to recent changes regarding how
|
* FIXME: Adapt this to recent changes regarding how
|
||||||
* updating exchange details works.
|
* updating exchange details works.
|
||||||
*/
|
*/
|
||||||
@ -1683,289 +1673,167 @@ export interface TombstoneRecord {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
|
export const WalletStoresV1 = {
|
||||||
constructor() {
|
coins: describeStore(
|
||||||
super("exchanges", { keyPath: "baseUrl" });
|
describeContents<CoinRecord>("coins", {
|
||||||
}
|
keyPath: "coinPub",
|
||||||
}
|
}),
|
||||||
|
{
|
||||||
class ExchangeDetailsStore extends Store<
|
byBaseUrl: describeIndex("byBaseUrl", "exchangeBaseUrl"),
|
||||||
"exchangeDetails",
|
byDenomPubHash: describeIndex("byDenomPubHash", "denomPubHash"),
|
||||||
ExchangeDetailsRecord
|
byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"),
|
||||||
> {
|
},
|
||||||
constructor() {
|
),
|
||||||
super("exchangeDetails", {
|
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"],
|
keyPath: ["exchangeBaseUrl", "currency", "masterPublicKey"],
|
||||||
});
|
}),
|
||||||
}
|
{},
|
||||||
}
|
),
|
||||||
|
proposals: describeStore(
|
||||||
class CoinsStore extends Store<"coins", CoinRecord> {
|
describeContents<ProposalRecord>("proposals", { keyPath: "proposalId" }),
|
||||||
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",
|
|
||||||
{
|
{
|
||||||
|
byUrlAndOrderId: describeIndex("byUrlAndOrderId", [
|
||||||
|
"merchantBaseUrl",
|
||||||
|
"orderId",
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
refreshGroups: describeStore(
|
||||||
|
describeContents<RefreshGroupRecord>("refreshGroups", {
|
||||||
keyPath: "refreshGroupId",
|
keyPath: "refreshGroupId",
|
||||||
},
|
}),
|
||||||
|
{},
|
||||||
),
|
),
|
||||||
recoupGroups: new Store<"recoupGroups", RecoupGroupRecord>("recoupGroups", {
|
recoupGroups: describeStore(
|
||||||
keyPath: "recoupGroupId",
|
describeContents<RecoupGroupRecord>("recoupGroups", {
|
||||||
}),
|
keyPath: "recoupGroupId",
|
||||||
reserves: new ReservesStore(),
|
}),
|
||||||
purchases: new PurchasesStore(),
|
{},
|
||||||
tips: new TipsStore(),
|
),
|
||||||
withdrawalGroups: new WithdrawalGroupsStore(),
|
reserves: describeStore(
|
||||||
planchets: new PlanchetsStore(),
|
describeContents<ReserveRecord>("reserves", { keyPath: "reservePub" }),
|
||||||
bankWithdrawUris: new BankWithdrawUrisStore(),
|
|
||||||
backupProviders: new BackupProvidersStore(),
|
|
||||||
depositGroups: new DepositGroupsStore(),
|
|
||||||
tombstones: new TombstonesStore(),
|
|
||||||
ghostDepositGroups: new Store<"ghostDepositGroups", GhostDepositGroupRecord>(
|
|
||||||
"ghostDepositGroups",
|
|
||||||
{
|
{
|
||||||
keyPath: "contractTermsHash",
|
byInitialWithdrawalGroupId: describeIndex(
|
||||||
|
"byInitialWithdrawalGroupId",
|
||||||
|
"initialWithdrawalGroupId",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
purchases: describeStore(
|
||||||
|
describeContents<PurchaseRecord>("purchases", { keyPath: "proposalId" }),
|
||||||
|
{
|
||||||
|
byFulfillmentUrl: describeIndex(
|
||||||
|
"byFulfillmentUrl",
|
||||||
|
"download.contractData.fulfillmentUrl",
|
||||||
|
),
|
||||||
|
byMerchantUrlAndOrderId: describeIndex("byOrderId", [
|
||||||
|
"download.contractData.merchantBaseUrl",
|
||||||
|
"download.contractData.orderId",
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
tips: describeStore(
|
||||||
|
describeContents<TipRecord>("tips", { keyPath: "walletTipId" }),
|
||||||
|
{
|
||||||
|
byMerchantTipIdAndBaseUrl: describeIndex("byMerchantTipIdAndBaseUrl", [
|
||||||
|
"merchantTipId",
|
||||||
|
"merchantBaseUrl",
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
withdrawalGroups: describeStore(
|
||||||
|
describeContents<WithdrawalGroupRecord>("withdrawalGroups", {
|
||||||
|
keyPath: "withdrawalGroupId",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
byReservePub: describeIndex("byReservePub", "reservePub"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
planchets: describeStore(
|
||||||
|
describeContents<PlanchetRecord>("planchets", { keyPath: "coinPub" }),
|
||||||
|
{
|
||||||
|
byGroupAndIndex: describeIndex("byGroupAndIndex", [
|
||||||
|
"withdrawalGroupId",
|
||||||
|
"coinIdx",
|
||||||
|
]),
|
||||||
|
byGroup: describeIndex("byGroup", "withdrawalGroupId"),
|
||||||
|
byCoinEvHash: describeIndex("byCoinEv", "coinEvHash"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
bankWithdrawUris: describeStore(
|
||||||
|
describeContents<BankWithdrawUriRecord>("bankWithdrawUris", {
|
||||||
|
keyPath: "talerWithdrawUri",
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
backupProviders: describeStore(
|
||||||
|
describeContents<BackupProviderRecord>("backupProviders", {
|
||||||
|
keyPath: "baseUrl",
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
depositGroups: describeStore(
|
||||||
|
describeContents<DepositGroupRecord>("depositGroups", {
|
||||||
|
keyPath: "depositGroupId",
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
tombstones: describeStore(
|
||||||
|
describeContents<TombstoneRecord>("tombstones", { keyPath: "id" }),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
ghostDepositGroups: describeStore(
|
||||||
|
describeContents<GhostDepositGroupRecord>("ghostDepositGroups", {
|
||||||
|
keyPath: "contractTermsHash",
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MetaConfigStore extends Store<"metaConfig", ConfigRecord<any>> {
|
export const walletMetadataStore = {
|
||||||
constructor() {
|
metaConfig: describeStore(
|
||||||
super("metaConfig", { keyPath: "key" });
|
describeContents<ConfigRecord<any>>("metaConfig", { keyPath: "key" }),
|
||||||
}
|
{},
|
||||||
}
|
),
|
||||||
|
|
||||||
export const MetaStores = {
|
|
||||||
metaConfig: new MetaConfigStore(),
|
|
||||||
};
|
};
|
||||||
|
@ -57,7 +57,6 @@ import {
|
|||||||
} from "./state";
|
} from "./state";
|
||||||
import { Amounts, getTimestampNow } from "@gnu-taler/taler-util";
|
import { Amounts, getTimestampNow } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
Stores,
|
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
RefundState,
|
RefundState,
|
||||||
@ -66,29 +65,28 @@ import {
|
|||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
|
import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js";
|
||||||
import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util";
|
import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util";
|
||||||
import { getExchangeDetails } from "../exchanges.js";
|
|
||||||
|
|
||||||
export async function exportBackup(
|
export async function exportBackup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
): Promise<WalletBackupContentV1> {
|
): Promise<WalletBackupContentV1> {
|
||||||
await provideBackupState(ws);
|
await provideBackupState(ws);
|
||||||
return ws.db.runWithWriteTransaction(
|
return ws.db
|
||||||
[
|
.mktx((x) => ({
|
||||||
Stores.config,
|
config: x.config,
|
||||||
Stores.exchanges,
|
exchanges: x.exchanges,
|
||||||
Stores.exchangeDetails,
|
exchangeDetails: x.exchangeDetails,
|
||||||
Stores.coins,
|
coins: x.coins,
|
||||||
Stores.denominations,
|
denominations: x.denominations,
|
||||||
Stores.purchases,
|
purchases: x.purchases,
|
||||||
Stores.proposals,
|
proposals: x.proposals,
|
||||||
Stores.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
Stores.backupProviders,
|
backupProviders: x.backupProviders,
|
||||||
Stores.tips,
|
tips: x.tips,
|
||||||
Stores.recoupGroups,
|
recoupGroups: x.recoupGroups,
|
||||||
Stores.reserves,
|
reserves: x.reserves,
|
||||||
Stores.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
],
|
}))
|
||||||
async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const bs = await getWalletBackupState(ws, tx);
|
const bs = await getWalletBackupState(ws, tx);
|
||||||
|
|
||||||
const backupExchangeDetails: BackupExchangeDetails[] = [];
|
const backupExchangeDetails: BackupExchangeDetails[] = [];
|
||||||
@ -108,7 +106,7 @@ export async function exportBackup(
|
|||||||
[reservePub: string]: BackupWithdrawalGroup[];
|
[reservePub: string]: BackupWithdrawalGroup[];
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wg) => {
|
await tx.withdrawalGroups.iter().forEachAsync(async (wg) => {
|
||||||
const withdrawalGroups = (withdrawalGroupsByReserve[
|
const withdrawalGroups = (withdrawalGroupsByReserve[
|
||||||
wg.reservePub
|
wg.reservePub
|
||||||
] ??= []);
|
] ??= []);
|
||||||
@ -126,7 +124,7 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.reserves).forEach((reserve) => {
|
await tx.reserves.iter().forEach((reserve) => {
|
||||||
const backupReserve: BackupReserve = {
|
const backupReserve: BackupReserve = {
|
||||||
initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map(
|
initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map(
|
||||||
(x) => ({
|
(x) => ({
|
||||||
@ -149,7 +147,7 @@ export async function exportBackup(
|
|||||||
backupReserves.push(backupReserve);
|
backupReserves.push(backupReserve);
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.tips).forEach((tip) => {
|
await tx.tips.iter().forEach((tip) => {
|
||||||
backupTips.push({
|
backupTips.push({
|
||||||
exchange_base_url: tip.exchangeBaseUrl,
|
exchange_base_url: tip.exchangeBaseUrl,
|
||||||
merchant_base_url: tip.merchantBaseUrl,
|
merchant_base_url: tip.merchantBaseUrl,
|
||||||
@ -169,7 +167,7 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.recoupGroups).forEach((recoupGroup) => {
|
await tx.recoupGroups.iter().forEach((recoupGroup) => {
|
||||||
backupRecoupGroups.push({
|
backupRecoupGroups.push({
|
||||||
recoup_group_id: recoupGroup.recoupGroupId,
|
recoup_group_id: recoupGroup.recoupGroupId,
|
||||||
timestamp_created: recoupGroup.timestampStarted,
|
timestamp_created: recoupGroup.timestampStarted,
|
||||||
@ -182,7 +180,7 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.backupProviders).forEach((bp) => {
|
await tx.backupProviders.iter().forEach((bp) => {
|
||||||
let terms: BackupBackupProviderTerms | undefined;
|
let terms: BackupBackupProviderTerms | undefined;
|
||||||
if (bp.terms) {
|
if (bp.terms) {
|
||||||
terms = {
|
terms = {
|
||||||
@ -199,7 +197,7 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.coins).forEach((coin) => {
|
await tx.coins.iter().forEach((coin) => {
|
||||||
let bcs: BackupCoinSource;
|
let bcs: BackupCoinSource;
|
||||||
switch (coin.coinSource.type) {
|
switch (coin.coinSource.type) {
|
||||||
case CoinSourceType.Refresh:
|
case CoinSourceType.Refresh:
|
||||||
@ -236,7 +234,7 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.denominations).forEach((denom) => {
|
await tx.denominations.iter().forEach((denom) => {
|
||||||
const backupDenoms = (backupDenominationsByExchange[
|
const backupDenoms = (backupDenominationsByExchange[
|
||||||
denom.exchangeBaseUrl
|
denom.exchangeBaseUrl
|
||||||
] ??= []);
|
] ??= []);
|
||||||
@ -258,7 +256,7 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.exchanges).forEachAsync(async (ex) => {
|
await tx.exchanges.iter().forEachAsync(async (ex) => {
|
||||||
const dp = ex.detailsPointer;
|
const dp = ex.detailsPointer;
|
||||||
if (!dp) {
|
if (!dp) {
|
||||||
return;
|
return;
|
||||||
@ -271,7 +269,7 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.exchangeDetails).forEachAsync(async (ex) => {
|
await tx.exchangeDetails.iter().forEachAsync(async (ex) => {
|
||||||
// Only back up permanently added exchanges.
|
// Only back up permanently added exchanges.
|
||||||
|
|
||||||
const wi = ex.wireInfo;
|
const wi = ex.wireInfo;
|
||||||
@ -323,7 +321,7 @@ export async function exportBackup(
|
|||||||
|
|
||||||
const purchaseProposalIdSet = new Set<string>();
|
const purchaseProposalIdSet = new Set<string>();
|
||||||
|
|
||||||
await tx.iter(Stores.purchases).forEach((purch) => {
|
await tx.purchases.iter().forEach((purch) => {
|
||||||
const refunds: BackupRefundItem[] = [];
|
const refunds: BackupRefundItem[] = [];
|
||||||
purchaseProposalIdSet.add(purch.proposalId);
|
purchaseProposalIdSet.add(purch.proposalId);
|
||||||
for (const refundKey of Object.keys(purch.refunds)) {
|
for (const refundKey of Object.keys(purch.refunds)) {
|
||||||
@ -376,7 +374,7 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.proposals).forEach((prop) => {
|
await tx.proposals.iter().forEach((prop) => {
|
||||||
if (purchaseProposalIdSet.has(prop.proposalId)) {
|
if (purchaseProposalIdSet.has(prop.proposalId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -413,7 +411,7 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.refreshGroups).forEach((rg) => {
|
await tx.refreshGroups.iter().forEach((rg) => {
|
||||||
const oldCoins: BackupRefreshOldCoin[] = [];
|
const oldCoins: BackupRefreshOldCoin[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < rg.oldCoinPubs.length; i++) {
|
for (let i = 0; i < rg.oldCoinPubs.length; i++) {
|
||||||
@ -482,13 +480,12 @@ export async function exportBackup(
|
|||||||
hash(stringToBytes(canonicalJson(backupBlob))),
|
hash(stringToBytes(canonicalJson(backupBlob))),
|
||||||
);
|
);
|
||||||
bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
|
bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
|
||||||
await tx.put(Stores.config, {
|
await tx.config.put({
|
||||||
key: WALLET_BACKUP_STATE_KEY,
|
key: WALLET_BACKUP_STATE_KEY,
|
||||||
value: bs,
|
value: bs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return backupBlob;
|
return backupBlob;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@ import {
|
|||||||
BackupRefreshReason,
|
BackupRefreshReason,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
Stores,
|
|
||||||
WalletContractData,
|
WalletContractData,
|
||||||
DenomSelectionState,
|
DenomSelectionState,
|
||||||
ExchangeUpdateStatus,
|
ExchangeUpdateStatus,
|
||||||
@ -46,8 +45,8 @@ import {
|
|||||||
AbortStatus,
|
AbortStatus,
|
||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
WireInfo,
|
WireInfo,
|
||||||
|
WalletStoresV1,
|
||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
import { TransactionHandle } from "../../index.js";
|
|
||||||
import { PayCoinSelection } from "../../util/coinSelection";
|
import { PayCoinSelection } from "../../util/coinSelection";
|
||||||
import { j2s } from "@gnu-taler/taler-util";
|
import { j2s } from "@gnu-taler/taler-util";
|
||||||
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
|
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
|
||||||
@ -57,6 +56,7 @@ import { InternalWalletState } from "../state";
|
|||||||
import { provideBackupState } from "./state";
|
import { provideBackupState } from "./state";
|
||||||
import { makeEventId, TombstoneTag } from "../transactions.js";
|
import { makeEventId, TombstoneTag } from "../transactions.js";
|
||||||
import { getExchangeDetails } from "../exchanges.js";
|
import { getExchangeDetails } from "../exchanges.js";
|
||||||
|
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
||||||
|
|
||||||
const logger = new Logger("operations/backup/import.ts");
|
const logger = new Logger("operations/backup/import.ts");
|
||||||
|
|
||||||
@ -74,9 +74,12 @@ function checkBackupInvariant(b: boolean, m?: string): asserts b {
|
|||||||
* Re-compute information about the coin selection for a payment.
|
* Re-compute information about the coin selection for a payment.
|
||||||
*/
|
*/
|
||||||
async function recoverPayCoinSelection(
|
async function recoverPayCoinSelection(
|
||||||
tx: TransactionHandle<
|
tx: GetReadWriteAccess<{
|
||||||
typeof Stores.exchanges | typeof Stores.coins | typeof Stores.denominations
|
exchanges: typeof WalletStoresV1.exchanges;
|
||||||
>,
|
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
||||||
|
coins: typeof WalletStoresV1.coins;
|
||||||
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
|
}>,
|
||||||
contractData: WalletContractData,
|
contractData: WalletContractData,
|
||||||
backupPurchase: BackupPurchase,
|
backupPurchase: BackupPurchase,
|
||||||
): Promise<PayCoinSelection> {
|
): Promise<PayCoinSelection> {
|
||||||
@ -93,9 +96,9 @@ async function recoverPayCoinSelection(
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const coinPub of coinPubs) {
|
for (const coinPub of coinPubs) {
|
||||||
const coinRecord = await tx.get(Stores.coins, coinPub);
|
const coinRecord = await tx.coins.get(coinPub);
|
||||||
checkBackupInvariant(!!coinRecord);
|
checkBackupInvariant(!!coinRecord);
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.denominations.get([
|
||||||
coinRecord.exchangeBaseUrl,
|
coinRecord.exchangeBaseUrl,
|
||||||
coinRecord.denomPubHash,
|
coinRecord.denomPubHash,
|
||||||
]);
|
]);
|
||||||
@ -154,11 +157,11 @@ async function recoverPayCoinSelection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getDenomSelStateFromBackup(
|
async function getDenomSelStateFromBackup(
|
||||||
tx: TransactionHandle<typeof Stores.denominations>,
|
tx: GetReadOnlyAccess<{ denominations: typeof WalletStoresV1.denominations }>,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
sel: BackupDenomSel,
|
sel: BackupDenomSel,
|
||||||
): Promise<DenomSelectionState> {
|
): Promise<DenomSelectionState> {
|
||||||
const d0 = await tx.get(Stores.denominations, [
|
const d0 = await tx.denominations.get([
|
||||||
exchangeBaseUrl,
|
exchangeBaseUrl,
|
||||||
sel[0].denom_pub_hash,
|
sel[0].denom_pub_hash,
|
||||||
]);
|
]);
|
||||||
@ -170,10 +173,7 @@ async function getDenomSelStateFromBackup(
|
|||||||
let totalCoinValue = Amounts.getZero(d0.value.currency);
|
let totalCoinValue = Amounts.getZero(d0.value.currency);
|
||||||
let totalWithdrawCost = Amounts.getZero(d0.value.currency);
|
let totalWithdrawCost = Amounts.getZero(d0.value.currency);
|
||||||
for (const s of sel) {
|
for (const s of sel) {
|
||||||
const d = await tx.get(Stores.denominations, [
|
const d = await tx.denominations.get([exchangeBaseUrl, s.denom_pub_hash]);
|
||||||
exchangeBaseUrl,
|
|
||||||
s.denom_pub_hash,
|
|
||||||
]);
|
|
||||||
checkBackupInvariant(!!d);
|
checkBackupInvariant(!!d);
|
||||||
totalCoinValue = Amounts.add(totalCoinValue, d.value).amount;
|
totalCoinValue = Amounts.add(totalCoinValue, d.value).amount;
|
||||||
totalWithdrawCost = Amounts.add(totalWithdrawCost, d.value, d.feeWithdraw)
|
totalWithdrawCost = Amounts.add(totalWithdrawCost, d.value, d.feeWithdraw)
|
||||||
@ -215,32 +215,32 @@ export async function importBackup(
|
|||||||
|
|
||||||
logger.info(`importing backup ${j2s(backupBlobArg)}`);
|
logger.info(`importing backup ${j2s(backupBlobArg)}`);
|
||||||
|
|
||||||
return ws.db.runWithWriteTransaction(
|
return ws.db
|
||||||
[
|
.mktx((x) => ({
|
||||||
Stores.config,
|
config: x.config,
|
||||||
Stores.exchanges,
|
exchanges: x.exchanges,
|
||||||
Stores.exchangeDetails,
|
exchangeDetails: x.exchangeDetails,
|
||||||
Stores.coins,
|
coins: x.coins,
|
||||||
Stores.denominations,
|
denominations: x.denominations,
|
||||||
Stores.purchases,
|
purchases: x.purchases,
|
||||||
Stores.proposals,
|
proposals: x.proposals,
|
||||||
Stores.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
Stores.backupProviders,
|
backupProviders: x.backupProviders,
|
||||||
Stores.tips,
|
tips: x.tips,
|
||||||
Stores.recoupGroups,
|
recoupGroups: x.recoupGroups,
|
||||||
Stores.reserves,
|
reserves: x.reserves,
|
||||||
Stores.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
Stores.tombstones,
|
tombstones: x.tombstones,
|
||||||
Stores.depositGroups,
|
depositGroups: x.depositGroups,
|
||||||
],
|
}))
|
||||||
async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
// FIXME: validate schema!
|
// FIXME: validate schema!
|
||||||
const backupBlob = backupBlobArg as WalletBackupContentV1;
|
const backupBlob = backupBlobArg as WalletBackupContentV1;
|
||||||
|
|
||||||
// FIXME: validate version
|
// FIXME: validate version
|
||||||
|
|
||||||
for (const tombstone of backupBlob.tombstones) {
|
for (const tombstone of backupBlob.tombstones) {
|
||||||
await tx.put(Stores.tombstones, {
|
await tx.tombstones.put({
|
||||||
id: tombstone,
|
id: tombstone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -250,14 +250,13 @@ export async function importBackup(
|
|||||||
// FIXME: Validate that the "details pointer" is correct
|
// FIXME: Validate that the "details pointer" is correct
|
||||||
|
|
||||||
for (const backupExchange of backupBlob.exchanges) {
|
for (const backupExchange of backupBlob.exchanges) {
|
||||||
const existingExchange = await tx.get(
|
const existingExchange = await tx.exchanges.get(
|
||||||
Stores.exchanges,
|
|
||||||
backupExchange.base_url,
|
backupExchange.base_url,
|
||||||
);
|
);
|
||||||
if (existingExchange) {
|
if (existingExchange) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await tx.put(Stores.exchanges, {
|
await tx.exchanges.put({
|
||||||
baseUrl: backupExchange.base_url,
|
baseUrl: backupExchange.base_url,
|
||||||
detailsPointer: {
|
detailsPointer: {
|
||||||
currency: backupExchange.currency,
|
currency: backupExchange.currency,
|
||||||
@ -272,7 +271,7 @@ export async function importBackup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const backupExchangeDetails of backupBlob.exchange_details) {
|
for (const backupExchangeDetails of backupBlob.exchange_details) {
|
||||||
const existingExchangeDetails = await tx.get(Stores.exchangeDetails, [
|
const existingExchangeDetails = await tx.exchangeDetails.get([
|
||||||
backupExchangeDetails.base_url,
|
backupExchangeDetails.base_url,
|
||||||
backupExchangeDetails.currency,
|
backupExchangeDetails.currency,
|
||||||
backupExchangeDetails.master_public_key,
|
backupExchangeDetails.master_public_key,
|
||||||
@ -296,7 +295,7 @@ export async function importBackup(
|
|||||||
wireFee: Amounts.parseOrThrow(fee.wire_fee),
|
wireFee: Amounts.parseOrThrow(fee.wire_fee),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await tx.put(Stores.exchangeDetails, {
|
await tx.exchangeDetails.put({
|
||||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
termsOfServiceAcceptedEtag: backupExchangeDetails.tos_etag_accepted,
|
termsOfServiceAcceptedEtag: backupExchangeDetails.tos_etag_accepted,
|
||||||
termsOfServiceText: undefined,
|
termsOfServiceText: undefined,
|
||||||
@ -327,7 +326,7 @@ export async function importBackup(
|
|||||||
const denomPubHash =
|
const denomPubHash =
|
||||||
cryptoComp.denomPubToHash[backupDenomination.denom_pub];
|
cryptoComp.denomPubToHash[backupDenomination.denom_pub];
|
||||||
checkLogicInvariant(!!denomPubHash);
|
checkLogicInvariant(!!denomPubHash);
|
||||||
const existingDenom = await tx.get(Stores.denominations, [
|
const existingDenom = await tx.denominations.get([
|
||||||
backupExchangeDetails.base_url,
|
backupExchangeDetails.base_url,
|
||||||
denomPubHash,
|
denomPubHash,
|
||||||
]);
|
]);
|
||||||
@ -336,7 +335,7 @@ export async function importBackup(
|
|||||||
`importing backup denomination: ${j2s(backupDenomination)}`,
|
`importing backup denomination: ${j2s(backupDenomination)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await tx.put(Stores.denominations, {
|
await tx.denominations.put({
|
||||||
denomPub: backupDenomination.denom_pub,
|
denomPub: backupDenomination.denom_pub,
|
||||||
denomPubHash: denomPubHash,
|
denomPubHash: denomPubHash,
|
||||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
@ -361,7 +360,7 @@ export async function importBackup(
|
|||||||
const compCoin =
|
const compCoin =
|
||||||
cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
|
cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
|
||||||
checkLogicInvariant(!!compCoin);
|
checkLogicInvariant(!!compCoin);
|
||||||
const existingCoin = await tx.get(Stores.coins, compCoin.coinPub);
|
const existingCoin = await tx.coins.get(compCoin.coinPub);
|
||||||
if (!existingCoin) {
|
if (!existingCoin) {
|
||||||
let coinSource: CoinSource;
|
let coinSource: CoinSource;
|
||||||
switch (backupCoin.coin_source.type) {
|
switch (backupCoin.coin_source.type) {
|
||||||
@ -388,7 +387,7 @@ export async function importBackup(
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await tx.put(Stores.coins, {
|
await tx.coins.put({
|
||||||
blindingKey: backupCoin.blinding_key,
|
blindingKey: backupCoin.blinding_key,
|
||||||
coinEvHash: compCoin.coinEvHash,
|
coinEvHash: compCoin.coinEvHash,
|
||||||
coinPriv: backupCoin.coin_priv,
|
coinPriv: backupCoin.coin_priv,
|
||||||
@ -416,7 +415,7 @@ export async function importBackup(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
checkLogicInvariant(!!reservePub);
|
checkLogicInvariant(!!reservePub);
|
||||||
const existingReserve = await tx.get(Stores.reserves, reservePub);
|
const existingReserve = await tx.reserves.get(reservePub);
|
||||||
const instructedAmount = Amounts.parseOrThrow(
|
const instructedAmount = Amounts.parseOrThrow(
|
||||||
backupReserve.instructed_amount,
|
backupReserve.instructed_amount,
|
||||||
);
|
);
|
||||||
@ -429,7 +428,7 @@ export async function importBackup(
|
|||||||
confirmUrl: backupReserve.bank_info.confirm_url,
|
confirmUrl: backupReserve.bank_info.confirm_url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await tx.put(Stores.reserves, {
|
await tx.reserves.put({
|
||||||
currency: instructedAmount.currency,
|
currency: instructedAmount.currency,
|
||||||
instructedAmount,
|
instructedAmount,
|
||||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
@ -467,12 +466,11 @@ export async function importBackup(
|
|||||||
if (tombstoneSet.has(ts)) {
|
if (tombstoneSet.has(ts)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const existingWg = await tx.get(
|
const existingWg = await tx.withdrawalGroups.get(
|
||||||
Stores.withdrawalGroups,
|
|
||||||
backupWg.withdrawal_group_id,
|
backupWg.withdrawal_group_id,
|
||||||
);
|
);
|
||||||
if (!existingWg) {
|
if (!existingWg) {
|
||||||
await tx.put(Stores.withdrawalGroups, {
|
await tx.withdrawalGroups.put({
|
||||||
denomsSel: await getDenomSelStateFromBackup(
|
denomsSel: await getDenomSelStateFromBackup(
|
||||||
tx,
|
tx,
|
||||||
backupExchangeDetails.base_url,
|
backupExchangeDetails.base_url,
|
||||||
@ -504,8 +502,7 @@ export async function importBackup(
|
|||||||
if (tombstoneSet.has(ts)) {
|
if (tombstoneSet.has(ts)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const existingProposal = await tx.get(
|
const existingProposal = await tx.proposals.get(
|
||||||
Stores.proposals,
|
|
||||||
backupProposal.proposal_id,
|
backupProposal.proposal_id,
|
||||||
);
|
);
|
||||||
if (!existingProposal) {
|
if (!existingProposal) {
|
||||||
@ -584,7 +581,7 @@ export async function importBackup(
|
|||||||
contractTermsRaw: backupProposal.contract_terms_raw,
|
contractTermsRaw: backupProposal.contract_terms_raw,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await tx.put(Stores.proposals, {
|
await tx.proposals.put({
|
||||||
claimToken: backupProposal.claim_token,
|
claimToken: backupProposal.claim_token,
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
merchantBaseUrl: backupProposal.merchant_base_url,
|
merchantBaseUrl: backupProposal.merchant_base_url,
|
||||||
@ -610,17 +607,16 @@ export async function importBackup(
|
|||||||
if (tombstoneSet.has(ts)) {
|
if (tombstoneSet.has(ts)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const existingPurchase = await tx.get(
|
const existingPurchase = await tx.purchases.get(
|
||||||
Stores.purchases,
|
|
||||||
backupPurchase.proposal_id,
|
backupPurchase.proposal_id,
|
||||||
);
|
);
|
||||||
if (!existingPurchase) {
|
if (!existingPurchase) {
|
||||||
const refunds: { [refundKey: string]: WalletRefundItem } = {};
|
const refunds: { [refundKey: string]: WalletRefundItem } = {};
|
||||||
for (const backupRefund of backupPurchase.refunds) {
|
for (const backupRefund of backupPurchase.refunds) {
|
||||||
const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
|
const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
|
||||||
const coin = await tx.get(Stores.coins, backupRefund.coin_pub);
|
const coin = await tx.coins.get(backupRefund.coin_pub);
|
||||||
checkBackupInvariant(!!coin);
|
checkBackupInvariant(!!coin);
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.denominations.get([
|
||||||
coin.exchangeBaseUrl,
|
coin.exchangeBaseUrl,
|
||||||
coin.denomPubHash,
|
coin.denomPubHash,
|
||||||
]);
|
]);
|
||||||
@ -724,7 +720,7 @@ export async function importBackup(
|
|||||||
},
|
},
|
||||||
contractTermsRaw: backupPurchase.contract_terms_raw,
|
contractTermsRaw: backupPurchase.contract_terms_raw,
|
||||||
};
|
};
|
||||||
await tx.put(Stores.purchases, {
|
await tx.purchases.put({
|
||||||
proposalId: backupPurchase.proposal_id,
|
proposalId: backupPurchase.proposal_id,
|
||||||
noncePriv: backupPurchase.nonce_priv,
|
noncePriv: backupPurchase.nonce_priv,
|
||||||
noncePub:
|
noncePub:
|
||||||
@ -766,8 +762,7 @@ export async function importBackup(
|
|||||||
if (tombstoneSet.has(ts)) {
|
if (tombstoneSet.has(ts)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const existingRg = await tx.get(
|
const existingRg = await tx.refreshGroups.get(
|
||||||
Stores.refreshGroups,
|
|
||||||
backupRefreshGroup.refresh_group_id,
|
backupRefreshGroup.refresh_group_id,
|
||||||
);
|
);
|
||||||
if (!existingRg) {
|
if (!existingRg) {
|
||||||
@ -800,7 +795,7 @@ export async function importBackup(
|
|||||||
| undefined
|
| undefined
|
||||||
)[] = [];
|
)[] = [];
|
||||||
for (const oldCoin of backupRefreshGroup.old_coins) {
|
for (const oldCoin of backupRefreshGroup.old_coins) {
|
||||||
const c = await tx.get(Stores.coins, oldCoin.coin_pub);
|
const c = await tx.coins.get(oldCoin.coin_pub);
|
||||||
checkBackupInvariant(!!c);
|
checkBackupInvariant(!!c);
|
||||||
if (oldCoin.refresh_session) {
|
if (oldCoin.refresh_session) {
|
||||||
const denomSel = await getDenomSelStateFromBackup(
|
const denomSel = await getDenomSelStateFromBackup(
|
||||||
@ -821,7 +816,7 @@ export async function importBackup(
|
|||||||
refreshSessionPerCoin.push(undefined);
|
refreshSessionPerCoin.push(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await tx.put(Stores.refreshGroups, {
|
await tx.refreshGroups.put({
|
||||||
timestampFinished: backupRefreshGroup.timestamp_finish,
|
timestampFinished: backupRefreshGroup.timestamp_finish,
|
||||||
timestampCreated: backupRefreshGroup.timestamp_created,
|
timestampCreated: backupRefreshGroup.timestamp_created,
|
||||||
refreshGroupId: backupRefreshGroup.refresh_group_id,
|
refreshGroupId: backupRefreshGroup.refresh_group_id,
|
||||||
@ -849,14 +844,14 @@ export async function importBackup(
|
|||||||
if (tombstoneSet.has(ts)) {
|
if (tombstoneSet.has(ts)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const existingTip = await tx.get(Stores.tips, backupTip.wallet_tip_id);
|
const existingTip = await tx.tips.get(backupTip.wallet_tip_id);
|
||||||
if (!existingTip) {
|
if (!existingTip) {
|
||||||
const denomsSel = await getDenomSelStateFromBackup(
|
const denomsSel = await getDenomSelStateFromBackup(
|
||||||
tx,
|
tx,
|
||||||
backupTip.exchange_base_url,
|
backupTip.exchange_base_url,
|
||||||
backupTip.selected_denoms,
|
backupTip.selected_denoms,
|
||||||
);
|
);
|
||||||
await tx.put(Stores.tips, {
|
await tx.tips.put({
|
||||||
acceptedTimestamp: backupTip.timestamp_accepted,
|
acceptedTimestamp: backupTip.timestamp_accepted,
|
||||||
createdTimestamp: backupTip.timestamp_created,
|
createdTimestamp: backupTip.timestamp_created,
|
||||||
denomsSel,
|
denomsSel,
|
||||||
@ -884,27 +879,26 @@ export async function importBackup(
|
|||||||
for (const tombstone of backupBlob.tombstones) {
|
for (const tombstone of backupBlob.tombstones) {
|
||||||
const [type, ...rest] = tombstone.split(":");
|
const [type, ...rest] = tombstone.split(":");
|
||||||
if (type === TombstoneTag.DeleteDepositGroup) {
|
if (type === TombstoneTag.DeleteDepositGroup) {
|
||||||
await tx.delete(Stores.depositGroups, rest[0]);
|
await tx.depositGroups.delete(rest[0]);
|
||||||
} else if (type === TombstoneTag.DeletePayment) {
|
} else if (type === TombstoneTag.DeletePayment) {
|
||||||
await tx.delete(Stores.purchases, rest[0]);
|
await tx.purchases.delete(rest[0]);
|
||||||
await tx.delete(Stores.proposals, rest[0]);
|
await tx.proposals.delete(rest[0]);
|
||||||
} else if (type === TombstoneTag.DeleteRefreshGroup) {
|
} else if (type === TombstoneTag.DeleteRefreshGroup) {
|
||||||
await tx.delete(Stores.refreshGroups, rest[0]);
|
await tx.refreshGroups.delete(rest[0]);
|
||||||
} else if (type === TombstoneTag.DeleteRefund) {
|
} else if (type === TombstoneTag.DeleteRefund) {
|
||||||
// Nothing required, will just prevent display
|
// Nothing required, will just prevent display
|
||||||
// in the transactions list
|
// in the transactions list
|
||||||
} else if (type === TombstoneTag.DeleteReserve) {
|
} else if (type === TombstoneTag.DeleteReserve) {
|
||||||
// FIXME: Once we also have account (=kyc) reserves,
|
// FIXME: Once we also have account (=kyc) reserves,
|
||||||
// we need to check if the reserve is an account before deleting here
|
// we need to check if the reserve is an account before deleting here
|
||||||
await tx.delete(Stores.reserves, rest[0]);
|
await tx.reserves.delete(rest[0]);
|
||||||
} else if (type === TombstoneTag.DeleteTip) {
|
} else if (type === TombstoneTag.DeleteTip) {
|
||||||
await tx.delete(Stores.tips, rest[0]);
|
await tx.tips.delete(rest[0]);
|
||||||
} else if (type === TombstoneTag.DeleteWithdrawalGroup) {
|
} else if (type === TombstoneTag.DeleteWithdrawalGroup) {
|
||||||
await tx.delete(Stores.withdrawalGroups, rest[0]);
|
await tx.withdrawalGroups.delete(rest[0]);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`unable to process tombstone of type '${type}'`);
|
logger.warn(`unable to process tombstone of type '${type}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,6 @@ import {
|
|||||||
BackupProviderRecord,
|
BackupProviderRecord,
|
||||||
BackupProviderTerms,
|
BackupProviderTerms,
|
||||||
ConfigRecord,
|
ConfigRecord,
|
||||||
Stores,
|
|
||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
|
import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants";
|
||||||
import {
|
import {
|
||||||
@ -312,18 +311,17 @@ async function runBackupCycleForProvider(
|
|||||||
|
|
||||||
// FIXME: check if the provider is overcharging us!
|
// FIXME: check if the provider is overcharging us!
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.backupProviders],
|
.mktx((x) => ({ backupProviders: x.backupProviders }))
|
||||||
async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const provRec = await tx.get(Stores.backupProviders, provider.baseUrl);
|
const provRec = await tx.backupProviders.get(provider.baseUrl);
|
||||||
checkDbInvariant(!!provRec);
|
checkDbInvariant(!!provRec);
|
||||||
const ids = new Set(provRec.paymentProposalIds);
|
const ids = new Set(provRec.paymentProposalIds);
|
||||||
ids.add(proposalId);
|
ids.add(proposalId);
|
||||||
provRec.paymentProposalIds = Array.from(ids).sort();
|
provRec.paymentProposalIds = Array.from(ids).sort();
|
||||||
provRec.currentPaymentProposalId = proposalId;
|
provRec.currentPaymentProposalId = proposalId;
|
||||||
await tx.put(Stores.backupProviders, provRec);
|
await tx.backupProviders.put(provRec);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (doPay) {
|
if (doPay) {
|
||||||
const confirmRes = await confirmPay(ws, proposalId);
|
const confirmRes = await confirmPay(ws, proposalId);
|
||||||
@ -344,19 +342,18 @@ async function runBackupCycleForProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resp.status === HttpResponseStatus.NoContent) {
|
if (resp.status === HttpResponseStatus.NoContent) {
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.backupProviders],
|
.mktx((x) => ({ backupProviders: x.backupProviders }))
|
||||||
async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const prov = await tx.get(Stores.backupProviders, provider.baseUrl);
|
const prov = await tx.backupProviders.get(provider.baseUrl);
|
||||||
if (!prov) {
|
if (!prov) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prov.lastBackupHash = encodeCrock(currentBackupHash);
|
prov.lastBackupHash = encodeCrock(currentBackupHash);
|
||||||
prov.lastBackupTimestamp = getTimestampNow();
|
prov.lastBackupTimestamp = getTimestampNow();
|
||||||
prov.lastError = undefined;
|
prov.lastError = undefined;
|
||||||
await tx.put(Stores.backupProviders, prov);
|
await tx.backupProviders.put(prov);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,19 +364,18 @@ async function runBackupCycleForProvider(
|
|||||||
const blob = await decryptBackup(backupConfig, backupEnc);
|
const blob = await decryptBackup(backupConfig, backupEnc);
|
||||||
const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
|
const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
|
||||||
await importBackup(ws, blob, cryptoData);
|
await importBackup(ws, blob, cryptoData);
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.backupProviders],
|
.mktx((x) => ({ backupProvider: x.backupProviders }))
|
||||||
async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const prov = await tx.get(Stores.backupProviders, provider.baseUrl);
|
const prov = await tx.backupProvider.get(provider.baseUrl);
|
||||||
if (!prov) {
|
if (!prov) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prov.lastBackupHash = encodeCrock(hash(backupEnc));
|
prov.lastBackupHash = encodeCrock(hash(backupEnc));
|
||||||
prov.lastBackupTimestamp = getTimestampNow();
|
prov.lastBackupTimestamp = getTimestampNow();
|
||||||
prov.lastError = undefined;
|
prov.lastError = undefined;
|
||||||
await tx.put(Stores.backupProviders, prov);
|
await tx.backupProvider.put(prov);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
logger.info("processed existing backup");
|
logger.info("processed existing backup");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -390,14 +386,16 @@ async function runBackupCycleForProvider(
|
|||||||
|
|
||||||
const err = await readTalerErrorResponse(resp);
|
const err = await readTalerErrorResponse(resp);
|
||||||
logger.error(`got error response from backup provider: ${j2s(err)}`);
|
logger.error(`got error response from backup provider: ${j2s(err)}`);
|
||||||
await ws.db.runWithWriteTransaction([Stores.backupProviders], async (tx) => {
|
await ws.db
|
||||||
const prov = await tx.get(Stores.backupProviders, provider.baseUrl);
|
.mktx((x) => ({ backupProvider: x.backupProviders }))
|
||||||
if (!prov) {
|
.runReadWrite(async (tx) => {
|
||||||
return;
|
const prov = await tx.backupProvider.get(provider.baseUrl);
|
||||||
}
|
if (!prov) {
|
||||||
prov.lastError = err;
|
return;
|
||||||
await tx.put(Stores.backupProviders, prov);
|
}
|
||||||
});
|
prov.lastError = err;
|
||||||
|
await tx.backupProvider.put(prov);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -408,7 +406,11 @@ async function runBackupCycleForProvider(
|
|||||||
* 3. Upload the updated backup blob.
|
* 3. Upload the updated backup blob.
|
||||||
*/
|
*/
|
||||||
export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
|
export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
|
||||||
const providers = await ws.db.iter(Stores.backupProviders).toArray();
|
const providers = await ws.db
|
||||||
|
.mktx((x) => ({ backupProviders: x.backupProviders }))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return await tx.backupProviders.iter().toArray();
|
||||||
|
});
|
||||||
logger.trace("got backup providers", providers);
|
logger.trace("got backup providers", providers);
|
||||||
const backupJson = await exportBackup(ws);
|
const backupJson = await exportBackup(ws);
|
||||||
|
|
||||||
@ -472,35 +474,43 @@ export async function addBackupProvider(
|
|||||||
logger.info(`adding backup provider ${j2s(req)}`);
|
logger.info(`adding backup provider ${j2s(req)}`);
|
||||||
await provideBackupState(ws);
|
await provideBackupState(ws);
|
||||||
const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl);
|
const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl);
|
||||||
const oldProv = await ws.db.get(Stores.backupProviders, canonUrl);
|
await ws.db
|
||||||
if (oldProv) {
|
.mktx((x) => ({ backupProviders: x.backupProviders }))
|
||||||
logger.info("old backup provider found");
|
.runReadWrite(async (tx) => {
|
||||||
if (req.activate) {
|
const oldProv = await tx.backupProviders.get(canonUrl);
|
||||||
oldProv.active = true;
|
if (oldProv) {
|
||||||
logger.info("setting existing backup provider to active");
|
logger.info("old backup provider found");
|
||||||
await ws.db.put(Stores.backupProviders, oldProv);
|
if (req.activate) {
|
||||||
}
|
oldProv.active = true;
|
||||||
return;
|
logger.info("setting existing backup provider to active");
|
||||||
}
|
await tx.backupProviders.put(oldProv);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
const termsUrl = new URL("terms", canonUrl);
|
const termsUrl = new URL("terms", canonUrl);
|
||||||
const resp = await ws.http.get(termsUrl.href);
|
const resp = await ws.http.get(termsUrl.href);
|
||||||
const terms = await readSuccessResponseJsonOrThrow(
|
const terms = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
codecForSyncTermsOfServiceResponse(),
|
codecForSyncTermsOfServiceResponse(),
|
||||||
);
|
);
|
||||||
await ws.db.put(Stores.backupProviders, {
|
await ws.db
|
||||||
active: !!req.activate,
|
.mktx((x) => ({ backupProviders: x.backupProviders }))
|
||||||
terms: {
|
.runReadWrite(async (tx) => {
|
||||||
annualFee: terms.annual_fee,
|
await tx.backupProviders.put({
|
||||||
storageLimitInMegabytes: terms.storage_limit_in_megabytes,
|
active: !!req.activate,
|
||||||
supportedProtocolVersion: terms.version,
|
terms: {
|
||||||
},
|
annualFee: terms.annual_fee,
|
||||||
paymentProposalIds: [],
|
storageLimitInMegabytes: terms.storage_limit_in_megabytes,
|
||||||
baseUrl: canonUrl,
|
supportedProtocolVersion: terms.version,
|
||||||
lastError: undefined,
|
},
|
||||||
retryInfo: initRetryInfo(false),
|
paymentProposalIds: [],
|
||||||
uids: [encodeCrock(getRandomBytes(32))],
|
baseUrl: canonUrl,
|
||||||
});
|
lastError: undefined,
|
||||||
|
retryInfo: initRetryInfo(false),
|
||||||
|
uids: [encodeCrock(getRandomBytes(32))],
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeBackupProvider(
|
export async function removeBackupProvider(
|
||||||
@ -654,7 +664,11 @@ export async function getBackupInfo(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
): Promise<BackupInfo> {
|
): Promise<BackupInfo> {
|
||||||
const backupConfig = await provideBackupState(ws);
|
const backupConfig = await provideBackupState(ws);
|
||||||
const providerRecords = await ws.db.iter(Stores.backupProviders).toArray();
|
const providerRecords = await ws.db
|
||||||
|
.mktx((x) => ({ backupProviders: x.backupProviders }))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return await tx.backupProviders.iter().toArray();
|
||||||
|
});
|
||||||
const providers: ProviderInfo[] = [];
|
const providers: ProviderInfo[] = [];
|
||||||
for (const x of providerRecords) {
|
for (const x of providerRecords) {
|
||||||
providers.push({
|
providers.push({
|
||||||
@ -675,13 +689,18 @@ export async function getBackupInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get information about the current state of wallet backups.
|
* Get backup recovery information, including the wallet's
|
||||||
|
* private key.
|
||||||
*/
|
*/
|
||||||
export async function getBackupRecovery(
|
export async function getBackupRecovery(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
): Promise<BackupRecovery> {
|
): Promise<BackupRecovery> {
|
||||||
const bs = await provideBackupState(ws);
|
const bs = await provideBackupState(ws);
|
||||||
const providers = await ws.db.iter(Stores.backupProviders).toArray();
|
const providers = await ws.db
|
||||||
|
.mktx((x) => ({ backupProviders: x.backupProviders }))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return await tx.backupProviders.iter().toArray();
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
providers: providers
|
providers: providers
|
||||||
.filter((x) => x.active)
|
.filter((x) => x.active)
|
||||||
@ -698,12 +717,12 @@ async function backupRecoveryTheirs(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
br: BackupRecovery,
|
br: BackupRecovery,
|
||||||
) {
|
) {
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.config, Stores.backupProviders],
|
.mktx((x) => ({ config: x.config, backupProviders: x.backupProviders }))
|
||||||
async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
let backupStateEntry:
|
let backupStateEntry:
|
||||||
| ConfigRecord<WalletBackupConfState>
|
| ConfigRecord<WalletBackupConfState>
|
||||||
| undefined = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY);
|
| undefined = await tx.config.get(WALLET_BACKUP_STATE_KEY);
|
||||||
checkDbInvariant(!!backupStateEntry);
|
checkDbInvariant(!!backupStateEntry);
|
||||||
backupStateEntry.value.lastBackupNonce = undefined;
|
backupStateEntry.value.lastBackupNonce = undefined;
|
||||||
backupStateEntry.value.lastBackupTimestamp = undefined;
|
backupStateEntry.value.lastBackupTimestamp = undefined;
|
||||||
@ -713,11 +732,11 @@ async function backupRecoveryTheirs(
|
|||||||
backupStateEntry.value.walletRootPub = encodeCrock(
|
backupStateEntry.value.walletRootPub = encodeCrock(
|
||||||
eddsaGetPublic(decodeCrock(br.walletRootPriv)),
|
eddsaGetPublic(decodeCrock(br.walletRootPriv)),
|
||||||
);
|
);
|
||||||
await tx.put(Stores.config, backupStateEntry);
|
await tx.config.put(backupStateEntry);
|
||||||
for (const prov of br.providers) {
|
for (const prov of br.providers) {
|
||||||
const existingProv = await tx.get(Stores.backupProviders, prov.url);
|
const existingProv = await tx.backupProviders.get(prov.url);
|
||||||
if (!existingProv) {
|
if (!existingProv) {
|
||||||
await tx.put(Stores.backupProviders, {
|
await tx.backupProviders.put({
|
||||||
active: true,
|
active: true,
|
||||||
baseUrl: prov.url,
|
baseUrl: prov.url,
|
||||||
paymentProposalIds: [],
|
paymentProposalIds: [],
|
||||||
@ -727,14 +746,13 @@ async function backupRecoveryTheirs(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const providers = await tx.iter(Stores.backupProviders).toArray();
|
const providers = await tx.backupProviders.iter().toArray();
|
||||||
for (const prov of providers) {
|
for (const prov of providers) {
|
||||||
prov.lastBackupTimestamp = undefined;
|
prov.lastBackupTimestamp = undefined;
|
||||||
prov.lastBackupHash = undefined;
|
prov.lastBackupHash = undefined;
|
||||||
await tx.put(Stores.backupProviders, prov);
|
await tx.backupProviders.put(prov);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function backupRecoveryOurs(ws: InternalWalletState, br: BackupRecovery) {
|
async function backupRecoveryOurs(ws: InternalWalletState, br: BackupRecovery) {
|
||||||
@ -746,7 +764,11 @@ export async function loadBackupRecovery(
|
|||||||
br: RecoveryLoadRequest,
|
br: RecoveryLoadRequest,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const bs = await provideBackupState(ws);
|
const bs = await provideBackupState(ws);
|
||||||
const providers = await ws.db.iter(Stores.backupProviders).toArray();
|
const providers = await ws.db
|
||||||
|
.mktx((x) => ({ backupProviders: x.backupProviders }))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return await tx.backupProviders.iter().toArray();
|
||||||
|
});
|
||||||
let strategy = br.strategy;
|
let strategy = br.strategy;
|
||||||
if (
|
if (
|
||||||
br.recovery.walletRootPriv != bs.walletRootPriv &&
|
br.recovery.walletRootPriv != bs.walletRootPriv &&
|
||||||
@ -772,12 +794,11 @@ export async function exportBackupEncrypted(
|
|||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
await provideBackupState(ws);
|
await provideBackupState(ws);
|
||||||
const blob = await exportBackup(ws);
|
const blob = await exportBackup(ws);
|
||||||
const bs = await ws.db.runWithWriteTransaction(
|
const bs = await ws.db
|
||||||
[Stores.config],
|
.mktx((x) => ({ config: x.config }))
|
||||||
async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
return await getWalletBackupState(ws, tx);
|
return await getWalletBackupState(ws, tx);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
return encryptBackup(bs, blob);
|
return encryptBackup(bs, blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,9 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Timestamp } from "@gnu-taler/taler-util";
|
import { Timestamp } from "@gnu-taler/taler-util";
|
||||||
import { ConfigRecord, Stores } from "../../db.js";
|
import { ConfigRecord, WalletStoresV1 } from "../../db.js";
|
||||||
import { getRandomBytes, encodeCrock, TransactionHandle } from "../../index.js";
|
import { getRandomBytes, encodeCrock } from "../../index.js";
|
||||||
import { checkDbInvariant } from "../../util/invariants";
|
import { checkDbInvariant } from "../../util/invariants";
|
||||||
|
import { GetReadOnlyAccess } from "../../util/query.js";
|
||||||
|
import { Wallet } from "../../wallet.js";
|
||||||
import { InternalWalletState } from "../state";
|
import { InternalWalletState } from "../state";
|
||||||
|
|
||||||
export interface WalletBackupConfState {
|
export interface WalletBackupConfState {
|
||||||
@ -48,10 +50,13 @@ export const WALLET_BACKUP_STATE_KEY = "walletBackupState";
|
|||||||
export async function provideBackupState(
|
export async function provideBackupState(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
): Promise<WalletBackupConfState> {
|
): Promise<WalletBackupConfState> {
|
||||||
const bs: ConfigRecord<WalletBackupConfState> | undefined = await ws.db.get(
|
const bs: ConfigRecord<WalletBackupConfState> | undefined = await ws.db
|
||||||
Stores.config,
|
.mktx((x) => ({
|
||||||
WALLET_BACKUP_STATE_KEY,
|
config: x.config,
|
||||||
);
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.config.get(WALLET_BACKUP_STATE_KEY);
|
||||||
|
});
|
||||||
if (bs) {
|
if (bs) {
|
||||||
return bs.value;
|
return bs.value;
|
||||||
}
|
}
|
||||||
@ -62,32 +67,36 @@ export async function provideBackupState(
|
|||||||
// FIXME: device ID should be configured when wallet is initialized
|
// FIXME: device ID should be configured when wallet is initialized
|
||||||
// and be based on hostname
|
// and be based on hostname
|
||||||
const deviceId = `wallet-core-${encodeCrock(d)}`;
|
const deviceId = `wallet-core-${encodeCrock(d)}`;
|
||||||
return await ws.db.runWithWriteTransaction([Stores.config], async (tx) => {
|
return await ws.db
|
||||||
let backupStateEntry:
|
.mktx((x) => ({
|
||||||
| ConfigRecord<WalletBackupConfState>
|
config: x.config,
|
||||||
| undefined = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY);
|
}))
|
||||||
if (!backupStateEntry) {
|
.runReadWrite(async (tx) => {
|
||||||
backupStateEntry = {
|
let backupStateEntry:
|
||||||
key: WALLET_BACKUP_STATE_KEY,
|
| ConfigRecord<WalletBackupConfState>
|
||||||
value: {
|
| undefined = await tx.config.get(WALLET_BACKUP_STATE_KEY);
|
||||||
deviceId,
|
if (!backupStateEntry) {
|
||||||
clocks: { [deviceId]: 1 },
|
backupStateEntry = {
|
||||||
walletRootPub: k.pub,
|
key: WALLET_BACKUP_STATE_KEY,
|
||||||
walletRootPriv: k.priv,
|
value: {
|
||||||
lastBackupPlainHash: undefined,
|
deviceId,
|
||||||
},
|
clocks: { [deviceId]: 1 },
|
||||||
};
|
walletRootPub: k.pub,
|
||||||
await tx.put(Stores.config, backupStateEntry);
|
walletRootPriv: k.priv,
|
||||||
}
|
lastBackupPlainHash: undefined,
|
||||||
return backupStateEntry.value;
|
},
|
||||||
});
|
};
|
||||||
|
await tx.config.put(backupStateEntry);
|
||||||
|
}
|
||||||
|
return backupStateEntry.value;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWalletBackupState(
|
export async function getWalletBackupState(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tx: TransactionHandle<typeof Stores.config>,
|
tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>,
|
||||||
): Promise<WalletBackupConfState> {
|
): Promise<WalletBackupConfState> {
|
||||||
let bs = await tx.get(Stores.config, WALLET_BACKUP_STATE_KEY);
|
const bs = await tx.config.get(WALLET_BACKUP_STATE_KEY);
|
||||||
checkDbInvariant(!!bs, "wallet backup state should be in DB");
|
checkDbInvariant(!!bs, "wallet backup state should be in DB");
|
||||||
return bs.value;
|
return bs.value;
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,10 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { AmountJson, BalancesResponse, Amounts } from "@gnu-taler/taler-util";
|
import { AmountJson, BalancesResponse, Amounts, Logger } from "@gnu-taler/taler-util";
|
||||||
import { Stores, CoinStatus } from "../db.js";
|
|
||||||
import { TransactionHandle } from "../index.js";
|
import { CoinStatus, WalletStoresV1 } from "../db.js";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
import { InternalWalletState } from "./state.js";
|
import { InternalWalletState } from "./state.js";
|
||||||
|
|
||||||
const logger = new Logger("withdraw.ts");
|
const logger = new Logger("withdraw.ts");
|
||||||
@ -36,13 +36,12 @@ interface WalletBalance {
|
|||||||
*/
|
*/
|
||||||
export async function getBalancesInsideTransaction(
|
export async function getBalancesInsideTransaction(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tx: TransactionHandle<
|
tx: GetReadOnlyAccess<{
|
||||||
| typeof Stores.reserves
|
reserves: typeof WalletStoresV1.reserves;
|
||||||
| typeof Stores.coins
|
coins: typeof WalletStoresV1.coins;
|
||||||
| typeof Stores.reserves
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
| typeof Stores.refreshGroups
|
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
|
||||||
| typeof Stores.withdrawalGroups
|
}>,
|
||||||
>,
|
|
||||||
): Promise<BalancesResponse> {
|
): Promise<BalancesResponse> {
|
||||||
const balanceStore: Record<string, WalletBalance> = {};
|
const balanceStore: Record<string, WalletBalance> = {};
|
||||||
|
|
||||||
@ -63,7 +62,7 @@ export async function getBalancesInsideTransaction(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Initialize balance to zero, even if we didn't start withdrawing yet.
|
// Initialize balance to zero, even if we didn't start withdrawing yet.
|
||||||
await tx.iter(Stores.reserves).forEach((r) => {
|
await tx.reserves.iter().forEach((r) => {
|
||||||
const b = initBalance(r.currency);
|
const b = initBalance(r.currency);
|
||||||
if (!r.initialWithdrawalStarted) {
|
if (!r.initialWithdrawalStarted) {
|
||||||
b.pendingIncoming = Amounts.add(
|
b.pendingIncoming = Amounts.add(
|
||||||
@ -73,7 +72,7 @@ export async function getBalancesInsideTransaction(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.coins).forEach((c) => {
|
await tx.coins.iter().forEach((c) => {
|
||||||
// Only count fresh coins, as dormant coins will
|
// Only count fresh coins, as dormant coins will
|
||||||
// already be in a refresh session.
|
// already be in a refresh session.
|
||||||
if (c.status === CoinStatus.Fresh) {
|
if (c.status === CoinStatus.Fresh) {
|
||||||
@ -82,7 +81,7 @@ export async function getBalancesInsideTransaction(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.refreshGroups).forEach((r) => {
|
await tx.refreshGroups.iter().forEach((r) => {
|
||||||
// Don't count finished refreshes, since the refresh already resulted
|
// Don't count finished refreshes, since the refresh already resulted
|
||||||
// in coins being added to the wallet.
|
// in coins being added to the wallet.
|
||||||
if (r.timestampFinished) {
|
if (r.timestampFinished) {
|
||||||
@ -108,7 +107,7 @@ export async function getBalancesInsideTransaction(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.iter(Stores.withdrawalGroups).forEach((wds) => {
|
await tx.withdrawalGroups.iter().forEach((wds) => {
|
||||||
if (wds.timestampFinish) {
|
if (wds.timestampFinish) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -147,18 +146,17 @@ export async function getBalances(
|
|||||||
): Promise<BalancesResponse> {
|
): Promise<BalancesResponse> {
|
||||||
logger.trace("starting to compute balance");
|
logger.trace("starting to compute balance");
|
||||||
|
|
||||||
const wbal = await ws.db.runWithReadTransaction(
|
const wbal = await ws.db
|
||||||
[
|
.mktx((x) => ({
|
||||||
Stores.coins,
|
coins: x.coins,
|
||||||
Stores.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
Stores.reserves,
|
reserves: x.reserves,
|
||||||
Stores.purchases,
|
purchases: x.purchases,
|
||||||
Stores.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
],
|
}))
|
||||||
async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
return getBalancesInsideTransaction(ws, tx);
|
return getBalancesInsideTransaction(ws, tx);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
logger.trace("finished computing wallet balance");
|
logger.trace("finished computing wallet balance");
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { ExchangeRecord, Stores } from "../db.js";
|
import { ExchangeRecord } from "../db.js";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import { InternalWalletState } from "./state.js";
|
import { InternalWalletState } from "./state.js";
|
||||||
@ -38,37 +38,44 @@ export async function getExchangeTrust(
|
|||||||
): Promise<TrustInfo> {
|
): Promise<TrustInfo> {
|
||||||
let isTrusted = false;
|
let isTrusted = false;
|
||||||
let isAudited = false;
|
let isAudited = false;
|
||||||
const exchangeDetails = await ws.db.runWithReadTransaction(
|
|
||||||
[Stores.exchangeDetails, Stores.exchanges],
|
|
||||||
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) {
|
return await ws.db
|
||||||
const auditorTrustRecord = await ws.db.getIndexed(
|
.mktx((x) => ({
|
||||||
Stores.auditorTrustStore.auditorPubIndex,
|
exchanges: x.exchanges,
|
||||||
auditor.auditor_pub,
|
exchangeDetails: x.exchangeDetails,
|
||||||
);
|
exchangesTrustStore: x.exchangeTrust,
|
||||||
if (auditorTrustRecord && auditorTrustRecord.uids.length > 0) {
|
auditorTrust: x.auditorTrust,
|
||||||
isAudited = true;
|
}))
|
||||||
break;
|
.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";
|
} from "./pay";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import { DepositGroupRecord, Stores } from "../db.js";
|
import { DepositGroupRecord } from "../db.js";
|
||||||
|
|
||||||
import { guardOperationException } from "./errors.js";
|
import { guardOperationException } from "./errors.js";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
|
|
||||||
@ -116,12 +117,17 @@ async function resetDepositGroupRetry(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
depositGroupId: string,
|
depositGroupId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.mutate(Stores.depositGroups, depositGroupId, (x) => {
|
await ws.db
|
||||||
if (x.retryInfo.active) {
|
.mktx((x) => ({
|
||||||
x.retryInfo = initRetryInfo();
|
depositGroups: x.depositGroups,
|
||||||
}
|
}))
|
||||||
return x;
|
.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(
|
async function incrementDepositRetry(
|
||||||
@ -129,19 +135,21 @@ async function incrementDepositRetry(
|
|||||||
depositGroupId: string,
|
depositGroupId: string,
|
||||||
err: TalerErrorDetails | undefined,
|
err: TalerErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => {
|
await ws.db
|
||||||
const r = await tx.get(Stores.depositGroups, depositGroupId);
|
.mktx((x) => ({ depositGroups: x.depositGroups }))
|
||||||
if (!r) {
|
.runReadWrite(async (tx) => {
|
||||||
return;
|
const r = await tx.depositGroups.get(depositGroupId);
|
||||||
}
|
if (!r) {
|
||||||
if (!r.retryInfo) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
if (!r.retryInfo) {
|
||||||
r.retryInfo.retryCounter++;
|
return;
|
||||||
updateRetryInfoTimeout(r.retryInfo);
|
}
|
||||||
r.lastError = err;
|
r.retryInfo.retryCounter++;
|
||||||
await tx.put(Stores.depositGroups, r);
|
updateRetryInfoTimeout(r.retryInfo);
|
||||||
});
|
r.lastError = err;
|
||||||
|
await tx.depositGroups.put(r);
|
||||||
|
});
|
||||||
if (err) {
|
if (err) {
|
||||||
ws.notify({ type: NotificationType.DepositOperationError, error: err });
|
ws.notify({ type: NotificationType.DepositOperationError, error: err });
|
||||||
}
|
}
|
||||||
@ -170,7 +178,13 @@ async function processDepositGroupImpl(
|
|||||||
if (forceNow) {
|
if (forceNow) {
|
||||||
await resetDepositGroupRetry(ws, depositGroupId);
|
await resetDepositGroupRetry(ws, depositGroupId);
|
||||||
}
|
}
|
||||||
const depositGroup = await ws.db.get(Stores.depositGroups, depositGroupId);
|
const depositGroup = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
depositGroups: x.depositGroups,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.depositGroups.get(depositGroupId);
|
||||||
|
});
|
||||||
if (!depositGroup) {
|
if (!depositGroup) {
|
||||||
logger.warn(`deposit group ${depositGroupId} not found`);
|
logger.warn(`deposit group ${depositGroupId} not found`);
|
||||||
return;
|
return;
|
||||||
@ -213,32 +227,38 @@ async function processDepositGroupImpl(
|
|||||||
merchant_pub: depositGroup.merchantPub,
|
merchant_pub: depositGroup.merchantPub,
|
||||||
});
|
});
|
||||||
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
|
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
|
||||||
await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => {
|
await ws.db
|
||||||
const dg = await tx.get(Stores.depositGroups, depositGroupId);
|
.mktx((x) => ({ depositGroups: x.depositGroups }))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const dg = await tx.depositGroups.get(depositGroupId);
|
||||||
|
if (!dg) {
|
||||||
|
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) {
|
if (!dg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dg.depositedPerCoin[i] = true;
|
let allDeposited = true;
|
||||||
await tx.put(Stores.depositGroups, dg);
|
for (const d of depositGroup.depositedPerCoin) {
|
||||||
});
|
if (!d) {
|
||||||
}
|
allDeposited = false;
|
||||||
|
}
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
if (allDeposited) {
|
||||||
if (allDeposited) {
|
dg.timestampFinished = getTimestampNow();
|
||||||
dg.timestampFinished = getTimestampNow();
|
await tx.depositGroups.put(dg);
|
||||||
await tx.put(Stores.depositGroups, dg);
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trackDepositGroup(
|
export async function trackDepositGroup(
|
||||||
@ -249,10 +269,13 @@ export async function trackDepositGroup(
|
|||||||
status: number;
|
status: number;
|
||||||
body: any;
|
body: any;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
const depositGroup = await ws.db.get(
|
const depositGroup = await ws.db
|
||||||
Stores.depositGroups,
|
.mktx((x) => ({
|
||||||
req.depositGroupId,
|
depositGroups: x.depositGroups,
|
||||||
);
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.depositGroups.get(req.depositGroupId);
|
||||||
|
});
|
||||||
if (!depositGroup) {
|
if (!depositGroup) {
|
||||||
throw Error("deposit group not found");
|
throw Error("deposit group not found");
|
||||||
}
|
}
|
||||||
@ -306,23 +329,26 @@ export async function createDepositGroup(
|
|||||||
|
|
||||||
const amount = Amounts.parseOrThrow(req.amount);
|
const amount = Amounts.parseOrThrow(req.amount);
|
||||||
|
|
||||||
const allExchanges = await ws.db.iter(Stores.exchanges).toArray();
|
|
||||||
const exchangeInfos: { url: string; master_pub: string }[] = [];
|
const exchangeInfos: { url: string; master_pub: string }[] = [];
|
||||||
for (const e of allExchanges) {
|
|
||||||
const details = await ws.db.runWithReadTransaction(
|
await ws.db
|
||||||
[Stores.exchanges, Stores.exchangeDetails],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
exchanges: x.exchanges,
|
||||||
return getExchangeDetails(tx, e.baseUrl);
|
exchangeDetails: x.exchangeDetails,
|
||||||
},
|
}))
|
||||||
);
|
.runReadOnly(async (tx) => {
|
||||||
if (!details) {
|
const allExchanges = await tx.exchanges.iter().toArray();
|
||||||
continue;
|
for (const e of allExchanges) {
|
||||||
}
|
const details = await getExchangeDetails(tx, e.baseUrl);
|
||||||
exchangeInfos.push({
|
if (!details) {
|
||||||
master_pub: details.masterPublicKey,
|
continue;
|
||||||
url: e.baseUrl,
|
}
|
||||||
|
exchangeInfos.push({
|
||||||
|
master_pub: details.masterPublicKey,
|
||||||
|
url: e.baseUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = getTimestampNow();
|
const timestamp = getTimestampNow();
|
||||||
const timestampRound = timestampTruncateToSecond(timestamp);
|
const timestampRound = timestampTruncateToSecond(timestamp);
|
||||||
@ -421,20 +447,17 @@ export async function createDepositGroup(
|
|||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[
|
.mktx((x) => ({
|
||||||
Stores.depositGroups,
|
depositGroups: x.depositGroups,
|
||||||
Stores.coins,
|
coins: x.coins,
|
||||||
Stores.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
Stores.denominations,
|
denominations: x.denominations,
|
||||||
],
|
}))
|
||||||
async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
await applyCoinSpend(ws, tx, payCoinSel);
|
await applyCoinSpend(ws, tx, payCoinSel);
|
||||||
await tx.put(Stores.depositGroups, depositGroup);
|
await tx.depositGroups.put(depositGroup);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await ws.db.put(Stores.depositGroups, depositGroup);
|
|
||||||
|
|
||||||
return { depositGroupId };
|
return { depositGroupId };
|
||||||
}
|
}
|
||||||
|
@ -41,13 +41,13 @@ import {
|
|||||||
import {
|
import {
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
DenominationStatus,
|
DenominationStatus,
|
||||||
Stores,
|
|
||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
ExchangeUpdateStatus,
|
ExchangeUpdateStatus,
|
||||||
WireFee,
|
WireFee,
|
||||||
ExchangeUpdateReason,
|
ExchangeUpdateReason,
|
||||||
ExchangeDetailsRecord,
|
ExchangeDetailsRecord,
|
||||||
WireInfo,
|
WireInfo,
|
||||||
|
WalletStoresV1,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import {
|
import {
|
||||||
URL,
|
URL,
|
||||||
@ -73,7 +73,7 @@ import {
|
|||||||
} from "./versions.js";
|
} from "./versions.js";
|
||||||
import { HttpRequestLibrary } from "../util/http.js";
|
import { HttpRequestLibrary } from "../util/http.js";
|
||||||
import { CryptoApi } from "../crypto/workers/cryptoApi.js";
|
import { CryptoApi } from "../crypto/workers/cryptoApi.js";
|
||||||
import { TransactionHandle } from "../util/query.js";
|
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
|
||||||
|
|
||||||
const logger = new Logger("exchanges.ts");
|
const logger = new Logger("exchanges.ts");
|
||||||
|
|
||||||
@ -108,15 +108,17 @@ async function handleExchangeUpdateError(
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
err: TalerErrorDetails,
|
err: TalerErrorDetails,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
|
await ws.db
|
||||||
const exchange = await tx.get(Stores.exchanges, baseUrl);
|
.mktx((x) => ({ exchanges: x.exchanges }))
|
||||||
if (!exchange) {
|
.runReadOnly(async (tx) => {
|
||||||
return;
|
const exchange = await tx.exchanges.get(baseUrl);
|
||||||
}
|
if (!exchange) {
|
||||||
exchange.retryInfo.retryCounter++;
|
return;
|
||||||
updateRetryInfoTimeout(exchange.retryInfo);
|
}
|
||||||
exchange.lastError = err;
|
exchange.retryInfo.retryCounter++;
|
||||||
});
|
updateRetryInfoTimeout(exchange.retryInfo);
|
||||||
|
exchange.lastError = err;
|
||||||
|
});
|
||||||
if (err) {
|
if (err) {
|
||||||
ws.notify({ type: NotificationType.ExchangeOperationError, error: err });
|
ws.notify({ type: NotificationType.ExchangeOperationError, error: err });
|
||||||
}
|
}
|
||||||
@ -153,12 +155,13 @@ async function downloadExchangeWithTermsOfService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getExchangeDetails(
|
export async function getExchangeDetails(
|
||||||
tx: TransactionHandle<
|
tx: GetReadOnlyAccess<{
|
||||||
typeof Stores.exchanges | typeof Stores.exchangeDetails
|
exchanges: typeof WalletStoresV1.exchanges;
|
||||||
>,
|
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
||||||
|
}>,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
): Promise<ExchangeDetailsRecord | undefined> {
|
): Promise<ExchangeDetailsRecord | undefined> {
|
||||||
const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
|
const r = await tx.exchanges.get(exchangeBaseUrl);
|
||||||
if (!r) {
|
if (!r) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -167,28 +170,32 @@ export async function getExchangeDetails(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { currency, masterPublicKey } = dp;
|
const { currency, masterPublicKey } = dp;
|
||||||
return await tx.get(Stores.exchangeDetails, [
|
return await tx.exchangeDetails.get([r.baseUrl, currency, masterPublicKey]);
|
||||||
r.baseUrl,
|
|
||||||
currency,
|
|
||||||
masterPublicKey,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
|
||||||
|
db.mktx((x) => ({
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
}));
|
||||||
|
|
||||||
export async function acceptExchangeTermsOfService(
|
export async function acceptExchangeTermsOfService(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
etag: string | undefined,
|
etag: string | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.exchanges, Stores.exchangeDetails],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
exchanges: x.exchanges,
|
||||||
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
const d = await getExchangeDetails(tx, exchangeBaseUrl);
|
const d = await getExchangeDetails(tx, exchangeBaseUrl);
|
||||||
if (d) {
|
if (d) {
|
||||||
d.termsOfServiceAcceptedEtag = etag;
|
d.termsOfServiceAcceptedEtag = etag;
|
||||||
await tx.put(Stores.exchangeDetails, d);
|
await tx.exchangeDetails.put(d);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateWireInfo(
|
async function validateWireInfo(
|
||||||
@ -284,21 +291,24 @@ async function provideExchangeRecord(
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
): Promise<ExchangeRecord> {
|
): Promise<ExchangeRecord> {
|
||||||
let r = await ws.db.get(Stores.exchanges, baseUrl);
|
return await ws.db
|
||||||
if (!r) {
|
.mktx((x) => ({ exchanges: x.exchanges }))
|
||||||
const newExchangeRecord: ExchangeRecord = {
|
.runReadWrite(async (tx) => {
|
||||||
permanent: true,
|
let r = await tx.exchanges.get(baseUrl);
|
||||||
baseUrl: baseUrl,
|
if (!r) {
|
||||||
updateStatus: ExchangeUpdateStatus.FetchKeys,
|
r = {
|
||||||
updateStarted: now,
|
permanent: true,
|
||||||
updateReason: ExchangeUpdateReason.Initial,
|
baseUrl: baseUrl,
|
||||||
retryInfo: initRetryInfo(false),
|
updateStatus: ExchangeUpdateStatus.FetchKeys,
|
||||||
detailsPointer: undefined,
|
updateStarted: now,
|
||||||
};
|
updateReason: ExchangeUpdateReason.Initial,
|
||||||
await ws.db.put(Stores.exchanges, newExchangeRecord);
|
retryInfo: initRetryInfo(false),
|
||||||
r = newExchangeRecord;
|
detailsPointer: undefined,
|
||||||
}
|
};
|
||||||
return r;
|
await tx.exchanges.put(r);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExchangeKeysDownloadResult {
|
interface ExchangeKeysDownloadResult {
|
||||||
@ -427,16 +437,17 @@ async function updateExchangeFromUrlImpl(
|
|||||||
|
|
||||||
let recoupGroupId: string | undefined = undefined;
|
let recoupGroupId: string | undefined = undefined;
|
||||||
|
|
||||||
const updated = await ws.db.runWithWriteTransaction(
|
const updated = await ws.db
|
||||||
[
|
.mktx((x) => ({
|
||||||
Stores.exchanges,
|
exchanges: x.exchanges,
|
||||||
Stores.exchangeDetails,
|
exchangeDetails: x.exchangeDetails,
|
||||||
Stores.denominations,
|
denominations: x.denominations,
|
||||||
Stores.recoupGroups,
|
coins: x.coins,
|
||||||
Stores.coins,
|
refreshGroups: x.refreshGroups,
|
||||||
],
|
recoupGroups: x.recoupGroups,
|
||||||
async (tx) => {
|
}))
|
||||||
const r = await tx.get(Stores.exchanges, baseUrl);
|
.runReadWrite(async (tx) => {
|
||||||
|
const r = await tx.exchanges.get(baseUrl);
|
||||||
if (!r) {
|
if (!r) {
|
||||||
logger.warn(`exchange ${baseUrl} no longer present`);
|
logger.warn(`exchange ${baseUrl} no longer present`);
|
||||||
return;
|
return;
|
||||||
@ -473,18 +484,18 @@ async function updateExchangeFromUrlImpl(
|
|||||||
// FIXME: only change if pointer really changed
|
// FIXME: only change if pointer really changed
|
||||||
updateClock: getTimestampNow(),
|
updateClock: getTimestampNow(),
|
||||||
};
|
};
|
||||||
await tx.put(Stores.exchanges, r);
|
await tx.exchanges.put(r);
|
||||||
await tx.put(Stores.exchangeDetails, details);
|
await tx.exchangeDetails.put(details);
|
||||||
|
|
||||||
for (const currentDenom of keysInfo.currentDenominations) {
|
for (const currentDenom of keysInfo.currentDenominations) {
|
||||||
const oldDenom = await tx.get(Stores.denominations, [
|
const oldDenom = await tx.denominations.get([
|
||||||
baseUrl,
|
baseUrl,
|
||||||
currentDenom.denomPubHash,
|
currentDenom.denomPubHash,
|
||||||
]);
|
]);
|
||||||
if (oldDenom) {
|
if (oldDenom) {
|
||||||
// FIXME: Do consistency check
|
// FIXME: Do consistency check
|
||||||
} else {
|
} else {
|
||||||
await tx.put(Stores.denominations, currentDenom);
|
await tx.denominations.put(currentDenom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,7 +504,7 @@ async function updateExchangeFromUrlImpl(
|
|||||||
const newlyRevokedCoinPubs: string[] = [];
|
const newlyRevokedCoinPubs: string[] = [];
|
||||||
logger.trace("recoup list from exchange", recoupDenomList);
|
logger.trace("recoup list from exchange", recoupDenomList);
|
||||||
for (const recoupInfo of recoupDenomList) {
|
for (const recoupInfo of recoupDenomList) {
|
||||||
const oldDenom = await tx.get(Stores.denominations, [
|
const oldDenom = await tx.denominations.get([
|
||||||
r.baseUrl,
|
r.baseUrl,
|
||||||
recoupInfo.h_denom_pub,
|
recoupInfo.h_denom_pub,
|
||||||
]);
|
]);
|
||||||
@ -509,9 +520,9 @@ async function updateExchangeFromUrlImpl(
|
|||||||
}
|
}
|
||||||
logger.trace("revoking denom", recoupInfo.h_denom_pub);
|
logger.trace("revoking denom", recoupInfo.h_denom_pub);
|
||||||
oldDenom.isRevoked = true;
|
oldDenom.isRevoked = true;
|
||||||
await tx.put(Stores.denominations, oldDenom);
|
await tx.denominations.put(oldDenom);
|
||||||
const affectedCoins = await tx
|
const affectedCoins = await tx.coins.indexes.byDenomPubHash
|
||||||
.iterIndexed(Stores.coins.denomPubHashIndex, recoupInfo.h_denom_pub)
|
.iter(recoupInfo.h_denom_pub)
|
||||||
.toArray();
|
.toArray();
|
||||||
for (const ac of affectedCoins) {
|
for (const ac of affectedCoins) {
|
||||||
newlyRevokedCoinPubs.push(ac.coinPub);
|
newlyRevokedCoinPubs.push(ac.coinPub);
|
||||||
@ -525,8 +536,7 @@ async function updateExchangeFromUrlImpl(
|
|||||||
exchange: r,
|
exchange: r,
|
||||||
exchangeDetails: details,
|
exchangeDetails: details,
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (recoupGroupId) {
|
if (recoupGroupId) {
|
||||||
// Asynchronously start recoup. This doesn't need to finish
|
// Asynchronously start recoup. This doesn't need to finish
|
||||||
@ -553,12 +563,11 @@ export async function getExchangePaytoUri(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// We do the update here, since the exchange might not even exist
|
// We do the update here, since the exchange might not even exist
|
||||||
// yet in our database.
|
// yet in our database.
|
||||||
const details = await ws.db.runWithReadTransaction(
|
const details = await getExchangeDetails
|
||||||
[Stores.exchangeDetails, Stores.exchanges],
|
.makeContext(ws.db)
|
||||||
async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
return getExchangeDetails(tx, exchangeBaseUrl);
|
return getExchangeDetails(tx, exchangeBaseUrl);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
const accounts = details?.wireInfo.accounts ?? [];
|
const accounts = details?.wireInfo.accounts ?? [];
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
const res = parsePaytoUri(account.payto_uri);
|
const res = parsePaytoUri(account.payto_uri);
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -21,8 +21,8 @@ import {
|
|||||||
ExchangeUpdateStatus,
|
ExchangeUpdateStatus,
|
||||||
ProposalStatus,
|
ProposalStatus,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
Stores,
|
|
||||||
AbortStatus,
|
AbortStatus,
|
||||||
|
WalletStoresV1,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import {
|
import {
|
||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
@ -37,10 +37,10 @@ import {
|
|||||||
getDurationRemaining,
|
getDurationRemaining,
|
||||||
durationMin,
|
durationMin,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { TransactionHandle } from "../util/query";
|
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { getBalancesInsideTransaction } from "./balance";
|
import { getBalancesInsideTransaction } from "./balance";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
|
|
||||||
function updateRetryDelay(
|
function updateRetryDelay(
|
||||||
oldDelay: Duration,
|
oldDelay: Duration,
|
||||||
@ -53,14 +53,15 @@ function updateRetryDelay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherExchangePending(
|
async function gatherExchangePending(
|
||||||
tx: TransactionHandle<
|
tx: GetReadOnlyAccess<{
|
||||||
typeof Stores.exchanges | typeof Stores.exchangeDetails
|
exchanges: typeof WalletStoresV1.exchanges;
|
||||||
>,
|
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
||||||
|
}>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.exchanges).forEachAsync(async (e) => {
|
await tx.exchanges.iter().forEachAsync(async (e) => {
|
||||||
switch (e.updateStatus) {
|
switch (e.updateStatus) {
|
||||||
case ExchangeUpdateStatus.Finished:
|
case ExchangeUpdateStatus.Finished:
|
||||||
if (e.lastError) {
|
if (e.lastError) {
|
||||||
@ -153,13 +154,13 @@ async function gatherExchangePending(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherReservePending(
|
async function gatherReservePending(
|
||||||
tx: TransactionHandle<typeof Stores.reserves>,
|
tx: GetReadOnlyAccess<{ reserves: typeof WalletStoresV1.reserves }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// FIXME: this should be optimized by using an index for "onlyDue==true".
|
// FIXME: this should be optimized by using an index for "onlyDue==true".
|
||||||
await tx.iter(Stores.reserves).forEach((reserve) => {
|
await tx.reserves.iter().forEach((reserve) => {
|
||||||
const reserveType = reserve.bankInfo
|
const reserveType = reserve.bankInfo
|
||||||
? ReserveType.TalerBankWithdraw
|
? ReserveType.TalerBankWithdraw
|
||||||
: ReserveType.Manual;
|
: ReserveType.Manual;
|
||||||
@ -207,12 +208,12 @@ async function gatherReservePending(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherRefreshPending(
|
async function gatherRefreshPending(
|
||||||
tx: TransactionHandle<typeof Stores.refreshGroups>,
|
tx: GetReadOnlyAccess<{ refreshGroups: typeof WalletStoresV1.refreshGroups }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.refreshGroups).forEach((r) => {
|
await tx.refreshGroups.iter().forEach((r) => {
|
||||||
if (r.timestampFinished) {
|
if (r.timestampFinished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -236,12 +237,15 @@ async function gatherRefreshPending(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherWithdrawalPending(
|
async function gatherWithdrawalPending(
|
||||||
tx: TransactionHandle<typeof Stores.withdrawalGroups>,
|
tx: GetReadOnlyAccess<{
|
||||||
|
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
|
||||||
|
planchets: typeof WalletStoresV1.planchets,
|
||||||
|
}>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => {
|
await tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
|
||||||
if (wsr.timestampFinish) {
|
if (wsr.timestampFinish) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -255,8 +259,8 @@ async function gatherWithdrawalPending(
|
|||||||
}
|
}
|
||||||
let numCoinsWithdrawn = 0;
|
let numCoinsWithdrawn = 0;
|
||||||
let numCoinsTotal = 0;
|
let numCoinsTotal = 0;
|
||||||
await tx
|
await tx.planchets.indexes.byGroup
|
||||||
.iterIndexed(Stores.planchets.byGroup, wsr.withdrawalGroupId)
|
.iter(wsr.withdrawalGroupId)
|
||||||
.forEach((x) => {
|
.forEach((x) => {
|
||||||
numCoinsTotal++;
|
numCoinsTotal++;
|
||||||
if (x.withdrawalDone) {
|
if (x.withdrawalDone) {
|
||||||
@ -276,12 +280,12 @@ async function gatherWithdrawalPending(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherProposalPending(
|
async function gatherProposalPending(
|
||||||
tx: TransactionHandle<typeof Stores.proposals>,
|
tx: GetReadOnlyAccess<{ proposals: typeof WalletStoresV1.proposals }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.proposals).forEach((proposal) => {
|
await tx.proposals.iter().forEach((proposal) => {
|
||||||
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
|
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
|
||||||
if (onlyDue) {
|
if (onlyDue) {
|
||||||
return;
|
return;
|
||||||
@ -327,12 +331,12 @@ async function gatherProposalPending(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherTipPending(
|
async function gatherTipPending(
|
||||||
tx: TransactionHandle<typeof Stores.tips>,
|
tx: GetReadOnlyAccess<{ tips: typeof WalletStoresV1.tips }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.tips).forEach((tip) => {
|
await tx.tips.iter().forEach((tip) => {
|
||||||
if (tip.pickedUpTimestamp) {
|
if (tip.pickedUpTimestamp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -357,12 +361,12 @@ async function gatherTipPending(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherPurchasePending(
|
async function gatherPurchasePending(
|
||||||
tx: TransactionHandle<typeof Stores.purchases>,
|
tx: GetReadOnlyAccess<{ purchases: typeof WalletStoresV1.purchases }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.purchases).forEach((pr) => {
|
await tx.purchases.iter().forEach((pr) => {
|
||||||
if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) {
|
if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) {
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
resp.nextRetryDelay,
|
resp.nextRetryDelay,
|
||||||
@ -400,12 +404,12 @@ async function gatherPurchasePending(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherRecoupPending(
|
async function gatherRecoupPending(
|
||||||
tx: TransactionHandle<typeof Stores.recoupGroups>,
|
tx: GetReadOnlyAccess<{ recoupGroups: typeof WalletStoresV1.recoupGroups }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.recoupGroups).forEach((rg) => {
|
await tx.recoupGroups.iter().forEach((rg) => {
|
||||||
if (rg.timestampFinished) {
|
if (rg.timestampFinished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -428,12 +432,12 @@ async function gatherRecoupPending(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gatherDepositPending(
|
async function gatherDepositPending(
|
||||||
tx: TransactionHandle<typeof Stores.depositGroups>,
|
tx: GetReadOnlyAccess<{ depositGroups: typeof WalletStoresV1.depositGroups }>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
resp: PendingOperationsResponse,
|
resp: PendingOperationsResponse,
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.depositGroups).forEach((dg) => {
|
await tx.depositGroups.iter().forEach((dg) => {
|
||||||
if (dg.timestampFinished) {
|
if (dg.timestampFinished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -460,20 +464,20 @@ export async function getPendingOperations(
|
|||||||
{ onlyDue = false } = {},
|
{ onlyDue = false } = {},
|
||||||
): Promise<PendingOperationsResponse> {
|
): Promise<PendingOperationsResponse> {
|
||||||
const now = getTimestampNow();
|
const now = getTimestampNow();
|
||||||
return await ws.db.runWithReadTransaction(
|
return await ws.db.mktx((x) => ({
|
||||||
[
|
exchanges: x.exchanges,
|
||||||
Stores.exchanges,
|
exchangeDetails: x.exchangeDetails,
|
||||||
Stores.reserves,
|
reserves: x.reserves,
|
||||||
Stores.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
Stores.coins,
|
coins: x.coins,
|
||||||
Stores.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
Stores.proposals,
|
proposals: x.proposals,
|
||||||
Stores.tips,
|
tips: x.tips,
|
||||||
Stores.purchases,
|
purchases: x.purchases,
|
||||||
Stores.recoupGroups,
|
planchets: x.planchets,
|
||||||
Stores.planchets,
|
depositGroups: x.depositGroups,
|
||||||
Stores.depositGroups,
|
recoupGroups: x.recoupGroups,
|
||||||
],
|
})).runReadWrite(
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const walletBalance = await getBalancesInsideTransaction(ws, tx);
|
const walletBalance = await getBalancesInsideTransaction(ws, tx);
|
||||||
const resp: PendingOperationsResponse = {
|
const resp: PendingOperationsResponse = {
|
||||||
|
@ -40,20 +40,19 @@ import {
|
|||||||
RecoupGroupRecord,
|
RecoupGroupRecord,
|
||||||
RefreshCoinSource,
|
RefreshCoinSource,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
Stores,
|
|
||||||
WithdrawCoinSource,
|
WithdrawCoinSource,
|
||||||
|
WalletStoresV1,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
|
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import { TransactionHandle } from "../util/query";
|
|
||||||
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
|
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
|
||||||
import { URL } from "../util/url";
|
import { URL } from "../util/url";
|
||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
|
||||||
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
||||||
import { getReserveRequestTimeout, processReserve } from "./reserves";
|
import { getReserveRequestTimeout, processReserve } from "./reserves";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
|
import { GetReadWriteAccess } from "../util/query.js";
|
||||||
|
|
||||||
const logger = new Logger("operations/recoup.ts");
|
const logger = new Logger("operations/recoup.ts");
|
||||||
|
|
||||||
@ -62,19 +61,23 @@ async function incrementRecoupRetry(
|
|||||||
recoupGroupId: string,
|
recoupGroupId: string,
|
||||||
err: TalerErrorDetails | undefined,
|
err: TalerErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => {
|
await ws.db
|
||||||
const r = await tx.get(Stores.recoupGroups, recoupGroupId);
|
.mktx((x) => ({
|
||||||
if (!r) {
|
recoupGroups: x.recoupGroups,
|
||||||
return;
|
}))
|
||||||
}
|
.runReadWrite(async (tx) => {
|
||||||
if (!r.retryInfo) {
|
const r = await tx.recoupGroups.get(recoupGroupId);
|
||||||
return;
|
if (!r) {
|
||||||
}
|
return;
|
||||||
r.retryInfo.retryCounter++;
|
}
|
||||||
updateRetryInfoTimeout(r.retryInfo);
|
if (!r.retryInfo) {
|
||||||
r.lastError = err;
|
return;
|
||||||
await tx.put(Stores.recoupGroups, r);
|
}
|
||||||
});
|
r.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(r.retryInfo);
|
||||||
|
r.lastError = err;
|
||||||
|
await tx.recoupGroups.put(r);
|
||||||
|
});
|
||||||
if (err) {
|
if (err) {
|
||||||
ws.notify({ type: NotificationType.RecoupOperationError, error: err });
|
ws.notify({ type: NotificationType.RecoupOperationError, error: err });
|
||||||
}
|
}
|
||||||
@ -82,7 +85,12 @@ async function incrementRecoupRetry(
|
|||||||
|
|
||||||
async function putGroupAsFinished(
|
async function putGroupAsFinished(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tx: TransactionHandle<typeof Stores.recoupGroups>,
|
tx: GetReadWriteAccess<{
|
||||||
|
recoupGroups: typeof WalletStoresV1.recoupGroups;
|
||||||
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
|
coins: typeof WalletStoresV1.coins;
|
||||||
|
}>,
|
||||||
recoupGroup: RecoupGroupRecord,
|
recoupGroup: RecoupGroupRecord,
|
||||||
coinIdx: number,
|
coinIdx: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -116,7 +124,7 @@ async function putGroupAsFinished(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await tx.put(Stores.recoupGroups, recoupGroup);
|
await tx.recoupGroups.put(recoupGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recoupTipCoin(
|
async function recoupTipCoin(
|
||||||
@ -128,16 +136,23 @@ async function recoupTipCoin(
|
|||||||
// We can't really recoup a coin we got via tipping.
|
// We can't really recoup a coin we got via tipping.
|
||||||
// Thus we just put the coin to sleep.
|
// Thus we just put the coin to sleep.
|
||||||
// FIXME: somehow report this to the user
|
// FIXME: somehow report this to the user
|
||||||
await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => {
|
await ws.db
|
||||||
const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
|
.mktx((x) => ({
|
||||||
if (!recoupGroup) {
|
recoupGroups: x.recoupGroups,
|
||||||
return;
|
denominations: WalletStoresV1.denominations,
|
||||||
}
|
refreshGroups: WalletStoresV1.refreshGroups,
|
||||||
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
coins: WalletStoresV1.coins,
|
||||||
return;
|
}))
|
||||||
}
|
.runReadWrite(async (tx) => {
|
||||||
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
|
||||||
});
|
if (!recoupGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recoupWithdrawCoin(
|
async function recoupWithdrawCoin(
|
||||||
@ -148,7 +163,13 @@ async function recoupWithdrawCoin(
|
|||||||
cs: WithdrawCoinSource,
|
cs: WithdrawCoinSource,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reservePub = cs.reservePub;
|
const reservePub = cs.reservePub;
|
||||||
const reserve = await ws.db.get(Stores.reserves, reservePub);
|
const reserve = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.reserves.get(reservePub);
|
||||||
|
});
|
||||||
if (!reserve) {
|
if (!reserve) {
|
||||||
// FIXME: We should at least emit some pending operation / warning for this?
|
// FIXME: We should at least emit some pending operation / warning for this?
|
||||||
return;
|
return;
|
||||||
@ -172,35 +193,29 @@ async function recoupWithdrawCoin(
|
|||||||
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exchangeDetails = await ws.db.runWithReadTransaction(
|
|
||||||
[Stores.exchanges, Stores.exchangeDetails],
|
|
||||||
async (tx) => {
|
|
||||||
return getExchangeDetails(tx, reserve.exchangeBaseUrl);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!exchangeDetails) {
|
|
||||||
// FIXME: report inconsistency?
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: verify that our expectations about the amount match
|
// FIXME: verify that our expectations about the amount match
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.coins, Stores.denominations, Stores.reserves, Stores.recoupGroups],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
coins: x.coins,
|
||||||
const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
|
denominations: x.denominations,
|
||||||
|
reserves: x.reserves,
|
||||||
|
recoupGroups: x.recoupGroups,
|
||||||
|
refreshGroups: x.refreshGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
|
||||||
if (!recoupGroup) {
|
if (!recoupGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const updatedCoin = await tx.get(Stores.coins, coin.coinPub);
|
const updatedCoin = await tx.coins.get(coin.coinPub);
|
||||||
if (!updatedCoin) {
|
if (!updatedCoin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const updatedReserve = await tx.get(Stores.reserves, reserve.reservePub);
|
const updatedReserve = await tx.reserves.get(reserve.reservePub);
|
||||||
if (!updatedReserve) {
|
if (!updatedReserve) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -214,11 +229,10 @@ async function recoupWithdrawCoin(
|
|||||||
updatedReserve.requestedQuery = true;
|
updatedReserve.requestedQuery = true;
|
||||||
updatedReserve.retryInfo = initRetryInfo();
|
updatedReserve.retryInfo = initRetryInfo();
|
||||||
}
|
}
|
||||||
await tx.put(Stores.coins, updatedCoin);
|
await tx.coins.put(updatedCoin);
|
||||||
await tx.put(Stores.reserves, updatedReserve);
|
await tx.reserves.put(updatedReserve);
|
||||||
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.RecoupFinished,
|
type: NotificationType.RecoupFinished,
|
||||||
@ -250,38 +264,24 @@ async function recoupRefreshCoin(
|
|||||||
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exchangeDetails = await ws.db.runWithReadTransaction(
|
await ws.db
|
||||||
[Stores.exchanges, Stores.exchangeDetails],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
coins: x.coins,
|
||||||
// FIXME: Get the exchange details based on the
|
denominations: x.denominations,
|
||||||
// exchange master public key instead of via just the URL.
|
reserves: x.reserves,
|
||||||
return getExchangeDetails(tx, coin.exchangeBaseUrl);
|
recoupGroups: x.recoupGroups,
|
||||||
},
|
refreshGroups: x.refreshGroups,
|
||||||
);
|
}))
|
||||||
if (!exchangeDetails) {
|
.runReadWrite(async (tx) => {
|
||||||
// FIXME: report inconsistency?
|
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
|
||||||
logger.warn("exchange details for recoup not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
|
||||||
[
|
|
||||||
Stores.coins,
|
|
||||||
Stores.denominations,
|
|
||||||
Stores.reserves,
|
|
||||||
Stores.recoupGroups,
|
|
||||||
Stores.refreshGroups,
|
|
||||||
],
|
|
||||||
async (tx) => {
|
|
||||||
const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
|
|
||||||
if (!recoupGroup) {
|
if (!recoupGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const oldCoin = await tx.get(Stores.coins, cs.oldCoinPub);
|
const oldCoin = await tx.coins.get(cs.oldCoinPub);
|
||||||
const revokedCoin = await tx.get(Stores.coins, coin.coinPub);
|
const revokedCoin = await tx.coins.get(coin.coinPub);
|
||||||
if (!revokedCoin) {
|
if (!revokedCoin) {
|
||||||
logger.warn("revoked coin for recoup not found");
|
logger.warn("revoked coin for recoup not found");
|
||||||
return;
|
return;
|
||||||
@ -300,23 +300,27 @@ async function recoupRefreshCoin(
|
|||||||
Amounts.stringify(oldCoin.currentAmount),
|
Amounts.stringify(oldCoin.currentAmount),
|
||||||
);
|
);
|
||||||
recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub);
|
recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub);
|
||||||
await tx.put(Stores.coins, revokedCoin);
|
await tx.coins.put(revokedCoin);
|
||||||
await tx.put(Stores.coins, oldCoin);
|
await tx.coins.put(oldCoin);
|
||||||
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetRecoupGroupRetry(
|
async function resetRecoupGroupRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
recoupGroupId: string,
|
recoupGroupId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.mutate(Stores.recoupGroups, recoupGroupId, (x) => {
|
await ws.db
|
||||||
if (x.retryInfo.active) {
|
.mktx((x) => ({
|
||||||
x.retryInfo = initRetryInfo();
|
recoupGroups: x.recoupGroups,
|
||||||
}
|
}))
|
||||||
return x;
|
.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(
|
export async function processRecoupGroup(
|
||||||
@ -342,7 +346,13 @@ async function processRecoupGroupImpl(
|
|||||||
if (forceNow) {
|
if (forceNow) {
|
||||||
await resetRecoupGroupRetry(ws, recoupGroupId);
|
await resetRecoupGroupRetry(ws, recoupGroupId);
|
||||||
}
|
}
|
||||||
const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId);
|
const recoupGroup = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
recoupGroups: x.recoupGroups,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.recoupGroups.get(recoupGroupId);
|
||||||
|
});
|
||||||
if (!recoupGroup) {
|
if (!recoupGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -358,9 +368,15 @@ async function processRecoupGroupImpl(
|
|||||||
const reserveSet = new Set<string>();
|
const reserveSet = new Set<string>();
|
||||||
for (let i = 0; i < recoupGroup.coinPubs.length; i++) {
|
for (let i = 0; i < recoupGroup.coinPubs.length; i++) {
|
||||||
const coinPub = recoupGroup.coinPubs[i];
|
const coinPub = recoupGroup.coinPubs[i];
|
||||||
const coin = await ws.db.get(Stores.coins, coinPub);
|
const coin = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
coins: x.coins,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.coins.get(coinPub);
|
||||||
|
});
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
throw Error(`Coin ${coinPub} not found, can't request payback`);
|
throw Error(`Coin ${coinPub} not found, can't request recoup`);
|
||||||
}
|
}
|
||||||
if (coin.coinSource.type === CoinSourceType.Withdraw) {
|
if (coin.coinSource.type === CoinSourceType.Withdraw) {
|
||||||
reserveSet.add(coin.coinSource.reservePub);
|
reserveSet.add(coin.coinSource.reservePub);
|
||||||
@ -376,7 +392,12 @@ async function processRecoupGroupImpl(
|
|||||||
|
|
||||||
export async function createRecoupGroup(
|
export async function createRecoupGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tx: TransactionHandle<typeof Stores.recoupGroups | typeof Stores.coins>,
|
tx: GetReadWriteAccess<{
|
||||||
|
recoupGroups: typeof WalletStoresV1.recoupGroups;
|
||||||
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
|
coins: typeof WalletStoresV1.coins;
|
||||||
|
}>,
|
||||||
coinPubs: string[],
|
coinPubs: string[],
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const recoupGroupId = encodeCrock(getRandomBytes(32));
|
const recoupGroupId = encodeCrock(getRandomBytes(32));
|
||||||
@ -396,7 +417,7 @@ export async function createRecoupGroup(
|
|||||||
|
|
||||||
for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) {
|
for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) {
|
||||||
const coinPub = coinPubs[coinIdx];
|
const coinPub = coinPubs[coinIdx];
|
||||||
const coin = await tx.get(Stores.coins, coinPub);
|
const coin = await tx.coins.get(coinPub);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
||||||
continue;
|
continue;
|
||||||
@ -407,10 +428,10 @@ export async function createRecoupGroup(
|
|||||||
}
|
}
|
||||||
recoupGroup.oldAmountPerCoin[coinIdx] = coin.currentAmount;
|
recoupGroup.oldAmountPerCoin[coinIdx] = coin.currentAmount;
|
||||||
coin.currentAmount = Amounts.getZero(coin.currentAmount.currency);
|
coin.currentAmount = Amounts.getZero(coin.currentAmount.currency);
|
||||||
await tx.put(Stores.coins, coin);
|
await tx.coins.put(coin);
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.put(Stores.recoupGroups, recoupGroup);
|
await tx.recoupGroups.put(recoupGroup);
|
||||||
|
|
||||||
return recoupGroupId;
|
return recoupGroupId;
|
||||||
}
|
}
|
||||||
@ -420,22 +441,34 @@ async function processRecoup(
|
|||||||
recoupGroupId: string,
|
recoupGroupId: string,
|
||||||
coinIdx: number,
|
coinIdx: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId);
|
const coin = await ws.db
|
||||||
if (!recoupGroup) {
|
.mktx((x) => ({
|
||||||
return;
|
recoupGroups: x.recoupGroups,
|
||||||
}
|
coins: x.coins,
|
||||||
if (recoupGroup.timestampFinished) {
|
}))
|
||||||
return;
|
.runReadOnly(async (tx) => {
|
||||||
}
|
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
|
||||||
if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
|
if (!recoupGroup) {
|
||||||
return;
|
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) {
|
if (!coin) {
|
||||||
throw Error(`Coin ${coinPub} not found, can't request payback`);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cs = coin.coinSource;
|
const cs = coin.coinSource;
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
RefreshGroupRecord,
|
RefreshGroupRecord,
|
||||||
RefreshPlanchet,
|
RefreshPlanchet,
|
||||||
Stores,
|
WalletStoresV1,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import {
|
import {
|
||||||
codecForExchangeMeltResponse,
|
codecForExchangeMeltResponse,
|
||||||
@ -38,7 +38,6 @@ import { amountToPretty } from "@gnu-taler/taler-util";
|
|||||||
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
import { checkDbInvariant } from "../util/invariants";
|
import { checkDbInvariant } from "../util/invariants";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import { TransactionHandle } from "../util/query";
|
|
||||||
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
|
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
|
||||||
import {
|
import {
|
||||||
Duration,
|
Duration,
|
||||||
@ -57,6 +56,8 @@ import { updateExchangeFromUrl } from "./exchanges";
|
|||||||
import { EXCHANGE_COINS_LOCK, InternalWalletState } from "./state";
|
import { EXCHANGE_COINS_LOCK, InternalWalletState } from "./state";
|
||||||
import { isWithdrawableDenom, selectWithdrawalDenominations } from "./withdraw";
|
import { isWithdrawableDenom, selectWithdrawalDenominations } from "./withdraw";
|
||||||
import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js";
|
import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js";
|
||||||
|
import { GetReadWriteAccess } from "../util/query.js";
|
||||||
|
import { Wallet } from "../wallet.js";
|
||||||
|
|
||||||
const logger = new Logger("refresh.ts");
|
const logger = new Logger("refresh.ts");
|
||||||
|
|
||||||
@ -95,7 +96,7 @@ export function getTotalRefreshCost(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a refresh session inside a refresh group.
|
* Create a refresh session for one particular coin inside a refresh group.
|
||||||
*/
|
*/
|
||||||
async function refreshCreateSession(
|
async function refreshCreateSession(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -105,45 +106,68 @@ async function refreshCreateSession(
|
|||||||
logger.trace(
|
logger.trace(
|
||||||
`creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`,
|
`creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`,
|
||||||
);
|
);
|
||||||
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
if (refreshGroup.finishedPerCoin[coinIndex]) {
|
|
||||||
return;
|
const { refreshGroup, coin } = d;
|
||||||
}
|
|
||||||
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 { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
|
const { exchange } = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
|
||||||
if (!exchange) {
|
if (!exchange) {
|
||||||
throw Error("db inconsistent: exchange of coin not found");
|
throw Error("db inconsistent: exchange of coin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldDenom = await ws.db.get(Stores.denominations, [
|
const { availableAmount, availableDenoms } = await ws.db
|
||||||
exchange.baseUrl,
|
.mktx((x) => ({
|
||||||
coin.denomPubHash,
|
denominations: x.denominations,
|
||||||
]);
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
const oldDenom = await tx.denominations.get([
|
||||||
|
exchange.baseUrl,
|
||||||
|
coin.denomPubHash,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!oldDenom) {
|
if (!oldDenom) {
|
||||||
throw Error("db inconsistent: denomination for coin not found");
|
throw Error("db inconsistent: denomination for coin not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableDenoms: DenominationRecord[] = await ws.db
|
const availableDenoms: DenominationRecord[] = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
|
.iter(exchange.baseUrl)
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
const availableAmount = Amounts.sub(
|
const availableAmount = Amounts.sub(
|
||||||
refreshGroup.inputPerCoin[coinIndex],
|
refreshGroup.inputPerCoin[coinIndex],
|
||||||
oldDenom.feeRefresh,
|
oldDenom.feeRefresh,
|
||||||
).amount;
|
).amount;
|
||||||
|
return { availableAmount, availableDenoms };
|
||||||
|
});
|
||||||
|
|
||||||
const newCoinDenoms = selectWithdrawalDenominations(
|
const newCoinDenoms = selectWithdrawalDenominations(
|
||||||
availableAmount,
|
availableAmount,
|
||||||
@ -156,10 +180,13 @@ async function refreshCreateSession(
|
|||||||
availableAmount,
|
availableAmount,
|
||||||
)} too small`,
|
)} too small`,
|
||||||
);
|
);
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.coins, Stores.refreshGroups],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
coins: x.coins,
|
||||||
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
|
refreshGroups: x.refreshGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const rg = await tx.refreshGroups.get(refreshGroupId);
|
||||||
if (!rg) {
|
if (!rg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -175,9 +202,8 @@ async function refreshCreateSession(
|
|||||||
rg.timestampFinished = getTimestampNow();
|
rg.timestampFinished = getTimestampNow();
|
||||||
rg.retryInfo = initRetryInfo(false);
|
rg.retryInfo = initRetryInfo(false);
|
||||||
}
|
}
|
||||||
await tx.put(Stores.refreshGroups, rg);
|
await tx.refreshGroups.put(rg);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
ws.notify({ type: NotificationType.RefreshUnwarranted });
|
ws.notify({ type: NotificationType.RefreshUnwarranted });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -185,10 +211,13 @@ async function refreshCreateSession(
|
|||||||
const sessionSecretSeed = encodeCrock(getRandomBytes(64));
|
const sessionSecretSeed = encodeCrock(getRandomBytes(64));
|
||||||
|
|
||||||
// Store refresh session for this coin in the database.
|
// Store refresh session for this coin in the database.
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.refreshGroups, Stores.coins],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
refreshGroups: x.refreshGroups,
|
||||||
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
|
coins: x.coins,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const rg = await tx.refreshGroups.get(refreshGroupId);
|
||||||
if (!rg) {
|
if (!rg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -204,9 +233,8 @@ async function refreshCreateSession(
|
|||||||
})),
|
})),
|
||||||
amountRefreshOutput: newCoinDenoms.totalCoinValue,
|
amountRefreshOutput: newCoinDenoms.totalCoinValue,
|
||||||
};
|
};
|
||||||
await tx.put(Stores.refreshGroups, rg);
|
await tx.refreshGroups.put(rg);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`created refresh session for coin #${coinIndex} in ${refreshGroupId}`,
|
`created refresh session for coin #${coinIndex} in ${refreshGroupId}`,
|
||||||
);
|
);
|
||||||
@ -222,48 +250,63 @@ async function refreshMelt(
|
|||||||
refreshGroupId: string,
|
refreshGroupId: string,
|
||||||
coinIndex: number,
|
coinIndex: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
const d = await ws.db
|
||||||
if (!refreshGroup) {
|
.mktx((x) => ({
|
||||||
return;
|
refreshGroups: x.refreshGroups,
|
||||||
}
|
coins: x.coins,
|
||||||
const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
|
denominations: x.denominations,
|
||||||
if (!refreshSession) {
|
}))
|
||||||
return;
|
.runReadWrite(async (tx) => {
|
||||||
}
|
const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
|
||||||
if (refreshSession.norevealIndex !== undefined) {
|
if (!refreshGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
|
||||||
|
if (!refreshSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (refreshSession.norevealIndex !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const oldCoin = await ws.db.get(
|
const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
|
||||||
Stores.coins,
|
checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
|
||||||
refreshGroup.oldCoinPubs[coinIndex],
|
const oldDenom = await tx.denominations.get([
|
||||||
);
|
oldCoin.exchangeBaseUrl,
|
||||||
checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
|
oldCoin.denomPubHash,
|
||||||
const oldDenom = await ws.db.get(Stores.denominations, [
|
]);
|
||||||
oldCoin.exchangeBaseUrl,
|
checkDbInvariant(
|
||||||
oldCoin.denomPubHash,
|
!!oldDenom,
|
||||||
]);
|
"denomination for melted coin doesn't exist",
|
||||||
checkDbInvariant(!!oldDenom, "denomination for melted coin doesn't exist");
|
);
|
||||||
|
|
||||||
const newCoinDenoms: RefreshNewDenomInfo[] = [];
|
const newCoinDenoms: RefreshNewDenomInfo[] = [];
|
||||||
|
|
||||||
for (const dh of refreshSession.newDenoms) {
|
for (const dh of refreshSession.newDenoms) {
|
||||||
const newDenom = await ws.db.get(Stores.denominations, [
|
const newDenom = await tx.denominations.get([
|
||||||
oldCoin.exchangeBaseUrl,
|
oldCoin.exchangeBaseUrl,
|
||||||
dh.denomPubHash,
|
dh.denomPubHash,
|
||||||
]);
|
]);
|
||||||
checkDbInvariant(
|
checkDbInvariant(
|
||||||
!!newDenom,
|
!!newDenom,
|
||||||
"new denomination for refresh not in database",
|
"new denomination for refresh not in database",
|
||||||
);
|
);
|
||||||
newCoinDenoms.push({
|
newCoinDenoms.push({
|
||||||
count: dh.count,
|
count: dh.count,
|
||||||
denomPub: newDenom.denomPub,
|
denomPub: newDenom.denomPub,
|
||||||
feeWithdraw: newDenom.feeWithdraw,
|
feeWithdraw: newDenom.feeWithdraw,
|
||||||
value: newDenom.value,
|
value: newDenom.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!d) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession } = d;
|
||||||
|
|
||||||
const derived = await ws.cryptoApi.deriveRefreshSession({
|
const derived = await ws.cryptoApi.deriveRefreshSession({
|
||||||
kappa: 3,
|
kappa: 3,
|
||||||
meltCoinDenomPubHash: oldCoin.denomPubHash,
|
meltCoinDenomPubHash: oldCoin.denomPubHash,
|
||||||
@ -303,20 +346,28 @@ async function refreshMelt(
|
|||||||
|
|
||||||
refreshSession.norevealIndex = norevealIndex;
|
refreshSession.norevealIndex = norevealIndex;
|
||||||
|
|
||||||
await ws.db.mutate(Stores.refreshGroups, refreshGroupId, (rg) => {
|
await ws.db
|
||||||
const rs = rg.refreshSessionPerCoin[coinIndex];
|
.mktx((x) => ({
|
||||||
if (rg.timestampFinished) {
|
refreshGroups: x.refreshGroups,
|
||||||
return;
|
}))
|
||||||
}
|
.runReadWrite(async (tx) => {
|
||||||
if (!rs) {
|
const rg = await tx.refreshGroups.get(refreshGroupId);
|
||||||
return;
|
if (!rg) {
|
||||||
}
|
return;
|
||||||
if (rs.norevealIndex !== undefined) {
|
}
|
||||||
return;
|
if (rg.timestampFinished) {
|
||||||
}
|
return;
|
||||||
rs.norevealIndex = norevealIndex;
|
}
|
||||||
return rg;
|
const rs = rg.refreshSessionPerCoin[coinIndex];
|
||||||
});
|
if (!rs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rs.norevealIndex !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rs.norevealIndex = norevealIndex;
|
||||||
|
await tx.refreshGroups.put(rg);
|
||||||
|
});
|
||||||
|
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.RefreshMelted,
|
type: NotificationType.RefreshMelted,
|
||||||
@ -328,49 +379,78 @@ async function refreshReveal(
|
|||||||
refreshGroupId: string,
|
refreshGroupId: string,
|
||||||
coinIndex: number,
|
coinIndex: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
const d = await ws.db
|
||||||
if (!refreshGroup) {
|
.mktx((x) => ({
|
||||||
return;
|
refreshGroups: x.refreshGroups,
|
||||||
}
|
coins: x.coins,
|
||||||
const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
|
denominations: x.denominations,
|
||||||
if (!refreshSession) {
|
}))
|
||||||
return;
|
.runReadOnly(async (tx) => {
|
||||||
}
|
const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
|
||||||
const norevealIndex = refreshSession.norevealIndex;
|
if (!refreshGroup) {
|
||||||
if (norevealIndex === undefined) {
|
return;
|
||||||
throw Error("can't reveal without melting first");
|
}
|
||||||
}
|
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(
|
const oldCoin = await tx.coins.get(refreshGroup.oldCoinPubs[coinIndex]);
|
||||||
Stores.coins,
|
checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
|
||||||
refreshGroup.oldCoinPubs[coinIndex],
|
const oldDenom = await tx.denominations.get([
|
||||||
);
|
oldCoin.exchangeBaseUrl,
|
||||||
checkDbInvariant(!!oldCoin, "melt coin doesn't exist");
|
oldCoin.denomPubHash,
|
||||||
const oldDenom = await ws.db.get(Stores.denominations, [
|
]);
|
||||||
oldCoin.exchangeBaseUrl,
|
checkDbInvariant(
|
||||||
oldCoin.denomPubHash,
|
!!oldDenom,
|
||||||
]);
|
"denomination for melted coin doesn't exist",
|
||||||
checkDbInvariant(!!oldDenom, "denomination for melted coin doesn't exist");
|
);
|
||||||
|
|
||||||
const newCoinDenoms: RefreshNewDenomInfo[] = [];
|
const newCoinDenoms: RefreshNewDenomInfo[] = [];
|
||||||
|
|
||||||
for (const dh of refreshSession.newDenoms) {
|
for (const dh of refreshSession.newDenoms) {
|
||||||
const newDenom = await ws.db.get(Stores.denominations, [
|
const newDenom = await tx.denominations.get([
|
||||||
oldCoin.exchangeBaseUrl,
|
oldCoin.exchangeBaseUrl,
|
||||||
dh.denomPubHash,
|
dh.denomPubHash,
|
||||||
]);
|
]);
|
||||||
checkDbInvariant(
|
checkDbInvariant(
|
||||||
!!newDenom,
|
!!newDenom,
|
||||||
"new denomination for refresh not in database",
|
"new denomination for refresh not in database",
|
||||||
);
|
);
|
||||||
newCoinDenoms.push({
|
newCoinDenoms.push({
|
||||||
count: dh.count,
|
count: dh.count,
|
||||||
denomPub: newDenom.denomPub,
|
denomPub: newDenom.denomPub,
|
||||||
feeWithdraw: newDenom.feeWithdraw,
|
feeWithdraw: newDenom.feeWithdraw,
|
||||||
value: newDenom.value,
|
value: newDenom.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
oldCoin,
|
||||||
|
oldDenom,
|
||||||
|
newCoinDenoms,
|
||||||
|
refreshSession,
|
||||||
|
refreshGroup,
|
||||||
|
norevealIndex,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!d) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
oldCoin,
|
||||||
|
oldDenom,
|
||||||
|
newCoinDenoms,
|
||||||
|
refreshSession,
|
||||||
|
refreshGroup,
|
||||||
|
norevealIndex,
|
||||||
|
} = d;
|
||||||
|
|
||||||
const derived = await ws.cryptoApi.deriveRefreshSession({
|
const derived = await ws.cryptoApi.deriveRefreshSession({
|
||||||
kappa: 3,
|
kappa: 3,
|
||||||
meltCoinDenomPubHash: oldCoin.denomPubHash,
|
meltCoinDenomPubHash: oldCoin.denomPubHash,
|
||||||
@ -389,14 +469,6 @@ async function refreshReveal(
|
|||||||
throw Error("refresh index error");
|
throw Error("refresh index error");
|
||||||
}
|
}
|
||||||
|
|
||||||
const meltCoinRecord = await ws.db.get(
|
|
||||||
Stores.coins,
|
|
||||||
refreshGroup.oldCoinPubs[coinIndex],
|
|
||||||
);
|
|
||||||
if (!meltCoinRecord) {
|
|
||||||
throw Error("inconsistent database");
|
|
||||||
}
|
|
||||||
|
|
||||||
const evs = planchets.map((x: RefreshPlanchet) => x.coinEv);
|
const evs = planchets.map((x: RefreshPlanchet) => x.coinEv);
|
||||||
const newDenomsFlat: string[] = [];
|
const newDenomsFlat: string[] = [];
|
||||||
const linkSigs: string[] = [];
|
const linkSigs: string[] = [];
|
||||||
@ -406,9 +478,9 @@ async function refreshReveal(
|
|||||||
for (let j = 0; j < dsel.count; j++) {
|
for (let j = 0; j < dsel.count; j++) {
|
||||||
const newCoinIndex = linkSigs.length;
|
const newCoinIndex = linkSigs.length;
|
||||||
const linkSig = await ws.cryptoApi.signCoinLink(
|
const linkSig = await ws.cryptoApi.signCoinLink(
|
||||||
meltCoinRecord.coinPriv,
|
oldCoin.coinPriv,
|
||||||
dsel.denomPubHash,
|
dsel.denomPubHash,
|
||||||
meltCoinRecord.coinPub,
|
oldCoin.coinPub,
|
||||||
derived.transferPubs[norevealIndex],
|
derived.transferPubs[norevealIndex],
|
||||||
planchets[newCoinIndex].coinEv,
|
planchets[newCoinIndex].coinEv,
|
||||||
);
|
);
|
||||||
@ -447,10 +519,17 @@ async function refreshReveal(
|
|||||||
for (let i = 0; i < refreshSession.newDenoms.length; i++) {
|
for (let i = 0; i < refreshSession.newDenoms.length; i++) {
|
||||||
for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
|
for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
|
||||||
const newCoinIndex = coins.length;
|
const newCoinIndex = coins.length;
|
||||||
const denom = await ws.db.get(Stores.denominations, [
|
// FIXME: Look up in earlier transaction!
|
||||||
oldCoin.exchangeBaseUrl,
|
const denom = await ws.db
|
||||||
refreshSession.newDenoms[i].denomPubHash,
|
.mktx((x) => ({
|
||||||
]);
|
denominations: x.denominations,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.denominations.get([
|
||||||
|
oldCoin.exchangeBaseUrl,
|
||||||
|
refreshSession.newDenoms[i].denomPubHash,
|
||||||
|
]);
|
||||||
|
});
|
||||||
if (!denom) {
|
if (!denom) {
|
||||||
console.error("denom not found");
|
console.error("denom not found");
|
||||||
continue;
|
continue;
|
||||||
@ -483,10 +562,13 @@ async function refreshReveal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.coins, Stores.refreshGroups],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
coins: x.coins,
|
||||||
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
|
refreshGroups: x.refreshGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const rg = await tx.refreshGroups.get(refreshGroupId);
|
||||||
if (!rg) {
|
if (!rg) {
|
||||||
logger.warn("no refresh session found");
|
logger.warn("no refresh session found");
|
||||||
return;
|
return;
|
||||||
@ -508,11 +590,10 @@ async function refreshReveal(
|
|||||||
rg.retryInfo = initRetryInfo(false);
|
rg.retryInfo = initRetryInfo(false);
|
||||||
}
|
}
|
||||||
for (const coin of coins) {
|
for (const coin of coins) {
|
||||||
await tx.put(Stores.coins, coin);
|
await tx.coins.put(coin);
|
||||||
}
|
}
|
||||||
await tx.put(Stores.refreshGroups, rg);
|
await tx.refreshGroups.put(rg);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
logger.trace("refresh finished (end of reveal)");
|
logger.trace("refresh finished (end of reveal)");
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.RefreshRevealed,
|
type: NotificationType.RefreshRevealed,
|
||||||
@ -524,19 +605,23 @@ async function incrementRefreshRetry(
|
|||||||
refreshGroupId: string,
|
refreshGroupId: string,
|
||||||
err: TalerErrorDetails | undefined,
|
err: TalerErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
|
await ws.db
|
||||||
const r = await tx.get(Stores.refreshGroups, refreshGroupId);
|
.mktx((x) => ({
|
||||||
if (!r) {
|
refreshGroups: x.refreshGroups,
|
||||||
return;
|
}))
|
||||||
}
|
.runReadWrite(async (tx) => {
|
||||||
if (!r.retryInfo) {
|
const r = await tx.refreshGroups.get(refreshGroupId);
|
||||||
return;
|
if (!r) {
|
||||||
}
|
return;
|
||||||
r.retryInfo.retryCounter++;
|
}
|
||||||
updateRetryInfoTimeout(r.retryInfo);
|
if (!r.retryInfo) {
|
||||||
r.lastError = err;
|
return;
|
||||||
await tx.put(Stores.refreshGroups, r);
|
}
|
||||||
});
|
r.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(r.retryInfo);
|
||||||
|
r.lastError = err;
|
||||||
|
await tx.refreshGroups.put(r);
|
||||||
|
});
|
||||||
if (err) {
|
if (err) {
|
||||||
ws.notify({ type: NotificationType.RefreshOperationError, error: err });
|
ws.notify({ type: NotificationType.RefreshOperationError, error: err });
|
||||||
}
|
}
|
||||||
@ -562,14 +647,19 @@ export async function processRefreshGroup(
|
|||||||
|
|
||||||
async function resetRefreshGroupRetry(
|
async function resetRefreshGroupRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
refreshSessionId: string,
|
refreshGroupId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.mutate(Stores.refreshGroups, refreshSessionId, (x) => {
|
await ws.db
|
||||||
if (x.retryInfo.active) {
|
.mktx((x) => ({
|
||||||
x.retryInfo = initRetryInfo();
|
refreshGroups: x.refreshGroups,
|
||||||
}
|
}))
|
||||||
return x;
|
.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(
|
async function processRefreshGroupImpl(
|
||||||
@ -580,13 +670,20 @@ async function processRefreshGroupImpl(
|
|||||||
if (forceNow) {
|
if (forceNow) {
|
||||||
await resetRefreshGroupRetry(ws, refreshGroupId);
|
await resetRefreshGroupRetry(ws, refreshGroupId);
|
||||||
}
|
}
|
||||||
const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
const refreshGroup = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
refreshGroups: x.refreshGroups,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.refreshGroups.get(refreshGroupId);
|
||||||
|
});
|
||||||
if (!refreshGroup) {
|
if (!refreshGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (refreshGroup.timestampFinished) {
|
if (refreshGroup.timestampFinished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Process refresh sessions of the group in parallel.
|
||||||
const ps = refreshGroup.oldCoinPubs.map((x, i) =>
|
const ps = refreshGroup.oldCoinPubs.map((x, i) =>
|
||||||
processRefreshSession(ws, refreshGroupId, i),
|
processRefreshSession(ws, refreshGroupId, i),
|
||||||
);
|
);
|
||||||
@ -602,7 +699,11 @@ async function processRefreshSession(
|
|||||||
logger.trace(
|
logger.trace(
|
||||||
`processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`,
|
`processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`,
|
||||||
);
|
);
|
||||||
let refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
let refreshGroup = await ws.db
|
||||||
|
.mktx((x) => ({ refreshGroups: x.refreshGroups }))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.refreshGroups.get(refreshGroupId);
|
||||||
|
});
|
||||||
if (!refreshGroup) {
|
if (!refreshGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -611,7 +712,11 @@ async function processRefreshSession(
|
|||||||
}
|
}
|
||||||
if (!refreshGroup.refreshSessionPerCoin[coinIndex]) {
|
if (!refreshGroup.refreshSessionPerCoin[coinIndex]) {
|
||||||
await refreshCreateSession(ws, refreshGroupId, coinIndex);
|
await refreshCreateSession(ws, refreshGroupId, coinIndex);
|
||||||
refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
|
refreshGroup = await ws.db
|
||||||
|
.mktx((x) => ({ refreshGroups: x.refreshGroups }))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.refreshGroups.get(refreshGroupId);
|
||||||
|
});
|
||||||
if (!refreshGroup) {
|
if (!refreshGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -646,11 +751,11 @@ async function processRefreshSession(
|
|||||||
*/
|
*/
|
||||||
export async function createRefreshGroup(
|
export async function createRefreshGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tx: TransactionHandle<
|
tx: GetReadWriteAccess<{
|
||||||
| typeof Stores.denominations
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
| typeof Stores.coins
|
coins: typeof WalletStoresV1.coins;
|
||||||
| typeof Stores.refreshGroups
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
>,
|
}>,
|
||||||
oldCoinPubs: CoinPublicKey[],
|
oldCoinPubs: CoinPublicKey[],
|
||||||
reason: RefreshReason,
|
reason: RefreshReason,
|
||||||
): Promise<RefreshGroupId> {
|
): Promise<RefreshGroupId> {
|
||||||
@ -667,8 +772,8 @@ export async function createRefreshGroup(
|
|||||||
if (denomsPerExchange[exchangeBaseUrl]) {
|
if (denomsPerExchange[exchangeBaseUrl]) {
|
||||||
return denomsPerExchange[exchangeBaseUrl];
|
return denomsPerExchange[exchangeBaseUrl];
|
||||||
}
|
}
|
||||||
const allDenoms = await tx
|
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.iterIndexed(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
|
.iter(exchangeBaseUrl)
|
||||||
.filter((x) => {
|
.filter((x) => {
|
||||||
return isWithdrawableDenom(x);
|
return isWithdrawableDenom(x);
|
||||||
});
|
});
|
||||||
@ -677,9 +782,9 @@ export async function createRefreshGroup(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const ocp of oldCoinPubs) {
|
for (const ocp of oldCoinPubs) {
|
||||||
const coin = await tx.get(Stores.coins, ocp.coinPub);
|
const coin = await tx.coins.get(ocp.coinPub);
|
||||||
checkDbInvariant(!!coin, "coin must be in database");
|
checkDbInvariant(!!coin, "coin must be in database");
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.denominations.get([
|
||||||
coin.exchangeBaseUrl,
|
coin.exchangeBaseUrl,
|
||||||
coin.denomPubHash,
|
coin.denomPubHash,
|
||||||
]);
|
]);
|
||||||
@ -691,7 +796,7 @@ export async function createRefreshGroup(
|
|||||||
inputPerCoin.push(refreshAmount);
|
inputPerCoin.push(refreshAmount);
|
||||||
coin.currentAmount = Amounts.getZero(refreshAmount.currency);
|
coin.currentAmount = Amounts.getZero(refreshAmount.currency);
|
||||||
coin.status = CoinStatus.Dormant;
|
coin.status = CoinStatus.Dormant;
|
||||||
await tx.put(Stores.coins, coin);
|
await tx.coins.put(coin);
|
||||||
const denoms = await getDenoms(coin.exchangeBaseUrl);
|
const denoms = await getDenoms(coin.exchangeBaseUrl);
|
||||||
const cost = getTotalRefreshCost(denoms, denom, refreshAmount);
|
const cost = getTotalRefreshCost(denoms, denom, refreshAmount);
|
||||||
const output = Amounts.sub(refreshAmount, cost).amount;
|
const output = Amounts.sub(refreshAmount, cost).amount;
|
||||||
@ -718,7 +823,7 @@ export async function createRefreshGroup(
|
|||||||
refreshGroup.timestampFinished = getTimestampNow();
|
refreshGroup.timestampFinished = getTimestampNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.put(Stores.refreshGroups, refreshGroup);
|
await tx.refreshGroups.put(refreshGroup);
|
||||||
|
|
||||||
logger.trace(`created refresh group ${refreshGroupId}`);
|
logger.trace(`created refresh group ${refreshGroupId}`);
|
||||||
|
|
||||||
@ -760,20 +865,20 @@ export async function autoRefresh(
|
|||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await updateExchangeFromUrl(ws, exchangeBaseUrl, true);
|
await updateExchangeFromUrl(ws, exchangeBaseUrl, true);
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[
|
.mktx((x) => ({
|
||||||
Stores.coins,
|
coins: x.coins,
|
||||||
Stores.denominations,
|
denominations: x.denominations,
|
||||||
Stores.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
Stores.exchanges,
|
exchanges: x.exchanges,
|
||||||
],
|
}))
|
||||||
async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const exchange = await tx.get(Stores.exchanges, exchangeBaseUrl);
|
const exchange = await tx.exchanges.get(exchangeBaseUrl);
|
||||||
if (!exchange) {
|
if (!exchange) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const coins = await tx
|
const coins = await tx.coins.indexes.byBaseUrl
|
||||||
.iterIndexed(Stores.coins.exchangeBaseUrlIndex, exchangeBaseUrl)
|
.iter(exchangeBaseUrl)
|
||||||
.toArray();
|
.toArray();
|
||||||
const refreshCoins: CoinPublicKey[] = [];
|
const refreshCoins: CoinPublicKey[] = [];
|
||||||
for (const coin of coins) {
|
for (const coin of coins) {
|
||||||
@ -783,7 +888,7 @@ export async function autoRefresh(
|
|||||||
if (coin.suspended) {
|
if (coin.suspended) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.denominations.get([
|
||||||
exchangeBaseUrl,
|
exchangeBaseUrl,
|
||||||
coin.denomPubHash,
|
coin.denomPubHash,
|
||||||
]);
|
]);
|
||||||
@ -800,8 +905,8 @@ export async function autoRefresh(
|
|||||||
await createRefreshGroup(ws, tx, refreshCoins, RefreshReason.Scheduled);
|
await createRefreshGroup(ws, tx, refreshCoins, RefreshReason.Scheduled);
|
||||||
}
|
}
|
||||||
|
|
||||||
const denoms = await tx
|
const denoms = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.iterIndexed(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
|
.iter(exchangeBaseUrl)
|
||||||
.toArray();
|
.toArray();
|
||||||
let minCheckThreshold = timestampAddDuration(
|
let minCheckThreshold = timestampAddDuration(
|
||||||
getTimestampNow(),
|
getTimestampNow(),
|
||||||
@ -817,7 +922,6 @@ export async function autoRefresh(
|
|||||||
minCheckThreshold = timestampMin(minCheckThreshold, checkThreshold);
|
minCheckThreshold = timestampMin(minCheckThreshold, checkThreshold);
|
||||||
}
|
}
|
||||||
exchange.nextRefreshCheck = minCheckThreshold;
|
exchange.nextRefreshCheck = minCheckThreshold;
|
||||||
await tx.put(Stores.exchanges, exchange);
|
await tx.exchanges.put(exchange);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -48,13 +48,21 @@ import {
|
|||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
import { TransactionHandle } from "../util/query";
|
|
||||||
import { URL } from "../util/url";
|
import { URL } from "../util/url";
|
||||||
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";
|
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";
|
||||||
import { checkDbInvariant } from "../util/invariants";
|
import { checkDbInvariant } from "../util/invariants";
|
||||||
import { TalerErrorCode } from "@gnu-taler/taler-util";
|
import { TalerErrorCode } from "@gnu-taler/taler-util";
|
||||||
import { Stores, PurchaseRecord, CoinStatus, RefundState, AbortStatus, RefundReason } from "../db.js";
|
import {
|
||||||
|
PurchaseRecord,
|
||||||
|
CoinStatus,
|
||||||
|
RefundState,
|
||||||
|
AbortStatus,
|
||||||
|
RefundReason,
|
||||||
|
WalletStoresV1,
|
||||||
|
} from "../db.js";
|
||||||
import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js";
|
import { getTotalRefreshCost, createRefreshGroup } from "./refresh.js";
|
||||||
|
import { GetReadWriteAccess } from "../util/query.js";
|
||||||
|
import { Wallet } from "../wallet.js";
|
||||||
|
|
||||||
const logger = new Logger("refund.ts");
|
const logger = new Logger("refund.ts");
|
||||||
|
|
||||||
@ -66,19 +74,23 @@ async function incrementPurchaseQueryRefundRetry(
|
|||||||
proposalId: string,
|
proposalId: string,
|
||||||
err: TalerErrorDetails | undefined,
|
err: TalerErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
|
await ws.db
|
||||||
const pr = await tx.get(Stores.purchases, proposalId);
|
.mktx((x) => ({
|
||||||
if (!pr) {
|
purchases: x.purchases,
|
||||||
return;
|
}))
|
||||||
}
|
.runReadWrite(async (tx) => {
|
||||||
if (!pr.refundStatusRetryInfo) {
|
const pr = await tx.purchases.get(proposalId);
|
||||||
return;
|
if (!pr) {
|
||||||
}
|
return;
|
||||||
pr.refundStatusRetryInfo.retryCounter++;
|
}
|
||||||
updateRetryInfoTimeout(pr.refundStatusRetryInfo);
|
if (!pr.refundStatusRetryInfo) {
|
||||||
pr.lastRefundStatusError = err;
|
return;
|
||||||
await tx.put(Stores.purchases, pr);
|
}
|
||||||
});
|
pr.refundStatusRetryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(pr.refundStatusRetryInfo);
|
||||||
|
pr.lastRefundStatusError = err;
|
||||||
|
await tx.purchases.put(pr);
|
||||||
|
});
|
||||||
if (err) {
|
if (err) {
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.RefundStatusOperationError,
|
type: NotificationType.RefundStatusOperationError,
|
||||||
@ -92,7 +104,10 @@ function getRefundKey(d: MerchantCoinRefundStatus): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function applySuccessfulRefund(
|
async function applySuccessfulRefund(
|
||||||
tx: TransactionHandle<typeof Stores.coins | typeof Stores.denominations>,
|
tx: GetReadWriteAccess<{
|
||||||
|
coins: typeof WalletStoresV1.coins;
|
||||||
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
|
}>,
|
||||||
p: PurchaseRecord,
|
p: PurchaseRecord,
|
||||||
refreshCoinsMap: Record<string, { coinPub: string }>,
|
refreshCoinsMap: Record<string, { coinPub: string }>,
|
||||||
r: MerchantCoinRefundSuccessStatus,
|
r: MerchantCoinRefundSuccessStatus,
|
||||||
@ -100,12 +115,12 @@ async function applySuccessfulRefund(
|
|||||||
// FIXME: check signature before storing it as valid!
|
// FIXME: check signature before storing it as valid!
|
||||||
|
|
||||||
const refundKey = getRefundKey(r);
|
const refundKey = getRefundKey(r);
|
||||||
const coin = await tx.get(Stores.coins, r.coin_pub);
|
const coin = await tx.coins.get(r.coin_pub);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
logger.warn("coin not found, can't apply refund");
|
logger.warn("coin not found, can't apply refund");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.denominations.get([
|
||||||
coin.exchangeBaseUrl,
|
coin.exchangeBaseUrl,
|
||||||
coin.denomPubHash,
|
coin.denomPubHash,
|
||||||
]);
|
]);
|
||||||
@ -119,13 +134,10 @@ async function applySuccessfulRefund(
|
|||||||
coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
|
coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
|
||||||
coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
|
coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
|
||||||
logger.trace(`coin amount after is ${Amounts.stringify(coin.currentAmount)}`);
|
logger.trace(`coin amount after is ${Amounts.stringify(coin.currentAmount)}`);
|
||||||
await tx.put(Stores.coins, coin);
|
await tx.coins.put(coin);
|
||||||
|
|
||||||
const allDenoms = await tx
|
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.iterIndexed(
|
.iter(coin.exchangeBaseUrl)
|
||||||
Stores.denominations.exchangeBaseUrlIndex,
|
|
||||||
coin.exchangeBaseUrl,
|
|
||||||
)
|
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
const amountLeft = Amounts.sub(
|
const amountLeft = Amounts.sub(
|
||||||
@ -153,18 +165,21 @@ async function applySuccessfulRefund(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function storePendingRefund(
|
async function storePendingRefund(
|
||||||
tx: TransactionHandle<typeof Stores.denominations | typeof Stores.coins>,
|
tx: GetReadWriteAccess<{
|
||||||
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
|
coins: typeof WalletStoresV1.coins;
|
||||||
|
}>,
|
||||||
p: PurchaseRecord,
|
p: PurchaseRecord,
|
||||||
r: MerchantCoinRefundFailureStatus,
|
r: MerchantCoinRefundFailureStatus,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const refundKey = getRefundKey(r);
|
const refundKey = getRefundKey(r);
|
||||||
|
|
||||||
const coin = await tx.get(Stores.coins, r.coin_pub);
|
const coin = await tx.coins.get(r.coin_pub);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
logger.warn("coin not found, can't apply refund");
|
logger.warn("coin not found, can't apply refund");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.denominations.get([
|
||||||
coin.exchangeBaseUrl,
|
coin.exchangeBaseUrl,
|
||||||
coin.denomPubHash,
|
coin.denomPubHash,
|
||||||
]);
|
]);
|
||||||
@ -173,11 +188,8 @@ async function storePendingRefund(
|
|||||||
throw Error("inconsistent database");
|
throw Error("inconsistent database");
|
||||||
}
|
}
|
||||||
|
|
||||||
const allDenoms = await tx
|
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.iterIndexed(
|
.iter(coin.exchangeBaseUrl)
|
||||||
Stores.denominations.exchangeBaseUrlIndex,
|
|
||||||
coin.exchangeBaseUrl,
|
|
||||||
)
|
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
const amountLeft = Amounts.sub(
|
const amountLeft = Amounts.sub(
|
||||||
@ -205,19 +217,22 @@ async function storePendingRefund(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function storeFailedRefund(
|
async function storeFailedRefund(
|
||||||
tx: TransactionHandle<typeof Stores.coins | typeof Stores.denominations>,
|
tx: GetReadWriteAccess<{
|
||||||
|
coins: typeof WalletStoresV1.coins;
|
||||||
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
|
}>,
|
||||||
p: PurchaseRecord,
|
p: PurchaseRecord,
|
||||||
refreshCoinsMap: Record<string, { coinPub: string }>,
|
refreshCoinsMap: Record<string, { coinPub: string }>,
|
||||||
r: MerchantCoinRefundFailureStatus,
|
r: MerchantCoinRefundFailureStatus,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const refundKey = getRefundKey(r);
|
const refundKey = getRefundKey(r);
|
||||||
|
|
||||||
const coin = await tx.get(Stores.coins, r.coin_pub);
|
const coin = await tx.coins.get(r.coin_pub);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
logger.warn("coin not found, can't apply refund");
|
logger.warn("coin not found, can't apply refund");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.denominations.get([
|
||||||
coin.exchangeBaseUrl,
|
coin.exchangeBaseUrl,
|
||||||
coin.denomPubHash,
|
coin.denomPubHash,
|
||||||
]);
|
]);
|
||||||
@ -226,11 +241,8 @@ async function storeFailedRefund(
|
|||||||
throw Error("inconsistent database");
|
throw Error("inconsistent database");
|
||||||
}
|
}
|
||||||
|
|
||||||
const allDenoms = await tx
|
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.iterIndexed(
|
.iter(coin.exchangeBaseUrl)
|
||||||
Stores.denominations.exchangeBaseUrlIndex,
|
|
||||||
coin.exchangeBaseUrl,
|
|
||||||
)
|
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
const amountLeft = Amounts.sub(
|
const amountLeft = Amounts.sub(
|
||||||
@ -260,12 +272,12 @@ async function storeFailedRefund(
|
|||||||
// Refund failed because the merchant didn't even try to deposit
|
// Refund failed because the merchant didn't even try to deposit
|
||||||
// the coin yet, so we try to refresh.
|
// the coin yet, so we try to refresh.
|
||||||
if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) {
|
if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) {
|
||||||
const coin = await tx.get(Stores.coins, r.coin_pub);
|
const coin = await tx.coins.get(r.coin_pub);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
logger.warn("coin not found, can't apply refund");
|
logger.warn("coin not found, can't apply refund");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.denominations.get([
|
||||||
coin.exchangeBaseUrl,
|
coin.exchangeBaseUrl,
|
||||||
coin.denomPubHash,
|
coin.denomPubHash,
|
||||||
]);
|
]);
|
||||||
@ -287,7 +299,7 @@ async function storeFailedRefund(
|
|||||||
).amount;
|
).amount;
|
||||||
}
|
}
|
||||||
refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
|
refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
|
||||||
await tx.put(Stores.coins, coin);
|
await tx.coins.put(coin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -301,15 +313,15 @@ async function acceptRefunds(
|
|||||||
logger.trace("handling refunds", refunds);
|
logger.trace("handling refunds", refunds);
|
||||||
const now = getTimestampNow();
|
const now = getTimestampNow();
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[
|
.mktx((x) => ({
|
||||||
Stores.purchases,
|
purchases: x.purchases,
|
||||||
Stores.coins,
|
coins: x.coins,
|
||||||
Stores.denominations,
|
denominations: x.denominations,
|
||||||
Stores.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
],
|
}))
|
||||||
async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const p = await tx.get(Stores.purchases, proposalId);
|
const p = await tx.purchases.get(proposalId);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
logger.error("purchase not found, not adding refunds");
|
logger.error("purchase not found, not adding refunds");
|
||||||
return;
|
return;
|
||||||
@ -409,9 +421,8 @@ async function acceptRefunds(
|
|||||||
logger.trace("refund query not done");
|
logger.trace("refund query not done");
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.put(Stores.purchases, p);
|
await tx.purchases.put(p);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.RefundQueried,
|
type: NotificationType.RefundQueried,
|
||||||
@ -444,10 +455,16 @@ export async function applyRefund(
|
|||||||
throw Error("invalid refund URI");
|
throw Error("invalid refund URI");
|
||||||
}
|
}
|
||||||
|
|
||||||
let purchase = await ws.db.getIndexed(Stores.purchases.orderIdIndex, [
|
let purchase = await ws.db
|
||||||
parseResult.merchantBaseUrl,
|
.mktx((x) => ({
|
||||||
parseResult.orderId,
|
purchases: x.purchases,
|
||||||
]);
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.purchases.indexes.byMerchantUrlAndOrderId.get([
|
||||||
|
parseResult.merchantBaseUrl,
|
||||||
|
parseResult.orderId,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
throw Error(
|
throw Error(
|
||||||
@ -458,10 +475,12 @@ export async function applyRefund(
|
|||||||
const proposalId = purchase.proposalId;
|
const proposalId = purchase.proposalId;
|
||||||
|
|
||||||
logger.info("processing purchase for refund");
|
logger.info("processing purchase for refund");
|
||||||
const success = await ws.db.runWithWriteTransaction(
|
const success = await ws.db
|
||||||
[Stores.purchases],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
purchases: x.purchases,
|
||||||
const p = await tx.get(Stores.purchases, proposalId);
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const p = await tx.purchases.get(proposalId);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
logger.error("no purchase found for refund URL");
|
logger.error("no purchase found for refund URL");
|
||||||
return false;
|
return false;
|
||||||
@ -469,10 +488,9 @@ export async function applyRefund(
|
|||||||
p.refundQueryRequested = true;
|
p.refundQueryRequested = true;
|
||||||
p.lastRefundStatusError = undefined;
|
p.lastRefundStatusError = undefined;
|
||||||
p.refundStatusRetryInfo = initRetryInfo();
|
p.refundStatusRetryInfo = initRetryInfo();
|
||||||
await tx.put(Stores.purchases, p);
|
await tx.purchases.put(p);
|
||||||
return true;
|
return true;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
ws.notify({
|
ws.notify({
|
||||||
@ -481,7 +499,13 @@ export async function applyRefund(
|
|||||||
await processPurchaseQueryRefund(ws, proposalId);
|
await processPurchaseQueryRefund(ws, proposalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
purchase = await ws.db.get(Stores.purchases, proposalId);
|
purchase = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
purchases: x.purchases,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.purchases.get(proposalId);
|
||||||
|
});
|
||||||
|
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
throw Error("purchase no longer exists");
|
throw Error("purchase no longer exists");
|
||||||
@ -559,12 +583,17 @@ async function resetPurchaseQueryRefundRetry(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.mutate(Stores.purchases, proposalId, (x) => {
|
await ws.db
|
||||||
if (x.refundStatusRetryInfo.active) {
|
.mktx((x) => ({
|
||||||
x.refundStatusRetryInfo = initRetryInfo();
|
purchases: x.purchases,
|
||||||
}
|
}))
|
||||||
return x;
|
.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(
|
async function processPurchaseQueryRefundImpl(
|
||||||
@ -575,7 +604,13 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
if (forceNow) {
|
if (forceNow) {
|
||||||
await resetPurchaseQueryRefundRetry(ws, proposalId);
|
await resetPurchaseQueryRefundRetry(ws, proposalId);
|
||||||
}
|
}
|
||||||
const purchase = await ws.db.get(Stores.purchases, proposalId);
|
const purchase = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
purchases: x.purchases,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.purchases.get(proposalId);
|
||||||
|
});
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -589,7 +624,6 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
`orders/${purchase.download.contractData.orderId}/refund`,
|
`orders/${purchase.download.contractData.orderId}/refund`,
|
||||||
purchase.download.contractData.merchantBaseUrl,
|
purchase.download.contractData.merchantBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
logger.trace(`making refund request to ${requestUrl.href}`);
|
logger.trace(`making refund request to ${requestUrl.href}`);
|
||||||
|
|
||||||
@ -620,18 +654,25 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const abortingCoins: AbortingCoin[] = [];
|
const abortingCoins: AbortingCoin[] = [];
|
||||||
for (let i = 0; i < purchase.payCoinSelection.coinPubs.length; i++) {
|
|
||||||
const coinPub = purchase.payCoinSelection.coinPubs[i];
|
await ws.db
|
||||||
const coin = await ws.db.get(Stores.coins, coinPub);
|
.mktx((x) => ({
|
||||||
checkDbInvariant(!!coin, "expected coin to be present");
|
coins: x.coins,
|
||||||
abortingCoins.push({
|
}))
|
||||||
coin_pub: coinPub,
|
.runReadOnly(async (tx) => {
|
||||||
contribution: Amounts.stringify(
|
for (let i = 0; i < purchase.payCoinSelection.coinPubs.length; i++) {
|
||||||
purchase.payCoinSelection.coinContributions[i],
|
const coinPub = purchase.payCoinSelection.coinPubs[i];
|
||||||
),
|
const coin = await tx.coins.get(coinPub);
|
||||||
exchange_url: coin.exchangeBaseUrl,
|
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 = {
|
const abortReq: AbortRequest = {
|
||||||
h_contract: purchase.download.contractData.contractTermsHash,
|
h_contract: purchase.download.contractData.contractTermsHash,
|
||||||
@ -678,26 +719,30 @@ export async function abortFailedPayWithRefund(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
|
await ws.db
|
||||||
const purchase = await tx.get(Stores.purchases, proposalId);
|
.mktx((x) => ({
|
||||||
if (!purchase) {
|
purchases: x.purchases,
|
||||||
throw Error("purchase not found");
|
}))
|
||||||
}
|
.runReadWrite(async (tx) => {
|
||||||
if (purchase.timestampFirstSuccessfulPay) {
|
const purchase = await tx.purchases.get(proposalId);
|
||||||
// No point in aborting it. We don't even report an error.
|
if (!purchase) {
|
||||||
logger.warn(`tried to abort successful payment`);
|
throw Error("purchase not found");
|
||||||
return;
|
}
|
||||||
}
|
if (purchase.timestampFirstSuccessfulPay) {
|
||||||
if (purchase.abortStatus !== AbortStatus.None) {
|
// No point in aborting it. We don't even report an error.
|
||||||
return;
|
logger.warn(`tried to abort successful payment`);
|
||||||
}
|
return;
|
||||||
purchase.refundQueryRequested = true;
|
}
|
||||||
purchase.paymentSubmitPending = false;
|
if (purchase.abortStatus !== AbortStatus.None) {
|
||||||
purchase.abortStatus = AbortStatus.AbortRefund;
|
return;
|
||||||
purchase.lastPayError = undefined;
|
}
|
||||||
purchase.payRetryInfo = initRetryInfo(false);
|
purchase.refundQueryRequested = true;
|
||||||
await tx.put(Stores.purchases, purchase);
|
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) => {
|
processPurchaseQueryRefund(ws, proposalId, true).catch((e) => {
|
||||||
logger.trace(`error during refund processing after abort pay: ${e}`);
|
logger.trace(`error during refund processing after abort pay: ${e}`);
|
||||||
});
|
});
|
||||||
|
@ -34,11 +34,11 @@ import {
|
|||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { randomBytes } from "../crypto/primitives/nacl-fast.js";
|
import { randomBytes } from "../crypto/primitives/nacl-fast.js";
|
||||||
import {
|
import {
|
||||||
Stores,
|
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
ReserveBankInfo,
|
ReserveBankInfo,
|
||||||
ReserveRecord,
|
ReserveRecord,
|
||||||
WithdrawalGroupRecord,
|
WithdrawalGroupRecord,
|
||||||
|
WalletStoresV1,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { assertUnreachable } from "../util/assertUnreachable.js";
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
|
import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
|
||||||
@ -65,9 +65,13 @@ import {
|
|||||||
import { getExchangeTrust } from "./currencies.js";
|
import { getExchangeTrust } from "./currencies.js";
|
||||||
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto.js";
|
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto.js";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import { readSuccessResponseJsonOrErrorCode, readSuccessResponseJsonOrThrow, throwUnexpectedRequestError } from "../util/http.js";
|
import {
|
||||||
|
readSuccessResponseJsonOrErrorCode,
|
||||||
|
readSuccessResponseJsonOrThrow,
|
||||||
|
throwUnexpectedRequestError,
|
||||||
|
} from "../util/http.js";
|
||||||
import { URL } from "../util/url.js";
|
import { URL } from "../util/url.js";
|
||||||
import { TransactionHandle } from "../util/query.js";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
|
|
||||||
const logger = new Logger("reserves.ts");
|
const logger = new Logger("reserves.ts");
|
||||||
|
|
||||||
@ -75,12 +79,17 @@ async function resetReserveRetry(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.mutate(Stores.reserves, reservePub, (x) => {
|
await ws.db
|
||||||
if (x.retryInfo.active) {
|
.mktx((x) => ({
|
||||||
x.retryInfo = initRetryInfo();
|
reserves: x.reserves,
|
||||||
}
|
}))
|
||||||
return x;
|
.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,
|
exchangeInfo.exchange,
|
||||||
);
|
);
|
||||||
|
|
||||||
const resp = await ws.db.runWithWriteTransaction(
|
const resp = await ws.db
|
||||||
[Stores.exchangeTrustStore, Stores.reserves, Stores.bankWithdrawUris],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
exchangeTrust: x.exchangeTrust,
|
||||||
|
reserves: x.reserves,
|
||||||
|
bankWithdrawUris: x.bankWithdrawUris,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
// Check if we have already created a reserve for that bankWithdrawStatusUrl
|
// Check if we have already created a reserve for that bankWithdrawStatusUrl
|
||||||
if (reserveRecord.bankInfo?.statusUrl) {
|
if (reserveRecord.bankInfo?.statusUrl) {
|
||||||
const bwi = await tx.get(
|
const bwi = await tx.bankWithdrawUris.get(
|
||||||
Stores.bankWithdrawUris,
|
|
||||||
reserveRecord.bankInfo.statusUrl,
|
reserveRecord.bankInfo.statusUrl,
|
||||||
);
|
);
|
||||||
if (bwi) {
|
if (bwi) {
|
||||||
const otherReserve = await tx.get(Stores.reserves, bwi.reservePub);
|
const otherReserve = await tx.reserves.get(bwi.reservePub);
|
||||||
if (otherReserve) {
|
if (otherReserve) {
|
||||||
logger.trace(
|
logger.trace(
|
||||||
"returning existing reserve for bankWithdrawStatusUri",
|
"returning existing reserve for bankWithdrawStatusUri",
|
||||||
@ -178,27 +190,26 @@ export async function createReserve(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await tx.put(Stores.bankWithdrawUris, {
|
await tx.bankWithdrawUris.put({
|
||||||
reservePub: reserveRecord.reservePub,
|
reservePub: reserveRecord.reservePub,
|
||||||
talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
|
talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!isAudited && !isTrusted) {
|
if (!isAudited && !isTrusted) {
|
||||||
await tx.put(Stores.exchangeTrustStore, {
|
await tx.exchangeTrust.put({
|
||||||
currency: reserveRecord.currency,
|
currency: reserveRecord.currency,
|
||||||
exchangeBaseUrl: reserveRecord.exchangeBaseUrl,
|
exchangeBaseUrl: reserveRecord.exchangeBaseUrl,
|
||||||
exchangeMasterPub: exchangeDetails.masterPublicKey,
|
exchangeMasterPub: exchangeDetails.masterPublicKey,
|
||||||
uids: [encodeCrock(getRandomBytes(32))],
|
uids: [encodeCrock(getRandomBytes(32))],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await tx.put(Stores.reserves, reserveRecord);
|
await tx.reserves.put(reserveRecord);
|
||||||
const r: CreateReserveResponse = {
|
const r: CreateReserveResponse = {
|
||||||
exchange: canonExchange,
|
exchange: canonExchange,
|
||||||
reservePub: keypair.pub,
|
reservePub: keypair.pub,
|
||||||
};
|
};
|
||||||
return r;
|
return r;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (reserveRecord.reservePub === resp.reservePub) {
|
if (reserveRecord.reservePub === resp.reservePub) {
|
||||||
// Only emit notification when a new reserve was created.
|
// Only emit notification when a new reserve was created.
|
||||||
@ -224,23 +235,27 @@ export async function forceQueryReserve(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
|
await ws.db
|
||||||
const reserve = await tx.get(Stores.reserves, reservePub);
|
.mktx((x) => ({
|
||||||
if (!reserve) {
|
reserves: x.reserves,
|
||||||
return;
|
}))
|
||||||
}
|
.runReadWrite(async (tx) => {
|
||||||
// Only force status query where it makes sense
|
const reserve = await tx.reserves.get(reservePub);
|
||||||
switch (reserve.reserveStatus) {
|
if (!reserve) {
|
||||||
case ReserveRecordStatus.DORMANT:
|
return;
|
||||||
reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
}
|
||||||
break;
|
// Only force status query where it makes sense
|
||||||
default:
|
switch (reserve.reserveStatus) {
|
||||||
reserve.requestedQuery = true;
|
case ReserveRecordStatus.DORMANT:
|
||||||
break;
|
reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
||||||
}
|
break;
|
||||||
reserve.retryInfo = initRetryInfo();
|
default:
|
||||||
await tx.put(Stores.reserves, reserve);
|
reserve.requestedQuery = true;
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
|
reserve.retryInfo = initRetryInfo();
|
||||||
|
await tx.reserves.put(reserve);
|
||||||
|
});
|
||||||
await processReserve(ws, reservePub, true);
|
await processReserve(ws, reservePub, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,7 +285,13 @@ async function registerReserveWithBank(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reserve = await ws.db.get(Stores.reserves, reservePub);
|
const reserve = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return await tx.reserves.get(reservePub);
|
||||||
|
});
|
||||||
switch (reserve?.reserveStatus) {
|
switch (reserve?.reserveStatus) {
|
||||||
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
||||||
case ReserveRecordStatus.REGISTERING_BANK:
|
case ReserveRecordStatus.REGISTERING_BANK:
|
||||||
@ -297,22 +318,30 @@ async function registerReserveWithBank(
|
|||||||
httpResp,
|
httpResp,
|
||||||
codecForBankWithdrawalOperationPostResponse(),
|
codecForBankWithdrawalOperationPostResponse(),
|
||||||
);
|
);
|
||||||
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
|
await ws.db
|
||||||
switch (r.reserveStatus) {
|
.mktx((x) => ({
|
||||||
case ReserveRecordStatus.REGISTERING_BANK:
|
reserves: x.reserves,
|
||||||
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
}))
|
||||||
break;
|
.runReadWrite(async (tx) => {
|
||||||
default:
|
const r = await tx.reserves.get(reservePub);
|
||||||
|
if (!r) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
r.timestampReserveInfoPosted = getTimestampNow();
|
switch (r.reserveStatus) {
|
||||||
r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
|
case ReserveRecordStatus.REGISTERING_BANK:
|
||||||
if (!r.bankInfo) {
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
||||||
throw Error("invariant failed");
|
break;
|
||||||
}
|
default:
|
||||||
r.retryInfo = initRetryInfo();
|
return;
|
||||||
return r;
|
}
|
||||||
});
|
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 });
|
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
|
||||||
return processReserveBankStatus(ws, reservePub);
|
return processReserveBankStatus(ws, reservePub);
|
||||||
}
|
}
|
||||||
@ -340,7 +369,13 @@ async function processReserveBankStatusImpl(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reserve = await ws.db.get(Stores.reserves, reservePub);
|
const reserve = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.reserves.get(reservePub);
|
||||||
|
});
|
||||||
switch (reserve?.reserveStatus) {
|
switch (reserve?.reserveStatus) {
|
||||||
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
||||||
case ReserveRecordStatus.REGISTERING_BANK:
|
case ReserveRecordStatus.REGISTERING_BANK:
|
||||||
@ -363,20 +398,28 @@ async function processReserveBankStatusImpl(
|
|||||||
|
|
||||||
if (status.aborted) {
|
if (status.aborted) {
|
||||||
logger.trace("bank aborted the withdrawal");
|
logger.trace("bank aborted the withdrawal");
|
||||||
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
|
await ws.db
|
||||||
switch (r.reserveStatus) {
|
.mktx((x) => ({
|
||||||
case ReserveRecordStatus.REGISTERING_BANK:
|
reserves: x.reserves,
|
||||||
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
}))
|
||||||
break;
|
.runReadWrite(async (tx) => {
|
||||||
default:
|
const r = await tx.reserves.get(reservePub);
|
||||||
|
if (!r) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const now = getTimestampNow();
|
switch (r.reserveStatus) {
|
||||||
r.timestampBankConfirmed = now;
|
case ReserveRecordStatus.REGISTERING_BANK:
|
||||||
r.reserveStatus = ReserveRecordStatus.BANK_ABORTED;
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
||||||
r.retryInfo = initRetryInfo();
|
break;
|
||||||
return r;
|
default:
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
const now = getTimestampNow();
|
||||||
|
r.timestampBankConfirmed = now;
|
||||||
|
r.reserveStatus = ReserveRecordStatus.BANK_ABORTED;
|
||||||
|
r.retryInfo = initRetryInfo();
|
||||||
|
await tx.reserves.put(r);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,37 +433,40 @@ async function processReserveBankStatusImpl(
|
|||||||
return await processReserveBankStatus(ws, reservePub);
|
return await processReserveBankStatus(ws, reservePub);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.transfer_done) {
|
await ws.db
|
||||||
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
|
.mktx((x) => ({
|
||||||
switch (r.reserveStatus) {
|
reserves: x.reserves,
|
||||||
case ReserveRecordStatus.REGISTERING_BANK:
|
}))
|
||||||
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
.runReadWrite(async (tx) => {
|
||||||
break;
|
const r = await tx.reserves.get(reservePub);
|
||||||
default:
|
if (!r) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const now = getTimestampNow();
|
if (status.transfer_done) {
|
||||||
r.timestampBankConfirmed = now;
|
switch (r.reserveStatus) {
|
||||||
r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
case ReserveRecordStatus.REGISTERING_BANK:
|
||||||
r.retryInfo = initRetryInfo();
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
||||||
return r;
|
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(
|
async function incrementReserveRetry(
|
||||||
@ -428,19 +474,23 @@ async function incrementReserveRetry(
|
|||||||
reservePub: string,
|
reservePub: string,
|
||||||
err: TalerErrorDetails | undefined,
|
err: TalerErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
|
await ws.db
|
||||||
const r = await tx.get(Stores.reserves, reservePub);
|
.mktx((x) => ({
|
||||||
if (!r) {
|
reserves: x.reserves,
|
||||||
return;
|
}))
|
||||||
}
|
.runReadWrite(async (tx) => {
|
||||||
if (!r.retryInfo) {
|
const r = await tx.reserves.get(reservePub);
|
||||||
return;
|
if (!r) {
|
||||||
}
|
return;
|
||||||
r.retryInfo.retryCounter++;
|
}
|
||||||
updateRetryInfoTimeout(r.retryInfo);
|
if (!r.retryInfo) {
|
||||||
r.lastError = err;
|
return;
|
||||||
await tx.put(Stores.reserves, r);
|
}
|
||||||
});
|
r.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(r.retryInfo);
|
||||||
|
r.lastError = err;
|
||||||
|
await tx.reserves.put(r);
|
||||||
|
});
|
||||||
if (err) {
|
if (err) {
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.ReserveOperationError,
|
type: NotificationType.ReserveOperationError,
|
||||||
@ -461,7 +511,13 @@ async function updateReserve(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
): Promise<{ ready: boolean }> {
|
): Promise<{ ready: boolean }> {
|
||||||
const reserve = await ws.db.get(Stores.reserves, reservePub);
|
const reserve = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.reserves.get(reservePub);
|
||||||
|
});
|
||||||
if (!reserve) {
|
if (!reserve) {
|
||||||
throw Error("reserve not in db");
|
throw Error("reserve not in db");
|
||||||
}
|
}
|
||||||
@ -508,10 +564,15 @@ async function updateReserve(
|
|||||||
reserve.exchangeBaseUrl,
|
reserve.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
|
const newWithdrawalGroup = await ws.db
|
||||||
[Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
coins: x.coins,
|
||||||
const newReserve = await tx.get(Stores.reserves, reserve.reservePub);
|
planchets: x.planchets,
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const newReserve = await tx.reserves.get(reserve.reservePub);
|
||||||
if (!newReserve) {
|
if (!newReserve) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -519,8 +580,8 @@ async function updateReserve(
|
|||||||
let amountReserveMinus = Amounts.getZero(currency);
|
let amountReserveMinus = Amounts.getZero(currency);
|
||||||
|
|
||||||
// Subtract withdrawal groups for this reserve from the available amount.
|
// Subtract withdrawal groups for this reserve from the available amount.
|
||||||
await tx
|
await tx.withdrawalGroups.indexes.byReservePub
|
||||||
.iterIndexed(Stores.withdrawalGroups.byReservePub, reservePub)
|
.iter(reservePub)
|
||||||
.forEach((wg) => {
|
.forEach((wg) => {
|
||||||
const cost = wg.denomsSel.totalWithdrawCost;
|
const cost = wg.denomsSel.totalWithdrawCost;
|
||||||
amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount;
|
amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount;
|
||||||
@ -549,16 +610,14 @@ async function updateReserve(
|
|||||||
case ReserveTransactionType.Withdraw: {
|
case ReserveTransactionType.Withdraw: {
|
||||||
// Now we check if the withdrawal transaction
|
// Now we check if the withdrawal transaction
|
||||||
// is part of any withdrawal known to this wallet.
|
// is part of any withdrawal known to this wallet.
|
||||||
const planchet = await tx.getIndexed(
|
const planchet = await tx.planchets.indexes.byCoinEvHash.get(
|
||||||
Stores.planchets.coinEvHashIndex,
|
|
||||||
entry.h_coin_envelope,
|
entry.h_coin_envelope,
|
||||||
);
|
);
|
||||||
if (planchet) {
|
if (planchet) {
|
||||||
// Amount is already accounted in some withdrawal session
|
// Amount is already accounted in some withdrawal session
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const coin = await tx.getIndexed(
|
const coin = await tx.coins.indexes.byCoinEvHash.get(
|
||||||
Stores.coins.coinEvHashIndex,
|
|
||||||
entry.h_coin_envelope,
|
entry.h_coin_envelope,
|
||||||
);
|
);
|
||||||
if (coin) {
|
if (coin) {
|
||||||
@ -594,7 +653,7 @@ async function updateReserve(
|
|||||||
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
|
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
|
||||||
newReserve.lastError = undefined;
|
newReserve.lastError = undefined;
|
||||||
newReserve.retryInfo = initRetryInfo(false);
|
newReserve.retryInfo = initRetryInfo(false);
|
||||||
await tx.put(Stores.reserves, newReserve);
|
await tx.reserves.put(newReserve);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -624,11 +683,10 @@ async function updateReserve(
|
|||||||
newReserve.retryInfo = initRetryInfo(false);
|
newReserve.retryInfo = initRetryInfo(false);
|
||||||
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
|
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
|
||||||
|
|
||||||
await tx.put(Stores.reserves, newReserve);
|
await tx.reserves.put(newReserve);
|
||||||
await tx.put(Stores.withdrawalGroups, withdrawalRecord);
|
await tx.withdrawalGroups.put(withdrawalRecord);
|
||||||
return withdrawalRecord;
|
return withdrawalRecord;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (newWithdrawalGroup) {
|
if (newWithdrawalGroup) {
|
||||||
logger.trace("processing new withdraw group");
|
logger.trace("processing new withdraw group");
|
||||||
@ -647,7 +705,13 @@ async function processReserveImpl(
|
|||||||
reservePub: string,
|
reservePub: string,
|
||||||
forceNow = false,
|
forceNow = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reserve = await ws.db.get(Stores.reserves, reservePub);
|
const reserve = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.reserves.get(reservePub);
|
||||||
|
});
|
||||||
if (!reserve) {
|
if (!reserve) {
|
||||||
logger.trace("not processing reserve: reserve does not exist");
|
logger.trace("not processing reserve: reserve does not exist");
|
||||||
return;
|
return;
|
||||||
@ -712,7 +776,13 @@ export async function createTalerWithdrawReserve(
|
|||||||
// We do this here, as the reserve should be registered before we return,
|
// We do this here, as the reserve should be registered before we return,
|
||||||
// so that we can redirect the user to the bank's status page.
|
// so that we can redirect the user to the bank's status page.
|
||||||
await processReserveBankStatus(ws, reserve.reservePub);
|
await processReserveBankStatus(ws, reserve.reservePub);
|
||||||
const processedReserve = await ws.db.get(Stores.reserves, reserve.reservePub);
|
const processedReserve = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.reserves.get(reserve.reservePub);
|
||||||
|
});
|
||||||
if (processedReserve?.reserveStatus === ReserveRecordStatus.BANK_ABORTED) {
|
if (processedReserve?.reserveStatus === ReserveRecordStatus.BANK_ABORTED) {
|
||||||
throw OperationFailedError.fromCode(
|
throw OperationFailedError.fromCode(
|
||||||
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
|
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
|
||||||
@ -730,14 +800,14 @@ export async function createTalerWithdrawReserve(
|
|||||||
* Get payto URIs needed to fund a reserve.
|
* Get payto URIs needed to fund a reserve.
|
||||||
*/
|
*/
|
||||||
export async function getFundingPaytoUris(
|
export async function getFundingPaytoUris(
|
||||||
tx: TransactionHandle<
|
tx: GetReadOnlyAccess<{
|
||||||
| typeof Stores.reserves
|
reserves: typeof WalletStoresV1.reserves;
|
||||||
| typeof Stores.exchanges
|
exchanges: typeof WalletStoresV1.exchanges;
|
||||||
| typeof Stores.exchangeDetails
|
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
||||||
>,
|
}>,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const r = await tx.get(Stores.reserves, reservePub);
|
const r = await tx.reserves.get(reservePub);
|
||||||
if (!r) {
|
if (!r) {
|
||||||
logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
|
logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
|
||||||
return [];
|
return [];
|
||||||
|
@ -17,12 +17,22 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { WalletNotification, BalancesResponse, Logger } from "@gnu-taler/taler-util";
|
import {
|
||||||
import { Stores } from "../db.js";
|
WalletNotification,
|
||||||
import { CryptoApi, OpenedPromise, Database, CryptoWorkerFactory, openPromise } from "../index.js";
|
BalancesResponse,
|
||||||
|
Logger,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { WalletStoresV1 } from "../db.js";
|
||||||
|
import {
|
||||||
|
CryptoApi,
|
||||||
|
OpenedPromise,
|
||||||
|
CryptoWorkerFactory,
|
||||||
|
openPromise,
|
||||||
|
} from "../index.js";
|
||||||
import { PendingOperationsResponse } from "../pending-types.js";
|
import { PendingOperationsResponse } from "../pending-types.js";
|
||||||
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo.js";
|
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo.js";
|
||||||
import { HttpRequestLibrary } from "../util/http";
|
import { HttpRequestLibrary } from "../util/http";
|
||||||
|
import { DbAccess } from "../util/query.js";
|
||||||
|
|
||||||
type NotificationListener = (n: WalletNotification) => void;
|
type NotificationListener = (n: WalletNotification) => void;
|
||||||
|
|
||||||
@ -34,9 +44,7 @@ export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock";
|
|||||||
export class InternalWalletState {
|
export class InternalWalletState {
|
||||||
memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
memoGetPending: AsyncOpMemoSingle<
|
memoGetPending: AsyncOpMemoSingle<PendingOperationsResponse> = new AsyncOpMemoSingle();
|
||||||
PendingOperationsResponse
|
|
||||||
> = new AsyncOpMemoSingle();
|
|
||||||
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
|
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
|
||||||
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
@ -60,7 +68,7 @@ export class InternalWalletState {
|
|||||||
// the actual value nullable.
|
// the actual value nullable.
|
||||||
// Check if we are in a DB migration / garbage collection
|
// Check if we are in a DB migration / garbage collection
|
||||||
// and throw an error in that case.
|
// and throw an error in that case.
|
||||||
public db: Database<typeof Stores>,
|
public db: DbAccess<typeof WalletStoresV1>,
|
||||||
public http: HttpRequestLibrary,
|
public http: HttpRequestLibrary,
|
||||||
cryptoWorkerFactory: CryptoWorkerFactory,
|
cryptoWorkerFactory: CryptoWorkerFactory,
|
||||||
) {
|
) {
|
||||||
|
@ -32,7 +32,6 @@ import {
|
|||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
|
import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
|
||||||
import {
|
import {
|
||||||
Stores,
|
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
@ -70,10 +69,16 @@ export async function prepareTip(
|
|||||||
throw Error("invalid taler://tip URI");
|
throw Error("invalid taler://tip URI");
|
||||||
}
|
}
|
||||||
|
|
||||||
let tipRecord = await ws.db.getIndexed(
|
let tipRecord = await ws.db
|
||||||
Stores.tips.byMerchantTipIdAndBaseUrl,
|
.mktx((x) => ({
|
||||||
[res.merchantTipId, res.merchantBaseUrl],
|
tips: x.tips,
|
||||||
);
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.tips.indexes.byMerchantTipIdAndBaseUrl.get([
|
||||||
|
res.merchantTipId,
|
||||||
|
res.merchantBaseUrl,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
if (!tipRecord) {
|
if (!tipRecord) {
|
||||||
const tipStatusUrl = new URL(
|
const tipStatusUrl = new URL(
|
||||||
@ -109,7 +114,7 @@ export async function prepareTip(
|
|||||||
const secretSeed = encodeCrock(getRandomBytes(64));
|
const secretSeed = encodeCrock(getRandomBytes(64));
|
||||||
const denomSelUid = encodeCrock(getRandomBytes(32));
|
const denomSelUid = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
tipRecord = {
|
const newTipRecord = {
|
||||||
walletTipId: walletTipId,
|
walletTipId: walletTipId,
|
||||||
acceptedTimestamp: undefined,
|
acceptedTimestamp: undefined,
|
||||||
tipAmountRaw: amount,
|
tipAmountRaw: amount,
|
||||||
@ -130,7 +135,14 @@ export async function prepareTip(
|
|||||||
secretSeed,
|
secretSeed,
|
||||||
denomSelUid,
|
denomSelUid,
|
||||||
};
|
};
|
||||||
await ws.db.put(Stores.tips, tipRecord);
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
tips: x.tips,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
await tx.tips.put(newTipRecord);
|
||||||
|
});
|
||||||
|
tipRecord = newTipRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tipStatus: PrepareTipResult = {
|
const tipStatus: PrepareTipResult = {
|
||||||
@ -151,19 +163,23 @@ async function incrementTipRetry(
|
|||||||
walletTipId: string,
|
walletTipId: string,
|
||||||
err: TalerErrorDetails | undefined,
|
err: TalerErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => {
|
await ws.db
|
||||||
const t = await tx.get(Stores.tips, walletTipId);
|
.mktx((x) => ({
|
||||||
if (!t) {
|
tips: x.tips,
|
||||||
return;
|
}))
|
||||||
}
|
.runReadWrite(async (tx) => {
|
||||||
if (!t.retryInfo) {
|
const t = await tx.tips.get(walletTipId);
|
||||||
return;
|
if (!t) {
|
||||||
}
|
return;
|
||||||
t.retryInfo.retryCounter++;
|
}
|
||||||
updateRetryInfoTimeout(t.retryInfo);
|
if (!t.retryInfo) {
|
||||||
t.lastError = err;
|
return;
|
||||||
await tx.put(Stores.tips, t);
|
}
|
||||||
});
|
t.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(t.retryInfo);
|
||||||
|
t.lastError = err;
|
||||||
|
await tx.tips.put(t);
|
||||||
|
});
|
||||||
if (err) {
|
if (err) {
|
||||||
ws.notify({ type: NotificationType.TipOperationError, error: err });
|
ws.notify({ type: NotificationType.TipOperationError, error: err });
|
||||||
}
|
}
|
||||||
@ -186,12 +202,17 @@ async function resetTipRetry(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tipId: string,
|
tipId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.mutate(Stores.tips, tipId, (x) => {
|
await ws.db
|
||||||
if (x.retryInfo.active) {
|
.mktx((x) => ({
|
||||||
x.retryInfo = initRetryInfo();
|
tips: x.tips,
|
||||||
}
|
}))
|
||||||
return x;
|
.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(
|
async function processTipImpl(
|
||||||
@ -202,7 +223,13 @@ async function processTipImpl(
|
|||||||
if (forceNow) {
|
if (forceNow) {
|
||||||
await resetTipRetry(ws, walletTipId);
|
await resetTipRetry(ws, walletTipId);
|
||||||
}
|
}
|
||||||
let tipRecord = await ws.db.get(Stores.tips, walletTipId);
|
const tipRecord = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
tips: x.tips,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.tips.get(walletTipId);
|
||||||
|
});
|
||||||
if (!tipRecord) {
|
if (!tipRecord) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -214,19 +241,22 @@ async function processTipImpl(
|
|||||||
|
|
||||||
const denomsForWithdraw = tipRecord.denomsSel;
|
const denomsForWithdraw = tipRecord.denomsSel;
|
||||||
|
|
||||||
tipRecord = await ws.db.get(Stores.tips, walletTipId);
|
|
||||||
checkDbInvariant(!!tipRecord, "tip record should be in database");
|
|
||||||
|
|
||||||
const planchets: DerivedTipPlanchet[] = [];
|
const planchets: DerivedTipPlanchet[] = [];
|
||||||
// Planchets in the form that the merchant expects
|
// Planchets in the form that the merchant expects
|
||||||
const planchetsDetail: TipPlanchetDetail[] = [];
|
const planchetsDetail: TipPlanchetDetail[] = [];
|
||||||
const denomForPlanchet: { [index: number]: DenominationRecord } = [];
|
const denomForPlanchet: { [index: number]: DenominationRecord } = [];
|
||||||
|
|
||||||
for (const dh of denomsForWithdraw.selectedDenoms) {
|
for (const dh of denomsForWithdraw.selectedDenoms) {
|
||||||
const denom = await ws.db.get(Stores.denominations, [
|
const denom = await ws.db
|
||||||
tipRecord.exchangeBaseUrl,
|
.mktx((x) => ({
|
||||||
dh.denomPubHash,
|
denominations: x.denominations,
|
||||||
]);
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.denominations.get([
|
||||||
|
tipRecord.exchangeBaseUrl,
|
||||||
|
dh.denomPubHash,
|
||||||
|
]);
|
||||||
|
});
|
||||||
checkDbInvariant(!!denom, "denomination should be in database");
|
checkDbInvariant(!!denom, "denomination should be in database");
|
||||||
for (let i = 0; i < dh.count; i++) {
|
for (let i = 0; i < dh.count; i++) {
|
||||||
const deriveReq = {
|
const deriveReq = {
|
||||||
@ -306,18 +336,20 @@ async function processTipImpl(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => {
|
await ws.db
|
||||||
const tipRecord = await tx.get(Stores.tips, walletTipId);
|
.mktx((x) => ({ tips: x.tips }))
|
||||||
if (!tipRecord) {
|
.runReadWrite(async (tx) => {
|
||||||
return;
|
const tipRecord = await tx.tips.get(walletTipId);
|
||||||
}
|
if (!tipRecord) {
|
||||||
tipRecord.lastError = makeErrorDetails(
|
return;
|
||||||
TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID,
|
}
|
||||||
"invalid signature from the exchange (via merchant tip) after unblinding",
|
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 tx.tips.put(tipRecord);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,10 +373,14 @@ async function processTipImpl(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.coins, Stores.tips, Stores.withdrawalGroups],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
coins: x.coins,
|
||||||
const tr = await tx.get(Stores.tips, walletTipId);
|
tips: x.tips,
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const tr = await tx.tips.get(walletTipId);
|
||||||
if (!tr) {
|
if (!tr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -354,27 +390,32 @@ async function processTipImpl(
|
|||||||
tr.pickedUpTimestamp = getTimestampNow();
|
tr.pickedUpTimestamp = getTimestampNow();
|
||||||
tr.lastError = undefined;
|
tr.lastError = undefined;
|
||||||
tr.retryInfo = initRetryInfo(false);
|
tr.retryInfo = initRetryInfo(false);
|
||||||
await tx.put(Stores.tips, tr);
|
await tx.tips.put(tr);
|
||||||
for (const cr of newCoinRecords) {
|
for (const cr of newCoinRecords) {
|
||||||
await tx.put(Stores.coins, cr);
|
await tx.coins.put(cr);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acceptTip(
|
export async function acceptTip(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tipId: string,
|
tipId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const tipRecord = await ws.db.get(Stores.tips, tipId);
|
const found = await ws.db
|
||||||
if (!tipRecord) {
|
.mktx((x) => ({
|
||||||
logger.error("tip not found");
|
tips: x.tips,
|
||||||
return;
|
}))
|
||||||
|
.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 { InternalWalletState } from "./state";
|
||||||
import {
|
import {
|
||||||
Stores,
|
|
||||||
WalletRefundItem,
|
WalletRefundItem,
|
||||||
RefundState,
|
RefundState,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
@ -85,296 +84,300 @@ export async function getTransactions(
|
|||||||
): Promise<TransactionsResponse> {
|
): Promise<TransactionsResponse> {
|
||||||
const transactions: Transaction[] = [];
|
const transactions: Transaction[] = [];
|
||||||
|
|
||||||
await ws.db.runWithReadTransaction(
|
await ws.db
|
||||||
[
|
.mktx((x) => ({
|
||||||
Stores.coins,
|
coins: x.coins,
|
||||||
Stores.denominations,
|
denominations: x.denominations,
|
||||||
Stores.exchanges,
|
exchanges: x.exchanges,
|
||||||
Stores.exchangeDetails,
|
exchangeDetails: x.exchangeDetails,
|
||||||
Stores.proposals,
|
proposals: x.proposals,
|
||||||
Stores.purchases,
|
purchases: x.purchases,
|
||||||
Stores.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
Stores.reserves,
|
reserves: x.reserves,
|
||||||
Stores.tips,
|
tips: x.tips,
|
||||||
Stores.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
Stores.planchets,
|
planchets: x.planchets,
|
||||||
Stores.recoupGroups,
|
recoupGroups: x.recoupGroups,
|
||||||
Stores.depositGroups,
|
depositGroups: x.depositGroups,
|
||||||
Stores.tombstones,
|
tombstones: x.tombstones,
|
||||||
],
|
}))
|
||||||
// Report withdrawals that are currently in progress.
|
.runReadOnly(
|
||||||
async (tx) => {
|
// Report withdrawals that are currently in progress.
|
||||||
tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => {
|
async (tx) => {
|
||||||
if (
|
tx.withdrawalGroups.iter().forEachAsync(async (wsr) => {
|
||||||
shouldSkipCurrency(
|
if (
|
||||||
transactionsRequest,
|
shouldSkipCurrency(
|
||||||
wsr.rawWithdrawalAmount.currency,
|
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
|
|
||||||
return;
|
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
|
if (shouldSkipSearch(transactionsRequest, [])) {
|
||||||
// were created, but where the actual withdrawal group has
|
return;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
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) {
|
const r = await tx.reserves.get(wsr.reservePub);
|
||||||
amountRaw = Amounts.add(amountRaw, refund.refundAmount).amount;
|
if (!r) {
|
||||||
amountEffective = Amounts.add(
|
return;
|
||||||
amountEffective,
|
|
||||||
Amounts.sub(
|
|
||||||
refund.refundAmount,
|
|
||||||
refund.refundFee,
|
|
||||||
refund.totalRefreshCostBound,
|
|
||||||
).amount,
|
|
||||||
).amount;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!r0) {
|
let amountRaw: AmountJson | undefined = undefined;
|
||||||
throw Error("invariant violated");
|
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({
|
transactions.push({
|
||||||
type: TransactionType.Refund,
|
type: TransactionType.Withdrawal,
|
||||||
info,
|
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
||||||
refundedTransactionId: paymentTransactionId,
|
|
||||||
transactionId: refundTransactionId,
|
|
||||||
timestamp: r0.obtainedTime,
|
|
||||||
amountEffective: Amounts.stringify(amountEffective),
|
|
||||||
amountRaw: Amounts.stringify(amountRaw),
|
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 txPending = transactions.filter((x) => x.pending);
|
||||||
const txNotPending = transactions.filter((x) => !x.pending);
|
const txNotPending = transactions.filter((x) => !x.pending);
|
||||||
@ -406,110 +409,126 @@ export async function deleteTransaction(
|
|||||||
|
|
||||||
if (type === TransactionType.Withdrawal) {
|
if (type === TransactionType.Withdrawal) {
|
||||||
const withdrawalGroupId = rest[0];
|
const withdrawalGroupId = rest[0];
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.withdrawalGroups, Stores.reserves, Stores.tombstones],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
const withdrawalGroupRecord = await tx.get(
|
reserves: x.reserves,
|
||||||
Stores.withdrawalGroups,
|
tombstones: x.tombstones,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const withdrawalGroupRecord = await tx.withdrawalGroups.get(
|
||||||
withdrawalGroupId,
|
withdrawalGroupId,
|
||||||
);
|
);
|
||||||
if (withdrawalGroupRecord) {
|
if (withdrawalGroupRecord) {
|
||||||
await tx.delete(Stores.withdrawalGroups, withdrawalGroupId);
|
await tx.withdrawalGroups.delete(withdrawalGroupId);
|
||||||
await tx.put(Stores.tombstones, {
|
await tx.tombstones.put({
|
||||||
id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
|
id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reserveRecord: ReserveRecord | undefined = await tx.getIndexed(
|
const reserveRecord:
|
||||||
Stores.reserves.byInitialWithdrawalGroupId,
|
| ReserveRecord
|
||||||
|
| undefined = await tx.reserves.indexes.byInitialWithdrawalGroupId.get(
|
||||||
withdrawalGroupId,
|
withdrawalGroupId,
|
||||||
);
|
);
|
||||||
if (reserveRecord && !reserveRecord.initialWithdrawalStarted) {
|
if (reserveRecord && !reserveRecord.initialWithdrawalStarted) {
|
||||||
const reservePub = reserveRecord.reservePub;
|
const reservePub = reserveRecord.reservePub;
|
||||||
await tx.delete(Stores.reserves, reservePub);
|
await tx.reserves.delete(reservePub);
|
||||||
await tx.put(Stores.tombstones, {
|
await tx.tombstones.put({
|
||||||
id: TombstoneTag.DeleteReserve + ":" + reservePub,
|
id: TombstoneTag.DeleteReserve + ":" + reservePub,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
} else if (type === TransactionType.Payment) {
|
} else if (type === TransactionType.Payment) {
|
||||||
const proposalId = rest[0];
|
const proposalId = rest[0];
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.proposals, Stores.purchases, Stores.tombstones],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
proposals: x.proposals,
|
||||||
|
purchases: x.purchases,
|
||||||
|
tombstones: x.tombstones,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
let found = false;
|
let found = false;
|
||||||
const proposal = await tx.get(Stores.proposals, proposalId);
|
const proposal = await tx.proposals.get(proposalId);
|
||||||
if (proposal) {
|
if (proposal) {
|
||||||
found = true;
|
found = true;
|
||||||
await tx.delete(Stores.proposals, proposalId);
|
await tx.proposals.delete(proposalId);
|
||||||
}
|
}
|
||||||
const purchase = await tx.get(Stores.purchases, proposalId);
|
const purchase = await tx.purchases.get(proposalId);
|
||||||
if (purchase) {
|
if (purchase) {
|
||||||
found = true;
|
found = true;
|
||||||
await tx.delete(Stores.proposals, proposalId);
|
await tx.proposals.delete(proposalId);
|
||||||
}
|
}
|
||||||
if (found) {
|
if (found) {
|
||||||
await tx.put(Stores.tombstones, {
|
await tx.tombstones.put({
|
||||||
id: TombstoneTag.DeletePayment + ":" + proposalId,
|
id: TombstoneTag.DeletePayment + ":" + proposalId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
} else if (type === TransactionType.Refresh) {
|
} else if (type === TransactionType.Refresh) {
|
||||||
const refreshGroupId = rest[0];
|
const refreshGroupId = rest[0];
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.refreshGroups, Stores.tombstones],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
refreshGroups: x.refreshGroups,
|
||||||
const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
|
tombstones: x.tombstones,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const rg = await tx.refreshGroups.get(refreshGroupId);
|
||||||
if (rg) {
|
if (rg) {
|
||||||
await tx.delete(Stores.refreshGroups, refreshGroupId);
|
await tx.refreshGroups.delete(refreshGroupId);
|
||||||
await tx.put(Stores.tombstones, {
|
await tx.tombstones.put({
|
||||||
id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
|
id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
} else if (type === TransactionType.Tip) {
|
} else if (type === TransactionType.Tip) {
|
||||||
const tipId = rest[0];
|
const tipId = rest[0];
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.tips, Stores.tombstones],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
tips: x.tips,
|
||||||
const tipRecord = await tx.get(Stores.tips, tipId);
|
tombstones: x.tombstones,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const tipRecord = await tx.tips.get(tipId);
|
||||||
if (tipRecord) {
|
if (tipRecord) {
|
||||||
await tx.delete(Stores.tips, tipId);
|
await tx.tips.delete(tipId);
|
||||||
await tx.put(Stores.tombstones, {
|
await tx.tombstones.put({
|
||||||
id: TombstoneTag.DeleteTip + ":" + tipId,
|
id: TombstoneTag.DeleteTip + ":" + tipId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
} else if (type === TransactionType.Deposit) {
|
} else if (type === TransactionType.Deposit) {
|
||||||
const depositGroupId = rest[0];
|
const depositGroupId = rest[0];
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.depositGroups, Stores.tombstones],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
depositGroups: x.depositGroups,
|
||||||
const tipRecord = await tx.get(Stores.depositGroups, depositGroupId);
|
tombstones: x.tombstones,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const tipRecord = await tx.depositGroups.get(depositGroupId);
|
||||||
if (tipRecord) {
|
if (tipRecord) {
|
||||||
await tx.delete(Stores.depositGroups, depositGroupId);
|
await tx.depositGroups.delete(depositGroupId);
|
||||||
await tx.put(Stores.tombstones, {
|
await tx.tombstones.put({
|
||||||
id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
|
id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
} else if (type === TransactionType.Refund) {
|
} else if (type === TransactionType.Refund) {
|
||||||
const proposalId = rest[0];
|
const proposalId = rest[0];
|
||||||
const executionTimeStr = rest[1];
|
const executionTimeStr = rest[1];
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.proposals, Stores.purchases, Stores.tombstones],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
proposals: x.proposals,
|
||||||
const purchase = await tx.get(Stores.purchases, proposalId);
|
purchases: x.purchases,
|
||||||
|
tombstones: x.tombstones,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const purchase = await tx.purchases.get(proposalId);
|
||||||
if (purchase) {
|
if (purchase) {
|
||||||
// This should just influence the history view,
|
// This should just influence the history view,
|
||||||
// but won't delete any actual refund information.
|
// but won't delete any actual refund information.
|
||||||
await tx.put(Stores.tombstones, {
|
await tx.tombstones.put({
|
||||||
id: makeEventId(
|
id: makeEventId(
|
||||||
TombstoneTag.DeleteRefund,
|
TombstoneTag.DeleteRefund,
|
||||||
proposalId,
|
proposalId,
|
||||||
@ -517,8 +536,7 @@ export async function deleteTransaction(
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
throw Error(`can't delete a '${type}' transaction`);
|
throw Error(`can't delete a '${type}' transaction`);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,6 @@ import {
|
|||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
Stores,
|
|
||||||
DenominationStatus,
|
DenominationStatus,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
@ -314,13 +313,17 @@ export async function getCandidateWithdrawalDenoms(
|
|||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
): Promise<DenominationRecord[]> {
|
): Promise<DenominationRecord[]> {
|
||||||
return await ws.db
|
return await ws.db
|
||||||
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
|
.mktx((x) => ({ denominations: x.denominations }))
|
||||||
.filter((d) => {
|
.runReadOnly(async (tx) => {
|
||||||
return (
|
return tx.denominations.indexes.byExchangeBaseUrl
|
||||||
(d.status === DenominationStatus.Unverified ||
|
.iter(exchangeBaseUrl)
|
||||||
d.status === DenominationStatus.VerifiedGood) &&
|
.filter((d) => {
|
||||||
!d.isRevoked
|
return (
|
||||||
);
|
(d.status === DenominationStatus.Unverified ||
|
||||||
|
d.status === DenominationStatus.VerifiedGood) &&
|
||||||
|
!d.isRevoked
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,17 +339,24 @@ async function processPlanchetGenerate(
|
|||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
coinIdx: number,
|
coinIdx: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const withdrawalGroup = await ws.db.get(
|
const withdrawalGroup = await ws.db
|
||||||
Stores.withdrawalGroups,
|
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
|
||||||
withdrawalGroupId,
|
.runReadOnly(async (tx) => {
|
||||||
);
|
return await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
|
});
|
||||||
if (!withdrawalGroup) {
|
if (!withdrawalGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [
|
let planchet = await ws.db
|
||||||
withdrawalGroupId,
|
.mktx((x) => ({
|
||||||
coinIdx,
|
planchets: x.planchets,
|
||||||
]);
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.planchets.indexes.byGroupAndIndex.get([
|
||||||
|
withdrawalGroupId,
|
||||||
|
coinIdx,
|
||||||
|
]);
|
||||||
|
});
|
||||||
if (!planchet) {
|
if (!planchet) {
|
||||||
let ci = 0;
|
let ci = 0;
|
||||||
let denomPubHash: string | undefined;
|
let denomPubHash: string | undefined;
|
||||||
@ -365,20 +375,26 @@ async function processPlanchetGenerate(
|
|||||||
if (!denomPubHash) {
|
if (!denomPubHash) {
|
||||||
throw Error("invariant violated");
|
throw Error("invariant violated");
|
||||||
}
|
}
|
||||||
const denom = await ws.db.get(Stores.denominations, [
|
|
||||||
withdrawalGroup.exchangeBaseUrl,
|
const { denom, reserve } = await ws.db
|
||||||
denomPubHash,
|
.mktx((x) => ({
|
||||||
]);
|
reserves: x.reserves,
|
||||||
if (!denom) {
|
denominations: x.denominations,
|
||||||
throw Error("invariant violated");
|
}))
|
||||||
}
|
.runReadOnly(async (tx) => {
|
||||||
const reserve = await ws.db.get(
|
const denom = await tx.denominations.get([
|
||||||
Stores.reserves,
|
withdrawalGroup.exchangeBaseUrl,
|
||||||
withdrawalGroup.reservePub,
|
denomPubHash!,
|
||||||
);
|
]);
|
||||||
if (!reserve) {
|
if (!denom) {
|
||||||
throw Error("invariant violated");
|
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({
|
const r = await ws.cryptoApi.createPlanchet({
|
||||||
denomPub: denom.denomPub,
|
denomPub: denom.denomPub,
|
||||||
feeWithdraw: denom.feeWithdraw,
|
feeWithdraw: denom.feeWithdraw,
|
||||||
@ -405,18 +421,20 @@ async function processPlanchetGenerate(
|
|||||||
withdrawalGroupId: withdrawalGroupId,
|
withdrawalGroupId: withdrawalGroupId,
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => {
|
await ws.db
|
||||||
const p = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [
|
.mktx((x) => ({ planchets: x.planchets }))
|
||||||
withdrawalGroupId,
|
.runReadWrite(async (tx) => {
|
||||||
coinIdx,
|
const p = await tx.planchets.indexes.byGroupAndIndex.get([
|
||||||
]);
|
withdrawalGroupId,
|
||||||
if (p) {
|
coinIdx,
|
||||||
planchet = p;
|
]);
|
||||||
return;
|
if (p) {
|
||||||
}
|
planchet = p;
|
||||||
await tx.put(Stores.planchets, newPlanchet);
|
return;
|
||||||
planchet = newPlanchet;
|
}
|
||||||
});
|
await tx.planchets.put(newPlanchet);
|
||||||
|
planchet = newPlanchet;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,59 +448,70 @@ async function processPlanchetExchangeRequest(
|
|||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
coinIdx: number,
|
coinIdx: number,
|
||||||
): Promise<WithdrawResponse | undefined> {
|
): Promise<WithdrawResponse | undefined> {
|
||||||
const withdrawalGroup = await ws.db.get(
|
const d = await ws.db
|
||||||
Stores.withdrawalGroups,
|
.mktx((x) => ({
|
||||||
withdrawalGroupId,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
);
|
planchets: x.planchets,
|
||||||
if (!withdrawalGroup) {
|
exchanges: x.exchanges,
|
||||||
return;
|
denominations: x.denominations,
|
||||||
}
|
}))
|
||||||
let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [
|
.runReadOnly(async (tx) => {
|
||||||
withdrawalGroupId,
|
const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
coinIdx,
|
if (!withdrawalGroup) {
|
||||||
]);
|
return;
|
||||||
if (!planchet) {
|
}
|
||||||
return;
|
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
|
||||||
}
|
withdrawalGroupId,
|
||||||
if (planchet.withdrawalDone) {
|
coinIdx,
|
||||||
logger.warn("processPlanchet: planchet already withdrawn");
|
]);
|
||||||
return;
|
if (!planchet) {
|
||||||
}
|
return;
|
||||||
const exchange = await ws.db.get(
|
}
|
||||||
Stores.exchanges,
|
if (planchet.withdrawalDone) {
|
||||||
withdrawalGroup.exchangeBaseUrl,
|
logger.warn("processPlanchet: planchet already withdrawn");
|
||||||
);
|
return;
|
||||||
if (!exchange) {
|
}
|
||||||
logger.error("db inconsistent: exchange for planchet not found");
|
const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
|
||||||
return;
|
if (!exchange) {
|
||||||
}
|
logger.error("db inconsistent: exchange for planchet not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const denom = await ws.db.get(Stores.denominations, [
|
const denom = await tx.denominations.get([
|
||||||
withdrawalGroup.exchangeBaseUrl,
|
withdrawalGroup.exchangeBaseUrl,
|
||||||
planchet.denomPubHash,
|
planchet.denomPubHash,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!denom) {
|
if (!denom) {
|
||||||
console.error("db inconsistent: denom for planchet not found");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
const { reqUrl, reqBody } = d;
|
||||||
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;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await ws.http.postJson(reqUrl, wd);
|
const resp = await ws.http.postJson(reqUrl, reqBody);
|
||||||
const r = await readSuccessResponseJsonOrThrow(
|
const r = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
codecForWithdrawResponse(),
|
codecForWithdrawResponse(),
|
||||||
@ -495,17 +524,19 @@ async function processPlanchetExchangeRequest(
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
const errDetails = e.operationError;
|
const errDetails = e.operationError;
|
||||||
await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => {
|
await ws.db
|
||||||
let planchet = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [
|
.mktx((x) => ({ planchets: x.planchets }))
|
||||||
withdrawalGroupId,
|
.runReadWrite(async (tx) => {
|
||||||
coinIdx,
|
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
|
||||||
]);
|
withdrawalGroupId,
|
||||||
if (!planchet) {
|
coinIdx,
|
||||||
return;
|
]);
|
||||||
}
|
if (!planchet) {
|
||||||
planchet.lastError = errDetails;
|
return;
|
||||||
await tx.put(Stores.planchets, planchet);
|
}
|
||||||
});
|
planchet.lastError = errDetails;
|
||||||
|
await tx.planchets.put(planchet);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -516,25 +547,36 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
coinIdx: number,
|
coinIdx: number,
|
||||||
resp: WithdrawResponse,
|
resp: WithdrawResponse,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const withdrawalGroup = await ws.db.get(
|
const d = await ws.db
|
||||||
Stores.withdrawalGroups,
|
.mktx((x) => ({
|
||||||
withdrawalGroupId,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
);
|
planchets: x.planchets,
|
||||||
if (!withdrawalGroup) {
|
}))
|
||||||
return;
|
.runReadOnly(async (tx) => {
|
||||||
}
|
const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [
|
if (!withdrawalGroup) {
|
||||||
withdrawalGroupId,
|
return;
|
||||||
coinIdx,
|
}
|
||||||
]);
|
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
|
||||||
if (!planchet) {
|
withdrawalGroupId,
|
||||||
return;
|
coinIdx,
|
||||||
}
|
]);
|
||||||
if (planchet.withdrawalDone) {
|
if (!planchet) {
|
||||||
logger.warn("processPlanchet: planchet already withdrawn");
|
return;
|
||||||
|
}
|
||||||
|
if (planchet.withdrawalDone) {
|
||||||
|
logger.warn("processPlanchet: planchet already withdrawn");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return { planchet, exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!d) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { planchet, exchangeBaseUrl } = d;
|
||||||
|
|
||||||
const denomSig = await ws.cryptoApi.rsaUnblind(
|
const denomSig = await ws.cryptoApi.rsaUnblind(
|
||||||
resp.ev_sig,
|
resp.ev_sig,
|
||||||
planchet.blindingKey,
|
planchet.blindingKey,
|
||||||
@ -548,21 +590,23 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => {
|
await ws.db
|
||||||
let planchet = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [
|
.mktx((x) => ({ planchets: x.planchets }))
|
||||||
withdrawalGroupId,
|
.runReadWrite(async (tx) => {
|
||||||
coinIdx,
|
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
|
||||||
]);
|
withdrawalGroupId,
|
||||||
if (!planchet) {
|
coinIdx,
|
||||||
return;
|
]);
|
||||||
}
|
if (!planchet) {
|
||||||
planchet.lastError = makeErrorDetails(
|
return;
|
||||||
TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID,
|
}
|
||||||
"invalid signature from the exchange after unblinding",
|
planchet.lastError = makeErrorDetails(
|
||||||
{},
|
TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID,
|
||||||
);
|
"invalid signature from the exchange after unblinding",
|
||||||
await tx.put(Stores.planchets, planchet);
|
{},
|
||||||
});
|
);
|
||||||
|
await tx.planchets.put(planchet);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -575,7 +619,7 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
denomPubHash: planchet.denomPubHash,
|
denomPubHash: planchet.denomPubHash,
|
||||||
denomSig,
|
denomSig,
|
||||||
coinEvHash: planchet.coinEvHash,
|
coinEvHash: planchet.coinEvHash,
|
||||||
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
|
exchangeBaseUrl: exchangeBaseUrl,
|
||||||
status: CoinStatus.Fresh,
|
status: CoinStatus.Fresh,
|
||||||
coinSource: {
|
coinSource: {
|
||||||
type: CoinSourceType.Withdraw,
|
type: CoinSourceType.Withdraw,
|
||||||
@ -588,23 +632,27 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
|
|
||||||
const planchetCoinPub = planchet.coinPub;
|
const planchetCoinPub = planchet.coinPub;
|
||||||
|
|
||||||
const firstSuccess = await ws.db.runWithWriteTransaction(
|
const firstSuccess = await ws.db
|
||||||
[Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
coins: x.coins,
|
||||||
const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
reserves: x.reserves,
|
||||||
|
planchets: x.planchets,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const ws = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
if (!ws) {
|
if (!ws) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const p = await tx.get(Stores.planchets, planchetCoinPub);
|
const p = await tx.planchets.get(planchetCoinPub);
|
||||||
if (!p || p.withdrawalDone) {
|
if (!p || p.withdrawalDone) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
p.withdrawalDone = true;
|
p.withdrawalDone = true;
|
||||||
await tx.put(Stores.planchets, p);
|
await tx.planchets.put(p);
|
||||||
await tx.add(Stores.coins, coin);
|
await tx.coins.add(coin);
|
||||||
return true;
|
return true;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (firstSuccess) {
|
if (firstSuccess) {
|
||||||
ws.notify({
|
ws.notify({
|
||||||
@ -636,12 +684,14 @@ export async function updateWithdrawalDenoms(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const exchangeDetails = await ws.db.runWithReadTransaction(
|
const exchangeDetails = await ws.db
|
||||||
[Stores.exchanges, Stores.exchangeDetails],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
exchanges: x.exchanges,
|
||||||
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
return getExchangeDetails(tx, exchangeBaseUrl);
|
return getExchangeDetails(tx, exchangeBaseUrl);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
if (!exchangeDetails) {
|
if (!exchangeDetails) {
|
||||||
logger.error("exchange details not available");
|
logger.error("exchange details not available");
|
||||||
throw Error(`exchange ${exchangeBaseUrl} details not available`);
|
throw Error(`exchange ${exchangeBaseUrl} details not available`);
|
||||||
@ -663,7 +713,11 @@ export async function updateWithdrawalDenoms(
|
|||||||
} else {
|
} else {
|
||||||
denom.status = DenominationStatus.VerifiedGood;
|
denom.status = DenominationStatus.VerifiedGood;
|
||||||
}
|
}
|
||||||
await ws.db.put(Stores.denominations, denom);
|
await ws.db
|
||||||
|
.mktx((x) => ({ denominations: x.denominations }))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
await tx.denominations.put(denom);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// FIXME: This debug info should either be made conditional on some flag
|
// FIXME: This debug info should either be made conditional on some flag
|
||||||
@ -698,16 +752,18 @@ async function incrementWithdrawalRetry(
|
|||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
err: TalerErrorDetails | undefined,
|
err: TalerErrorDetails | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction([Stores.withdrawalGroups], async (tx) => {
|
await ws.db
|
||||||
const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
|
||||||
if (!wsr) {
|
.runReadWrite(async (tx) => {
|
||||||
return;
|
const wsr = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
}
|
if (!wsr) {
|
||||||
wsr.retryInfo.retryCounter++;
|
return;
|
||||||
updateRetryInfoTimeout(wsr.retryInfo);
|
}
|
||||||
wsr.lastError = err;
|
wsr.retryInfo.retryCounter++;
|
||||||
await tx.put(Stores.withdrawalGroups, wsr);
|
updateRetryInfoTimeout(wsr.retryInfo);
|
||||||
});
|
wsr.lastError = err;
|
||||||
|
await tx.withdrawalGroups.put(wsr);
|
||||||
|
});
|
||||||
if (err) {
|
if (err) {
|
||||||
ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
|
ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
|
||||||
}
|
}
|
||||||
@ -730,12 +786,15 @@ async function resetWithdrawalGroupRetry(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.mutate(Stores.withdrawalGroups, withdrawalGroupId, (x) => {
|
await ws.db
|
||||||
if (x.retryInfo.active) {
|
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
|
||||||
x.retryInfo = initRetryInfo();
|
.runReadWrite(async (tx) => {
|
||||||
}
|
const x = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
return x;
|
if (x && x.retryInfo.active) {
|
||||||
});
|
x.retryInfo = initRetryInfo();
|
||||||
|
await tx.withdrawalGroups.put(x);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processWithdrawGroupImpl(
|
async function processWithdrawGroupImpl(
|
||||||
@ -747,10 +806,11 @@ async function processWithdrawGroupImpl(
|
|||||||
if (forceNow) {
|
if (forceNow) {
|
||||||
await resetWithdrawalGroupRetry(ws, withdrawalGroupId);
|
await resetWithdrawalGroupRetry(ws, withdrawalGroupId);
|
||||||
}
|
}
|
||||||
const withdrawalGroup = await ws.db.get(
|
const withdrawalGroup = await ws.db
|
||||||
Stores.withdrawalGroups,
|
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
|
||||||
withdrawalGroupId,
|
.runReadOnly(async (tx) => {
|
||||||
);
|
return tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
|
});
|
||||||
if (!withdrawalGroup) {
|
if (!withdrawalGroup) {
|
||||||
logger.trace("withdraw session doesn't exist");
|
logger.trace("withdraw session doesn't exist");
|
||||||
return;
|
return;
|
||||||
@ -793,16 +853,21 @@ async function processWithdrawGroupImpl(
|
|||||||
let finishedForFirstTime = false;
|
let finishedForFirstTime = false;
|
||||||
let errorsPerCoin: Record<number, TalerErrorDetails> = {};
|
let errorsPerCoin: Record<number, TalerErrorDetails> = {};
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db
|
||||||
[Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
coins: x.coins,
|
||||||
const wg = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
reserves: x.reserves,
|
||||||
|
planchets: x.planchets,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
if (!wg) {
|
if (!wg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx
|
await tx.planchets.indexes.byGroup
|
||||||
.iterIndexed(Stores.planchets.byGroup, withdrawalGroupId)
|
.iter(withdrawalGroupId)
|
||||||
.forEach((x) => {
|
.forEach((x) => {
|
||||||
if (x.withdrawalDone) {
|
if (x.withdrawalDone) {
|
||||||
numFinished++;
|
numFinished++;
|
||||||
@ -819,9 +884,8 @@ async function processWithdrawGroupImpl(
|
|||||||
wg.retryInfo = initRetryInfo(false);
|
wg.retryInfo = initRetryInfo(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.put(Stores.withdrawalGroups, wg);
|
await tx.withdrawalGroups.put(wg);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (numFinished != numTotalCoins) {
|
if (numFinished != numTotalCoins) {
|
||||||
throw OperationFailedError.fromCode(
|
throw OperationFailedError.fromCode(
|
||||||
@ -871,8 +935,12 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const possibleDenoms = await ws.db
|
const possibleDenoms = await ws.db
|
||||||
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl)
|
.mktx((x) => ({ denominations: x.denominations }))
|
||||||
.filter((d) => d.isOffered);
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.denominations.indexes.byExchangeBaseUrl
|
||||||
|
.iter()
|
||||||
|
.filter((d) => d.isOffered);
|
||||||
|
});
|
||||||
|
|
||||||
let versionMatch;
|
let versionMatch;
|
||||||
if (exchangeDetails.protocolVersion) {
|
if (exchangeDetails.protocolVersion) {
|
||||||
@ -953,23 +1021,24 @@ export async function getWithdrawalDetailsForUri(
|
|||||||
|
|
||||||
const exchanges: ExchangeListItem[] = [];
|
const exchanges: ExchangeListItem[] = [];
|
||||||
|
|
||||||
const exchangeRecords = await ws.db.iter(Stores.exchanges).toArray();
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
for (const r of exchangeRecords) {
|
exchanges: x.exchanges,
|
||||||
const details = await ws.db.runWithReadTransaction(
|
exchangeDetails: x.exchangeDetails,
|
||||||
[Stores.exchanges, Stores.exchangeDetails],
|
}))
|
||||||
async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
return getExchangeDetails(tx, r.baseUrl);
|
const exchangeRecords = await tx.exchanges.iter().toArray();
|
||||||
},
|
for (const r of exchangeRecords) {
|
||||||
);
|
const details = await getExchangeDetails(tx, r.baseUrl);
|
||||||
if (details) {
|
if (details) {
|
||||||
exchanges.push({
|
exchanges.push({
|
||||||
exchangeBaseUrl: details.exchangeBaseUrl,
|
exchangeBaseUrl: details.exchangeBaseUrl,
|
||||||
currency: details.currency,
|
currency: details.currency,
|
||||||
paytoUris: details.wireInfo.accounts.map((x) => x.payto_uri),
|
paytoUris: details.wireInfo.accounts.map((x) => x.payto_uri),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
amount: Amounts.stringify(info.amount),
|
amount: Amounts.stringify(info.amount),
|
||||||
|
@ -33,6 +33,7 @@ import {
|
|||||||
IDBVersionChangeEvent,
|
IDBVersionChangeEvent,
|
||||||
Event,
|
Event,
|
||||||
IDBCursor,
|
IDBCursor,
|
||||||
|
IDBKeyPath,
|
||||||
} from "@gnu-taler/idb-bridge";
|
} from "@gnu-taler/idb-bridge";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
@ -43,25 +44,6 @@ const logger = new Logger("query.ts");
|
|||||||
*/
|
*/
|
||||||
export const TransactionAbort = Symbol("transaction_abort");
|
export const TransactionAbort = Symbol("transaction_abort");
|
||||||
|
|
||||||
export interface StoreParams<T> {
|
|
||||||
validator?: (v: T) => T;
|
|
||||||
autoIncrement?: boolean;
|
|
||||||
keyPath?: string | string[] | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database version that this store was added in, or
|
|
||||||
* undefined if added in the first version.
|
|
||||||
*/
|
|
||||||
versionAdded?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Definition of an object store.
|
|
||||||
*/
|
|
||||||
export class Store<N extends string, T> {
|
|
||||||
constructor(public name: N, public storeParams?: StoreParams<T>) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for an index.
|
* Options for an index.
|
||||||
*/
|
*/
|
||||||
@ -111,37 +93,6 @@ function transactionToPromise(tx: IDBTransaction): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyMutation<T>(
|
|
||||||
req: IDBRequest,
|
|
||||||
f: (x: T) => T | undefined,
|
|
||||||
): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
req.onsuccess = () => {
|
|
||||||
const cursor = req.result;
|
|
||||||
if (cursor) {
|
|
||||||
const val = cursor.value;
|
|
||||||
const modVal = f(val);
|
|
||||||
if (modVal !== undefined && modVal !== null) {
|
|
||||||
const req2: IDBRequest = cursor.update(modVal);
|
|
||||||
req2.onerror = () => {
|
|
||||||
reject(req2.error);
|
|
||||||
};
|
|
||||||
req2.onsuccess = () => {
|
|
||||||
cursor.continue();
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
cursor.continue();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
req.onerror = () => {
|
|
||||||
reject(req.error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
|
type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
|
||||||
|
|
||||||
interface CursorEmptyResult<T> {
|
interface CursorEmptyResult<T> {
|
||||||
@ -269,210 +220,6 @@ class ResultStream<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnyStoreMap = { [s: string]: Store<any, any> };
|
|
||||||
|
|
||||||
type StoreName<S> = S extends Store<infer N, any> ? N : never;
|
|
||||||
type StoreContent<S> = S extends Store<any, infer R> ? R : never;
|
|
||||||
type IndexRecord<Ind> = Ind extends Index<any, any, any, infer R> ? R : never;
|
|
||||||
|
|
||||||
type InferStore<S> = S extends Store<infer N, infer R> ? Store<N, R> : never;
|
|
||||||
type InferIndex<Ind> = Ind extends Index<
|
|
||||||
infer StN,
|
|
||||||
infer IndN,
|
|
||||||
infer KT,
|
|
||||||
infer RT
|
|
||||||
>
|
|
||||||
? Index<StN, IndN, KT, RT>
|
|
||||||
: never;
|
|
||||||
|
|
||||||
export class TransactionHandle<StoreTypes extends Store<string, any>> {
|
|
||||||
constructor(private tx: IDBTransaction) {}
|
|
||||||
|
|
||||||
put<S extends StoreTypes>(
|
|
||||||
store: S,
|
|
||||||
value: StoreContent<S>,
|
|
||||||
key?: any,
|
|
||||||
): Promise<any> {
|
|
||||||
const req = this.tx.objectStore(store.name).put(value, key);
|
|
||||||
return requestToPromise(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
add<S extends StoreTypes>(
|
|
||||||
store: S,
|
|
||||||
value: StoreContent<S>,
|
|
||||||
key?: any,
|
|
||||||
): Promise<any> {
|
|
||||||
const req = this.tx.objectStore(store.name).add(value, key);
|
|
||||||
return requestToPromise(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
get<S extends StoreTypes>(
|
|
||||||
store: S,
|
|
||||||
key: any,
|
|
||||||
): Promise<StoreContent<S> | undefined> {
|
|
||||||
const req = this.tx.objectStore(store.name).get(key);
|
|
||||||
return requestToPromise(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
getIndexed<
|
|
||||||
St extends StoreTypes,
|
|
||||||
Ind extends Index<StoreName<St>, string, any, any>
|
|
||||||
>(index: InferIndex<Ind>, key: any): Promise<IndexRecord<Ind> | undefined> {
|
|
||||||
const req = this.tx
|
|
||||||
.objectStore(index.storeName)
|
|
||||||
.index(index.indexName)
|
|
||||||
.get(key);
|
|
||||||
return requestToPromise(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
iter<St extends InferStore<StoreTypes>>(
|
|
||||||
store: St,
|
|
||||||
key?: any,
|
|
||||||
): ResultStream<StoreContent<St>> {
|
|
||||||
const req = this.tx.objectStore(store.name).openCursor(key);
|
|
||||||
return new ResultStream<StoreContent<St>>(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
iterIndexed<
|
|
||||||
St extends InferStore<StoreTypes>,
|
|
||||||
Ind extends InferIndex<Index<StoreName<St>, string, any, any>>
|
|
||||||
>(index: Ind, key?: any): ResultStream<IndexRecord<Ind>> {
|
|
||||||
const req = this.tx
|
|
||||||
.objectStore(index.storeName)
|
|
||||||
.index(index.indexName)
|
|
||||||
.openCursor(key);
|
|
||||||
return new ResultStream<IndexRecord<Ind>>(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete<St extends StoreTypes>(
|
|
||||||
store: InferStore<St>,
|
|
||||||
key: any,
|
|
||||||
): Promise<void> {
|
|
||||||
const req = this.tx.objectStore(store.name).delete(key);
|
|
||||||
return requestToPromise(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutate<St extends StoreTypes>(
|
|
||||||
store: InferStore<St>,
|
|
||||||
key: any,
|
|
||||||
f: (x: StoreContent<St>) => StoreContent<St> | undefined,
|
|
||||||
): Promise<void> {
|
|
||||||
const req = this.tx.objectStore(store.name).openCursor(key);
|
|
||||||
return applyMutation(req, f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runWithTransaction<T, StoreTypes extends Store<string, {}>>(
|
|
||||||
db: IDBDatabase,
|
|
||||||
stores: StoreTypes[],
|
|
||||||
f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
|
|
||||||
mode: "readonly" | "readwrite",
|
|
||||||
): Promise<T> {
|
|
||||||
const stack = Error("Failed transaction was started here.");
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const storeName = stores.map((x) => x.name);
|
|
||||||
|
|
||||||
let txOrUndef: IDBTransaction | undefined = undefined
|
|
||||||
try {
|
|
||||||
txOrUndef = db.transaction(storeName, mode);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("error opening transaction");
|
|
||||||
logger.error(`${e}`);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const tx = txOrUndef;
|
|
||||||
|
|
||||||
let funResult: any = undefined;
|
|
||||||
let gotFunResult = false;
|
|
||||||
tx.oncomplete = () => {
|
|
||||||
// This is a fatal error: The transaction completed *before*
|
|
||||||
// the transaction function returned. Likely, the transaction
|
|
||||||
// function waited on a promise that is *not* resolved in the
|
|
||||||
// microtask queue, thus triggering the auto-commit behavior.
|
|
||||||
// Unfortunately, the auto-commit behavior of IDB can't be switched
|
|
||||||
// of. There are some proposals to add this functionality in the future.
|
|
||||||
if (!gotFunResult) {
|
|
||||||
const msg =
|
|
||||||
"BUG: transaction closed before transaction function returned";
|
|
||||||
console.error(msg);
|
|
||||||
reject(Error(msg));
|
|
||||||
}
|
|
||||||
resolve(funResult);
|
|
||||||
};
|
|
||||||
tx.onerror = () => {
|
|
||||||
logger.error("error in transaction");
|
|
||||||
logger.error(`${stack}`);
|
|
||||||
};
|
|
||||||
tx.onabort = () => {
|
|
||||||
if (tx.error) {
|
|
||||||
logger.error("Transaction aborted with error:", tx.error);
|
|
||||||
} else {
|
|
||||||
logger.error("Transaction aborted (no error)");
|
|
||||||
}
|
|
||||||
reject(TransactionAbort);
|
|
||||||
};
|
|
||||||
const th = new TransactionHandle(tx);
|
|
||||||
const resP = Promise.resolve().then(() => f(th));
|
|
||||||
resP
|
|
||||||
.then((result) => {
|
|
||||||
gotFunResult = true;
|
|
||||||
funResult = result;
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if (e == TransactionAbort) {
|
|
||||||
logger.trace("aborting transaction");
|
|
||||||
} else {
|
|
||||||
console.error("Transaction failed:", e);
|
|
||||||
console.error(stack);
|
|
||||||
tx.abort();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("fatal: aborting transaction failed", e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Definition of an index.
|
|
||||||
*/
|
|
||||||
export class Index<
|
|
||||||
StoreName extends string,
|
|
||||||
IndexName extends string,
|
|
||||||
S extends IDBValidKey,
|
|
||||||
T
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* Name of the store that this index is associated with.
|
|
||||||
*/
|
|
||||||
storeName: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options to use for the index.
|
|
||||||
*/
|
|
||||||
options: IndexOptions;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
s: Store<StoreName, T>,
|
|
||||||
public indexName: IndexName,
|
|
||||||
public keyPath: string | string[],
|
|
||||||
options?: IndexOptions,
|
|
||||||
) {
|
|
||||||
const defaultOptions = {
|
|
||||||
multiEntry: false,
|
|
||||||
};
|
|
||||||
this.options = { ...defaultOptions, ...(options || {}) };
|
|
||||||
this.storeName = s.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We want to have the key type parameter in use somewhere,
|
|
||||||
* because otherwise the compiler complains. In iterIndex the
|
|
||||||
* key type is pretty useful.
|
|
||||||
*/
|
|
||||||
protected _dummyKey: S | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a promise that resolves to the opened IndexedDB database.
|
* Return a promise that resolves to the opened IndexedDB database.
|
||||||
*/
|
*/
|
||||||
@ -519,152 +266,334 @@ export function openDatabase(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Database<StoreMap extends AnyStoreMap> {
|
export interface IndexDescriptor {
|
||||||
constructor(private db: IDBDatabase, stores: StoreMap) {}
|
name: string;
|
||||||
|
keyPath: IDBKeyPath | IDBKeyPath[];
|
||||||
|
multiEntry?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
static deleteDatabase(idbFactory: IDBFactory, dbName: string): Promise<void> {
|
export interface StoreDescriptor<RecordType> {
|
||||||
const req = idbFactory.deleteDatabase(dbName)
|
_dummy: undefined & RecordType;
|
||||||
return requestToPromise(req)
|
name: string;
|
||||||
}
|
keyPath?: IDBKeyPath | IDBKeyPath[];
|
||||||
|
autoIncrement?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
async exportDatabase(): Promise<any> {
|
export interface StoreOptions {
|
||||||
const db = this.db;
|
keyPath?: IDBKeyPath | IDBKeyPath[];
|
||||||
const dump = {
|
autoIncrement?: boolean;
|
||||||
name: db.name,
|
}
|
||||||
stores: {} as { [s: string]: any },
|
|
||||||
version: db.version,
|
export function describeContents<RecordType = never>(
|
||||||
|
name: string,
|
||||||
|
options: StoreOptions,
|
||||||
|
): StoreDescriptor<RecordType> {
|
||||||
|
return { name, keyPath: options.keyPath, _dummy: undefined as any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function describeIndex(
|
||||||
|
name: string,
|
||||||
|
keyPath: IDBKeyPath | IDBKeyPath[],
|
||||||
|
options: IndexOptions = {},
|
||||||
|
): IndexDescriptor {
|
||||||
|
return {
|
||||||
|
keyPath,
|
||||||
|
name,
|
||||||
|
multiEntry: options.multiEntry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndexReadOnlyAccessor<RecordType> {
|
||||||
|
iter(query?: IDBValidKey): ResultStream<RecordType>;
|
||||||
|
get(query: IDBValidKey): Promise<RecordType | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetIndexReadOnlyAccess<RecordType, IndexMap> = {
|
||||||
|
[P in keyof IndexMap]: IndexReadOnlyAccessor<RecordType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IndexReadWriteAccessor<RecordType> {
|
||||||
|
iter(query: IDBValidKey): ResultStream<RecordType>;
|
||||||
|
get(query: IDBValidKey): Promise<RecordType | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetIndexReadWriteAccess<RecordType, IndexMap> = {
|
||||||
|
[P in keyof IndexMap]: IndexReadWriteAccessor<RecordType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface StoreReadOnlyAccessor<RecordType, IndexMap> {
|
||||||
|
get(key: IDBValidKey): Promise<RecordType | undefined>;
|
||||||
|
iter(query?: IDBValidKey): ResultStream<RecordType>;
|
||||||
|
indexes: GetIndexReadOnlyAccess<RecordType, IndexMap>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreReadWriteAccessor<RecordType, IndexMap> {
|
||||||
|
get(key: IDBValidKey): Promise<RecordType | undefined>;
|
||||||
|
iter(query?: IDBValidKey): ResultStream<RecordType>;
|
||||||
|
put(r: RecordType): Promise<void>;
|
||||||
|
add(r: RecordType): Promise<void>;
|
||||||
|
delete(key: IDBValidKey): Promise<void>;
|
||||||
|
indexes: GetIndexReadWriteAccess<RecordType, IndexMap>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreWithIndexes<
|
||||||
|
SD extends StoreDescriptor<unknown>,
|
||||||
|
IndexMap
|
||||||
|
> {
|
||||||
|
store: SD;
|
||||||
|
indexMap: IndexMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type marker symbol, to check that the descriptor
|
||||||
|
* has been created through the right function.
|
||||||
|
*/
|
||||||
|
mark: Symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetRecordType<T> = T extends StoreDescriptor<infer X> ? X : unknown;
|
||||||
|
|
||||||
|
const storeWithIndexesSymbol = Symbol("StoreWithIndexesMark");
|
||||||
|
|
||||||
|
export function describeStore<SD extends StoreDescriptor<unknown>, IndexMap>(
|
||||||
|
s: SD,
|
||||||
|
m: IndexMap,
|
||||||
|
): StoreWithIndexes<SD, IndexMap> {
|
||||||
|
return {
|
||||||
|
store: s,
|
||||||
|
indexMap: m,
|
||||||
|
mark: storeWithIndexesSymbol,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetReadOnlyAccess<BoundStores> = {
|
||||||
|
[P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes<
|
||||||
|
infer SD,
|
||||||
|
infer IM
|
||||||
|
>
|
||||||
|
? StoreReadOnlyAccessor<GetRecordType<SD>, IM>
|
||||||
|
: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetReadWriteAccess<BoundStores> = {
|
||||||
|
[P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes<
|
||||||
|
infer SD,
|
||||||
|
infer IM
|
||||||
|
>
|
||||||
|
? StoreReadWriteAccessor<GetRecordType<SD>, IM>
|
||||||
|
: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReadOnlyTransactionFunction<BoundStores, T> = (
|
||||||
|
t: GetReadOnlyAccess<BoundStores>,
|
||||||
|
) => Promise<T>;
|
||||||
|
|
||||||
|
type ReadWriteTransactionFunction<BoundStores, T> = (
|
||||||
|
t: GetReadWriteAccess<BoundStores>,
|
||||||
|
) => Promise<T>;
|
||||||
|
|
||||||
|
export interface TransactionContext<BoundStores> {
|
||||||
|
runReadWrite<T>(f: ReadWriteTransactionFunction<BoundStores, T>): Promise<T>;
|
||||||
|
runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckDescriptor<T> = T extends StoreWithIndexes<infer SD, infer IM>
|
||||||
|
? StoreWithIndexes<SD, IM>
|
||||||
|
: unknown;
|
||||||
|
|
||||||
|
type GetPickerType<F, SM> = F extends (x: SM) => infer Out
|
||||||
|
? { [P in keyof Out]: CheckDescriptor<Out[P]> }
|
||||||
|
: unknown;
|
||||||
|
|
||||||
|
function runTx<Arg, Res>(
|
||||||
|
tx: IDBTransaction,
|
||||||
|
arg: Arg,
|
||||||
|
f: (t: Arg) => Promise<Res>,
|
||||||
|
): Promise<Res> {
|
||||||
|
const stack = Error("Failed transaction was started here.");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
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 = () => {
|
||||||
return new Promise((resolve, reject) => {
|
logger.error("error in transaction");
|
||||||
const tx = db.transaction(Array.from(db.objectStoreNames));
|
logger.error(`${stack}`);
|
||||||
tx.addEventListener("complete", () => {
|
};
|
||||||
resolve(dump);
|
tx.onabort = () => {
|
||||||
});
|
if (tx.error) {
|
||||||
// tslint:disable-next-line:prefer-for-of
|
logger.error("Transaction aborted with error:", tx.error);
|
||||||
for (let i = 0; i < db.objectStoreNames.length; i++) {
|
} else {
|
||||||
const name = db.objectStoreNames[i];
|
logger.error("Transaction aborted (no error)");
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
reject(TransactionAbort);
|
||||||
}
|
};
|
||||||
|
const resP = Promise.resolve().then(() => f(arg));
|
||||||
importDatabase(dump: any): Promise<void> {
|
resP
|
||||||
const db = this.db;
|
.then((result) => {
|
||||||
logger.info("importing db", dump);
|
gotFunResult = true;
|
||||||
return new Promise<void>((resolve, reject) => {
|
funResult = result;
|
||||||
const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
|
})
|
||||||
if (dump.stores) {
|
.catch((e) => {
|
||||||
for (const storeName in dump.stores) {
|
if (e == TransactionAbort) {
|
||||||
const objects = [];
|
logger.trace("aborting transaction");
|
||||||
const dumpStore = dump.stores[storeName];
|
} else {
|
||||||
for (const key in dumpStore) {
|
console.error("Transaction failed:", e);
|
||||||
objects.push(dumpStore[key]);
|
console.error(stack);
|
||||||
}
|
tx.abort();
|
||||||
logger.info(`importing ${objects.length} records into ${storeName}`);
|
|
||||||
const store = tx.objectStore(storeName);
|
|
||||||
for (const obj of objects) {
|
|
||||||
store.put(obj);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
tx.addEventListener("complete", () => {
|
.catch((e) => {
|
||||||
resolve();
|
console.error("fatal: aborting transaction failed", e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get<N extends keyof StoreMap, S extends StoreMap[N]>(
|
function makeReadContext(
|
||||||
store: S,
|
tx: IDBTransaction,
|
||||||
key: IDBValidKey,
|
storePick: { [n: string]: StoreWithIndexes<any, any> },
|
||||||
): Promise<StoreContent<S> | undefined> {
|
): any {
|
||||||
const tx = this.db.transaction([store.name], "readonly");
|
const ctx: { [s: string]: StoreReadOnlyAccessor<any, any> } = {};
|
||||||
const req = tx.objectStore(store.name).get(key);
|
for (const storeAlias in storePick) {
|
||||||
const v = await requestToPromise(req);
|
const indexes: { [s: string]: IndexReadOnlyAccessor<any> } = {};
|
||||||
await transactionToPromise(tx);
|
const swi = storePick[storeAlias];
|
||||||
return v;
|
const storeName = swi.store.name;
|
||||||
|
for (const indexName in storePick[storeAlias].indexMap) {
|
||||||
|
indexes[indexName] = {
|
||||||
|
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>>(
|
function makeWriteContext(
|
||||||
index: Ind,
|
tx: IDBTransaction,
|
||||||
key: IDBValidKey,
|
storePick: { [n: string]: StoreWithIndexes<any, any> },
|
||||||
): Promise<IndexRecord<Ind> | undefined> {
|
): any {
|
||||||
const tx = this.db.transaction([index.storeName], "readonly");
|
const ctx: { [s: string]: StoreReadWriteAccessor<any, any> } = {};
|
||||||
const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
|
for (const storeAlias in storePick) {
|
||||||
const v = await requestToPromise(req);
|
const indexes: { [s: string]: IndexReadWriteAccessor<any> } = {};
|
||||||
await transactionToPromise(tx);
|
const swi = storePick[storeAlias];
|
||||||
return v;
|
const storeName = swi.store.name;
|
||||||
}
|
for (const indexName in storePick[storeAlias].indexMap) {
|
||||||
|
indexes[indexName] = {
|
||||||
async put<St extends Store<string, any>>(
|
get(key) {
|
||||||
store: St,
|
const req = tx.objectStore(storeName).index(indexName).get(key);
|
||||||
value: StoreContent<St>,
|
return requestToPromise(req);
|
||||||
key?: IDBValidKey,
|
},
|
||||||
): Promise<any> {
|
iter(query) {
|
||||||
const tx = this.db.transaction([store.name], "readwrite");
|
const req = tx
|
||||||
const req = tx.objectStore(store.name).put(value, key);
|
.objectStore(storeName)
|
||||||
const v = await requestToPromise(req);
|
.index(indexName)
|
||||||
await transactionToPromise(tx);
|
.openCursor(query);
|
||||||
return v;
|
return new ResultStream<any>(req);
|
||||||
}
|
},
|
||||||
|
};
|
||||||
async mutate<N extends string, T>(
|
}
|
||||||
store: Store<N, T>,
|
ctx[storeAlias] = {
|
||||||
key: IDBValidKey,
|
indexes,
|
||||||
f: (x: T) => T | undefined,
|
get(key) {
|
||||||
): Promise<void> {
|
const req = tx.objectStore(storeName).get(key);
|
||||||
const tx = this.db.transaction([store.name], "readwrite");
|
return requestToPromise(req);
|
||||||
const req = tx.objectStore(store.name).openCursor(key);
|
},
|
||||||
await applyMutation(req, f);
|
iter(query) {
|
||||||
await transactionToPromise(tx);
|
const req = tx.objectStore(storeName).openCursor(query);
|
||||||
}
|
return new ResultStream<any>(req);
|
||||||
|
},
|
||||||
iter<N extends string, T>(store: Store<N, T>): ResultStream<T> {
|
add(r) {
|
||||||
const tx = this.db.transaction([store.name], "readonly");
|
const req = tx.objectStore(storeName).add(r);
|
||||||
const req = tx.objectStore(store.name).openCursor();
|
return requestToPromise(req);
|
||||||
return new ResultStream<T>(req);
|
},
|
||||||
}
|
put(r) {
|
||||||
|
const req = tx.objectStore(storeName).put(r);
|
||||||
iterIndex<Ind extends Index<string, string, any, any>>(
|
return requestToPromise(req);
|
||||||
index: InferIndex<Ind>,
|
},
|
||||||
query?: any,
|
delete(k) {
|
||||||
): ResultStream<IndexRecord<Ind>> {
|
const req = tx.objectStore(storeName).delete(k);
|
||||||
const tx = this.db.transaction([index.storeName], "readonly");
|
return requestToPromise(req);
|
||||||
const req = tx
|
},
|
||||||
.objectStore(index.storeName)
|
};
|
||||||
.index(index.indexName)
|
}
|
||||||
.openCursor(query);
|
}
|
||||||
return new ResultStream<IndexRecord<Ind>>(req);
|
|
||||||
}
|
/**
|
||||||
|
* Type-safe access to a database with a particular store map.
|
||||||
async runWithReadTransaction<
|
*
|
||||||
T,
|
* A store map is the metadata that describes the store.
|
||||||
N extends keyof StoreMap,
|
*/
|
||||||
StoreTypes extends StoreMap[N]
|
export class DbAccess<StoreMap> {
|
||||||
>(
|
constructor(private db: IDBDatabase, private stores: StoreMap) {}
|
||||||
stores: StoreTypes[],
|
|
||||||
f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
|
mktx<
|
||||||
): Promise<T> {
|
PickerType extends (x: StoreMap) => unknown,
|
||||||
return runWithTransaction<T, StoreTypes>(this.db, stores, f, "readonly");
|
BoundStores extends GetPickerType<PickerType, StoreMap>
|
||||||
}
|
>(f: PickerType): TransactionContext<BoundStores> {
|
||||||
|
const storePick = f(this.stores) as any;
|
||||||
async runWithWriteTransaction<
|
if (typeof storePick !== "object" || storePick === null) {
|
||||||
T,
|
throw Error();
|
||||||
N extends keyof StoreMap,
|
}
|
||||||
StoreTypes extends StoreMap[N]
|
const storeNames: string[] = [];
|
||||||
>(
|
for (const storeAlias of Object.keys(storePick)) {
|
||||||
stores: StoreTypes[],
|
const swi = (storePick as any)[storeAlias] as StoreWithIndexes<any, any>;
|
||||||
f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
|
if (swi.mark !== storeWithIndexesSymbol) {
|
||||||
): Promise<T> {
|
throw Error("invalid store descriptor returned from selector function");
|
||||||
return runWithTransaction<T, StoreTypes>(this.db, stores, f, "readwrite");
|
}
|
||||||
|
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";
|
} from "./operations/errors";
|
||||||
import {
|
import {
|
||||||
acceptExchangeTermsOfService,
|
acceptExchangeTermsOfService,
|
||||||
|
getExchangeDetails,
|
||||||
getExchangePaytoUri,
|
getExchangePaytoUri,
|
||||||
updateExchangeFromUrl,
|
updateExchangeFromUrl,
|
||||||
} from "./operations/exchanges";
|
} from "./operations/exchanges";
|
||||||
@ -111,7 +112,7 @@ import {
|
|||||||
RefundState,
|
RefundState,
|
||||||
ReserveRecord,
|
ReserveRecord,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
Stores,
|
WalletStoresV1,
|
||||||
} from "./db.js";
|
} from "./db.js";
|
||||||
import { NotificationType, WalletNotification } from "@gnu-taler/taler-util";
|
import { NotificationType, WalletNotification } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
@ -179,10 +180,10 @@ import { AsyncOpMemoSingle } from "./util/asyncMemo";
|
|||||||
import { HttpRequestLibrary } from "./util/http";
|
import { HttpRequestLibrary } from "./util/http";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger } from "@gnu-taler/taler-util";
|
||||||
import { AsyncCondition } from "./util/promiseUtils";
|
import { AsyncCondition } from "./util/promiseUtils";
|
||||||
import { Database } from "./util/query";
|
|
||||||
import { Duration, durationMin } from "@gnu-taler/taler-util";
|
import { Duration, durationMin } from "@gnu-taler/taler-util";
|
||||||
import { TimerGroup } from "./util/timer";
|
import { TimerGroup } from "./util/timer";
|
||||||
import { getExchangeTrust } from "./operations/currencies.js";
|
import { getExchangeTrust } from "./operations/currencies.js";
|
||||||
|
import { DbAccess } from "./util/query.js";
|
||||||
|
|
||||||
const builtinAuditors: AuditorTrustRecord[] = [
|
const builtinAuditors: AuditorTrustRecord[] = [
|
||||||
{
|
{
|
||||||
@ -205,12 +206,12 @@ export class Wallet {
|
|||||||
private stopped = false;
|
private stopped = false;
|
||||||
private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
|
private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
|
||||||
|
|
||||||
get db(): Database<typeof Stores> {
|
get db(): DbAccess<typeof WalletStoresV1> {
|
||||||
return this.ws.db;
|
return this.ws.db;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
db: Database<typeof Stores>,
|
db: DbAccess<typeof WalletStoresV1>,
|
||||||
http: HttpRequestLibrary,
|
http: HttpRequestLibrary,
|
||||||
cryptoWorkerFactory: CryptoWorkerFactory,
|
cryptoWorkerFactory: CryptoWorkerFactory,
|
||||||
) {
|
) {
|
||||||
@ -481,22 +482,21 @@ export class Wallet {
|
|||||||
* already been applied.
|
* already been applied.
|
||||||
*/
|
*/
|
||||||
async fillDefaults(): Promise<void> {
|
async fillDefaults(): Promise<void> {
|
||||||
await this.db.runWithWriteTransaction(
|
await this.db
|
||||||
[Stores.config, Stores.auditorTrustStore],
|
.mktx((x) => ({ config: x.config, auditorTrustStore: x.auditorTrust }))
|
||||||
async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
let applied = false;
|
let applied = false;
|
||||||
await tx.iter(Stores.config).forEach((x) => {
|
await tx.config.iter().forEach((x) => {
|
||||||
if (x.key == "currencyDefaultsApplied" && x.value == true) {
|
if (x.key == "currencyDefaultsApplied" && x.value == true) {
|
||||||
applied = true;
|
applied = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!applied) {
|
if (!applied) {
|
||||||
for (const c of builtinAuditors) {
|
for (const c of builtinAuditors) {
|
||||||
await tx.put(Stores.auditorTrustStore, c);
|
await tx.auditorTrustStore.put(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -553,10 +553,13 @@ export class Wallet {
|
|||||||
amount,
|
amount,
|
||||||
exchange: exchangeBaseUrl,
|
exchange: exchangeBaseUrl,
|
||||||
});
|
});
|
||||||
const exchangePaytoUris = await this.db.runWithReadTransaction(
|
const exchangePaytoUris = await this.db
|
||||||
[Stores.exchanges, Stores.reserves],
|
.mktx((x) => ({
|
||||||
(tx) => getFundingPaytoUris(tx, resp.reservePub),
|
exchanges: x.exchanges,
|
||||||
);
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub));
|
||||||
return {
|
return {
|
||||||
reservePub: resp.reservePub,
|
reservePub: resp.reservePub,
|
||||||
exchangePaytoUris,
|
exchangePaytoUris,
|
||||||
@ -627,29 +630,26 @@ export class Wallet {
|
|||||||
|
|
||||||
async refresh(oldCoinPub: string): Promise<void> {
|
async refresh(oldCoinPub: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const refreshGroupId = await this.db.runWithWriteTransaction(
|
const refreshGroupId = await this.db
|
||||||
[Stores.refreshGroups, Stores.denominations, Stores.coins],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
refreshGroups: x.refreshGroups,
|
||||||
|
denominations: x.denominations,
|
||||||
|
coins: x.coins,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
return await createRefreshGroup(
|
return await createRefreshGroup(
|
||||||
this.ws,
|
this.ws,
|
||||||
tx,
|
tx,
|
||||||
[{ coinPub: oldCoinPub }],
|
[{ coinPub: oldCoinPub }],
|
||||||
RefreshReason.Manual,
|
RefreshReason.Manual,
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
|
await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.latch.trigger();
|
this.latch.trigger();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findExchange(
|
|
||||||
exchangeBaseUrl: string,
|
|
||||||
): Promise<ExchangeRecord | undefined> {
|
|
||||||
return await this.db.get(Stores.exchanges, exchangeBaseUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPendingOperations({
|
async getPendingOperations({
|
||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
} = {}): Promise<PendingOperationsResponse> {
|
} = {}): Promise<PendingOperationsResponse> {
|
||||||
@ -665,87 +665,59 @@ export class Wallet {
|
|||||||
return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag);
|
return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
|
|
||||||
const denoms = await this.db
|
|
||||||
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
|
|
||||||
.toArray();
|
|
||||||
return denoms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all exchanges known to the exchange.
|
|
||||||
*
|
|
||||||
* @deprecated Use getExchanges instead
|
|
||||||
*/
|
|
||||||
async getExchangeRecords(): Promise<ExchangeRecord[]> {
|
|
||||||
return await this.db.iter(Stores.exchanges).toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getExchanges(): Promise<ExchangesListRespose> {
|
async getExchanges(): Promise<ExchangesListRespose> {
|
||||||
const exchangeRecords = await this.db.iter(Stores.exchanges).toArray();
|
|
||||||
const exchanges: ExchangeListItem[] = [];
|
const exchanges: ExchangeListItem[] = [];
|
||||||
for (const r of exchangeRecords) {
|
await this.db
|
||||||
const dp = r.detailsPointer;
|
.mktx((x) => ({
|
||||||
if (!dp) {
|
exchanges: x.exchanges,
|
||||||
continue;
|
exchangeDetails: x.exchangeDetails,
|
||||||
}
|
}))
|
||||||
const { currency, masterPublicKey } = dp;
|
.runReadOnly(async (tx) => {
|
||||||
const exchangeDetails = await this.db.get(Stores.exchangeDetails, [
|
const exchangeRecords = await tx.exchanges.iter().toArray();
|
||||||
r.baseUrl,
|
for (const r of exchangeRecords) {
|
||||||
currency,
|
const dp = r.detailsPointer;
|
||||||
masterPublicKey,
|
if (!dp) {
|
||||||
]);
|
continue;
|
||||||
if (!exchangeDetails) {
|
}
|
||||||
continue;
|
const { currency, masterPublicKey } = dp;
|
||||||
}
|
const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
|
||||||
exchanges.push({
|
if (!exchangeDetails) {
|
||||||
exchangeBaseUrl: r.baseUrl,
|
continue;
|
||||||
currency,
|
}
|
||||||
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
|
exchanges.push({
|
||||||
|
exchangeBaseUrl: r.baseUrl,
|
||||||
|
currency,
|
||||||
|
paytoUris: exchangeDetails.wireInfo.accounts.map(
|
||||||
|
(x) => x.payto_uri,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return { exchanges };
|
return { exchanges };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrencies(): Promise<WalletCurrencyInfo> {
|
async getCurrencies(): Promise<WalletCurrencyInfo> {
|
||||||
const trustedAuditors = await this.db
|
return await this.ws.db
|
||||||
.iter(Stores.auditorTrustStore)
|
.mktx((x) => ({
|
||||||
.toArray();
|
auditorTrust: x.auditorTrust,
|
||||||
const trustedExchanges = await this.db
|
exchangeTrust: x.exchangeTrust,
|
||||||
.iter(Stores.exchangeTrustStore)
|
}))
|
||||||
.toArray();
|
.runReadOnly(async (tx) => {
|
||||||
return {
|
const trustedAuditors = await tx.auditorTrust.iter().toArray();
|
||||||
trustedAuditors: trustedAuditors.map((x) => ({
|
const trustedExchanges = await tx.exchangeTrust.iter().toArray();
|
||||||
currency: x.currency,
|
return {
|
||||||
auditorBaseUrl: x.auditorBaseUrl,
|
trustedAuditors: trustedAuditors.map((x) => ({
|
||||||
auditorPub: x.auditorPub,
|
currency: x.currency,
|
||||||
})),
|
auditorBaseUrl: x.auditorBaseUrl,
|
||||||
trustedExchanges: trustedExchanges.map((x) => ({
|
auditorPub: x.auditorPub,
|
||||||
currency: x.currency,
|
})),
|
||||||
exchangeBaseUrl: x.exchangeBaseUrl,
|
trustedExchanges: trustedExchanges.map((x) => ({
|
||||||
exchangeMasterPub: x.exchangeMasterPub,
|
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -772,12 +744,6 @@ export class Wallet {
|
|||||||
return applyRefund(this.ws, talerRefundUri);
|
return applyRefund(this.ws, talerRefundUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPurchase(
|
|
||||||
contractTermsHash: string,
|
|
||||||
): Promise<PurchaseRecord | undefined> {
|
|
||||||
return this.db.get(Stores.purchases, contractTermsHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
async acceptTip(talerTipUri: string): Promise<void> {
|
async acceptTip(talerTipUri: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
return acceptTip(this.ws, talerTipUri);
|
return acceptTip(this.ws, talerTipUri);
|
||||||
@ -799,7 +765,13 @@ export class Wallet {
|
|||||||
* confirmation from the bank.).
|
* confirmation from the bank.).
|
||||||
*/
|
*/
|
||||||
public async handleNotifyReserve(): Promise<void> {
|
public async handleNotifyReserve(): Promise<void> {
|
||||||
const reserves = await this.db.iter(Stores.reserves).toArray();
|
const reserves = await this.ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.reserves.iter().toArray();
|
||||||
|
});
|
||||||
for (const r of reserves) {
|
for (const r of reserves) {
|
||||||
if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
|
if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
|
||||||
try {
|
try {
|
||||||
@ -837,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> {
|
async refuseProposal(proposalId: string): Promise<void> {
|
||||||
return refuseProposal(this.ws, proposalId);
|
return refuseProposal(this.ws, proposalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPurchaseDetails(proposalId: string): Promise<PurchaseDetails> {
|
|
||||||
const purchase = await this.db.get(Stores.purchases, proposalId);
|
|
||||||
if (!purchase) {
|
|
||||||
throw Error("unknown purchase");
|
|
||||||
}
|
|
||||||
const refundsDoneAmounts = Object.values(purchase.refunds)
|
|
||||||
.filter((x) => x.type === RefundState.Applied)
|
|
||||||
.map((x) => x.refundAmount);
|
|
||||||
|
|
||||||
const refundsPendingAmounts = Object.values(purchase.refunds)
|
|
||||||
.filter((x) => x.type === RefundState.Pending)
|
|
||||||
.map((x) => x.refundAmount);
|
|
||||||
const totalRefundAmount = Amounts.sum([
|
|
||||||
...refundsDoneAmounts,
|
|
||||||
...refundsPendingAmounts,
|
|
||||||
]).amount;
|
|
||||||
const refundsDoneFees = Object.values(purchase.refunds)
|
|
||||||
.filter((x) => x.type === RefundState.Applied)
|
|
||||||
.map((x) => x.refundFee);
|
|
||||||
const refundsPendingFees = Object.values(purchase.refunds)
|
|
||||||
.filter((x) => x.type === RefundState.Pending)
|
|
||||||
.map((x) => x.refundFee);
|
|
||||||
const totalRefundFees = Amounts.sum([
|
|
||||||
...refundsDoneFees,
|
|
||||||
...refundsPendingFees,
|
|
||||||
]).amount;
|
|
||||||
const totalFees = totalRefundFees;
|
|
||||||
return {
|
|
||||||
contractTerms: JSON.parse(purchase.download.contractTermsRaw),
|
|
||||||
hasRefund: purchase.timestampLastRefundStatus !== undefined,
|
|
||||||
totalRefundAmount: totalRefundAmount,
|
|
||||||
totalRefundAndRefreshFees: totalFees,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
|
benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
|
||||||
return this.ws.cryptoApi.benchmark(repetitions);
|
return this.ws.cryptoApi.benchmark(repetitions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setCoinSuspended(coinPub: string, suspended: boolean): Promise<void> {
|
async setCoinSuspended(coinPub: string, suspended: boolean): Promise<void> {
|
||||||
await this.db.runWithWriteTransaction([Stores.coins], async (tx) => {
|
await this.db
|
||||||
const c = await tx.get(Stores.coins, coinPub);
|
.mktx((x) => ({
|
||||||
if (!c) {
|
coins: x.coins,
|
||||||
logger.warn(`coin ${coinPub} not found, won't suspend`);
|
}))
|
||||||
return;
|
.runReadWrite(async (tx) => {
|
||||||
}
|
const c = await tx.coins.get(coinPub);
|
||||||
c.suspended = suspended;
|
if (!c) {
|
||||||
await tx.put(Stores.coins, 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.
|
* Dump the public information of coins we have in an easy-to-process format.
|
||||||
*/
|
*/
|
||||||
async dumpCoins(): Promise<CoinDumpJson> {
|
async dumpCoins(): Promise<CoinDumpJson> {
|
||||||
const coins = await this.db.iter(Stores.coins).toArray();
|
|
||||||
const coinsJson: CoinDumpJson = { coins: [] };
|
const coinsJson: CoinDumpJson = { coins: [] };
|
||||||
for (const c of coins) {
|
await this.ws.db
|
||||||
const denom = await this.db.get(Stores.denominations, [
|
.mktx((x) => ({
|
||||||
c.exchangeBaseUrl,
|
coins: x.coins,
|
||||||
c.denomPubHash,
|
denominations: x.denominations,
|
||||||
]);
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
if (!denom) {
|
}))
|
||||||
console.error("no denom session found for coin");
|
.runReadOnly(async (tx) => {
|
||||||
continue;
|
const coins = await tx.coins.iter().toArray();
|
||||||
}
|
for (const c of coins) {
|
||||||
const cs = c.coinSource;
|
const denom = await tx.denominations.get([
|
||||||
let refreshParentCoinPub: string | undefined;
|
c.exchangeBaseUrl,
|
||||||
if (cs.type == CoinSourceType.Refresh) {
|
c.denomPubHash,
|
||||||
refreshParentCoinPub = cs.oldCoinPub;
|
]);
|
||||||
}
|
if (!denom) {
|
||||||
let withdrawalReservePub: string | undefined;
|
console.error("no denom session found for coin");
|
||||||
if (cs.type == CoinSourceType.Withdraw) {
|
continue;
|
||||||
const ws = await this.db.get(
|
}
|
||||||
Stores.withdrawalGroups,
|
const cs = c.coinSource;
|
||||||
cs.withdrawalGroupId,
|
let refreshParentCoinPub: string | undefined;
|
||||||
);
|
if (cs.type == CoinSourceType.Refresh) {
|
||||||
if (!ws) {
|
refreshParentCoinPub = cs.oldCoinPub;
|
||||||
console.error("no withdrawal session found for coin");
|
}
|
||||||
continue;
|
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;
|
return coinsJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -963,6 +900,55 @@ export class Wallet {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateReserve(reservePub: string): Promise<ReserveRecord | undefined> {
|
||||||
|
await forceQueryReserve(this.ws, reservePub);
|
||||||
|
return await this.ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.reserves.get(reservePub);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCoins(): Promise<CoinRecord[]> {
|
||||||
|
return await this.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
coins: x.coins,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.coins.iter().toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getReservesForExchange(
|
||||||
|
exchangeBaseUrl?: string,
|
||||||
|
): Promise<ReserveRecord[]> {
|
||||||
|
return await this.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
if (exchangeBaseUrl) {
|
||||||
|
return await tx.reserves
|
||||||
|
.iter()
|
||||||
|
.filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
|
||||||
|
} else {
|
||||||
|
return await tx.reserves.iter().toArray();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getReserve(reservePub: string): Promise<ReserveRecord | undefined> {
|
||||||
|
return await this.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.reserves.get(reservePub);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async runIntegrationtest(args: IntegrationTestArgs): Promise<void> {
|
async runIntegrationtest(args: IntegrationTestArgs): Promise<void> {
|
||||||
return runIntegrationTest(this.ws.http, this, args);
|
return runIntegrationTest(this.ws.http, this, args);
|
||||||
}
|
}
|
||||||
@ -1144,17 +1130,20 @@ export class Wallet {
|
|||||||
case "forceRefresh": {
|
case "forceRefresh": {
|
||||||
const req = codecForForceRefreshRequest().decode(payload);
|
const req = codecForForceRefreshRequest().decode(payload);
|
||||||
const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
|
const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
|
||||||
const refreshGroupId = await this.db.runWithWriteTransaction(
|
const refreshGroupId = await this.db
|
||||||
[Stores.refreshGroups, Stores.denominations, Stores.coins],
|
.mktx((x) => ({
|
||||||
async (tx) => {
|
refreshGroups: x.refreshGroups,
|
||||||
|
denominations: x.denominations,
|
||||||
|
coins: x.coins,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
return await createRefreshGroup(
|
return await createRefreshGroup(
|
||||||
this.ws,
|
this.ws,
|
||||||
tx,
|
tx,
|
||||||
coinPubs,
|
coinPubs,
|
||||||
RefreshReason.Manual,
|
RefreshReason.Manual,
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
refreshGroupId,
|
refreshGroupId,
|
||||||
};
|
};
|
||||||
|
@ -30,10 +30,10 @@ import {
|
|||||||
OpenedPromise,
|
OpenedPromise,
|
||||||
openPromise,
|
openPromise,
|
||||||
openTalerDatabase,
|
openTalerDatabase,
|
||||||
Database,
|
|
||||||
Stores,
|
|
||||||
makeErrorDetails,
|
makeErrorDetails,
|
||||||
deleteTalerDatabase,
|
deleteTalerDatabase,
|
||||||
|
DbAccess,
|
||||||
|
WalletStoresV1,
|
||||||
} from "@gnu-taler/taler-wallet-core";
|
} from "@gnu-taler/taler-wallet-core";
|
||||||
import {
|
import {
|
||||||
classifyTalerUri,
|
classifyTalerUri,
|
||||||
@ -52,7 +52,7 @@ import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
|
|||||||
*/
|
*/
|
||||||
let currentWallet: Wallet | undefined;
|
let currentWallet: Wallet | undefined;
|
||||||
|
|
||||||
let currentDatabase: Database<typeof Stores> | undefined;
|
let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last version if an outdated DB, if applicable.
|
* Last version if an outdated DB, if applicable.
|
||||||
|
Loading…
Reference in New Issue
Block a user