history events WIP
This commit is contained in:
parent
1b9c5855a8
commit
fa4621e70c
@ -25,15 +25,15 @@ import {
|
|||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
DenominationStatus,
|
DenominationStatus,
|
||||||
WireFee,
|
WireFee,
|
||||||
|
ExchangeUpdateReason,
|
||||||
|
ExchangeUpdatedEventRecord,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import {
|
import {
|
||||||
canonicalizeBaseUrl,
|
canonicalizeBaseUrl,
|
||||||
extractTalerStamp,
|
extractTalerStamp,
|
||||||
extractTalerStampOrThrow,
|
extractTalerStampOrThrow,
|
||||||
} from "../util/helpers";
|
} from "../util/helpers";
|
||||||
import {
|
import { Database } from "../util/query";
|
||||||
Database
|
|
||||||
} from "../util/query";
|
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import { parsePaytoUri } from "../util/payto";
|
import { parsePaytoUri } from "../util/payto";
|
||||||
import {
|
import {
|
||||||
@ -78,7 +78,7 @@ async function setExchangeError(
|
|||||||
exchange.lastError = err;
|
exchange.lastError = err;
|
||||||
return exchange;
|
return exchange;
|
||||||
};
|
};
|
||||||
await ws.db.mutate( Stores.exchanges, baseUrl, mut);
|
await ws.db.mutate(Stores.exchanges, baseUrl, mut);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,12 +91,9 @@ async function updateExchangeWithKeys(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const existingExchangeRecord = await ws.db.get(
|
const existingExchangeRecord = await ws.db.get(Stores.exchanges, baseUrl);
|
||||||
Stores.exchanges,
|
|
||||||
baseUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FETCH_KEYS) {
|
if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const keysUrl = new URL("keys", baseUrl);
|
const keysUrl = new URL("keys", baseUrl);
|
||||||
@ -194,7 +191,7 @@ async function updateExchangeWithKeys(
|
|||||||
masterPublicKey: exchangeKeysJson.master_public_key,
|
masterPublicKey: exchangeKeysJson.master_public_key,
|
||||||
protocolVersion: protocolVersion,
|
protocolVersion: protocolVersion,
|
||||||
};
|
};
|
||||||
r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
|
r.updateStatus = ExchangeUpdateStatus.FetchWire;
|
||||||
r.lastError = undefined;
|
r.lastError = undefined;
|
||||||
await tx.put(Stores.exchanges, r);
|
await tx.put(Stores.exchanges, r);
|
||||||
|
|
||||||
@ -213,6 +210,38 @@ async function updateExchangeWithKeys(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateExchangeFinalize(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
exchangeBaseUrl: string,
|
||||||
|
) {
|
||||||
|
const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
|
||||||
|
if (!exchange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exchange.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ws.db.runWithWriteTransaction(
|
||||||
|
[Stores.exchanges, Stores.exchangeUpdatedEvents],
|
||||||
|
async tx => {
|
||||||
|
const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
r.updateStatus = ExchangeUpdateStatus.Finished;
|
||||||
|
await tx.put(Stores.exchanges, r);
|
||||||
|
const updateEvent: ExchangeUpdatedEventRecord = {
|
||||||
|
exchangeBaseUrl: exchange.baseUrl,
|
||||||
|
timestamp: getTimestampNow(),
|
||||||
|
};
|
||||||
|
await tx.put(Stores.exchangeUpdatedEvents, updateEvent);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function updateExchangeWithTermsOfService(
|
async function updateExchangeWithTermsOfService(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
@ -221,7 +250,7 @@ async function updateExchangeWithTermsOfService(
|
|||||||
if (!exchange) {
|
if (!exchange) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_TERMS) {
|
if (exchange.updateStatus != ExchangeUpdateStatus.FetchTerms) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reqUrl = new URL("terms", exchangeBaseUrl);
|
const reqUrl = new URL("terms", exchangeBaseUrl);
|
||||||
@ -243,12 +272,12 @@ async function updateExchangeWithTermsOfService(
|
|||||||
if (!r) {
|
if (!r) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (r.updateStatus != ExchangeUpdateStatus.FETCH_TERMS) {
|
if (r.updateStatus != ExchangeUpdateStatus.FetchTerms) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
r.termsOfServiceText = tosText;
|
r.termsOfServiceText = tosText;
|
||||||
r.termsOfServiceLastEtag = tosEtag;
|
r.termsOfServiceLastEtag = tosEtag;
|
||||||
r.updateStatus = ExchangeUpdateStatus.FINISHED;
|
r.updateStatus = ExchangeUpdateStatus.FinalizeUpdate;
|
||||||
await tx.put(Stores.exchanges, r);
|
await tx.put(Stores.exchanges, r);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -282,7 +311,7 @@ async function updateExchangeWithWireInfo(
|
|||||||
if (!exchange) {
|
if (!exchange) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
|
if (exchange.updateStatus != ExchangeUpdateStatus.FetchWire) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const details = exchange.details;
|
const details = exchange.details;
|
||||||
@ -349,14 +378,14 @@ async function updateExchangeWithWireInfo(
|
|||||||
if (!r) {
|
if (!r) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (r.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
|
if (r.updateStatus != ExchangeUpdateStatus.FetchWire) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
r.wireInfo = {
|
r.wireInfo = {
|
||||||
accounts: wireInfo.accounts,
|
accounts: wireInfo.accounts,
|
||||||
feesForType: feesForType,
|
feesForType: feesForType,
|
||||||
};
|
};
|
||||||
r.updateStatus = ExchangeUpdateStatus.FETCH_TERMS;
|
r.updateStatus = ExchangeUpdateStatus.FetchTerms;
|
||||||
r.lastError = undefined;
|
r.lastError = undefined;
|
||||||
await tx.put(Stores.exchanges, r);
|
await tx.put(Stores.exchanges, r);
|
||||||
});
|
});
|
||||||
@ -390,12 +419,13 @@ async function updateExchangeFromUrlImpl(
|
|||||||
const r = await ws.db.get(Stores.exchanges, baseUrl);
|
const r = await ws.db.get(Stores.exchanges, baseUrl);
|
||||||
if (!r) {
|
if (!r) {
|
||||||
const newExchangeRecord: ExchangeRecord = {
|
const newExchangeRecord: ExchangeRecord = {
|
||||||
|
builtIn: false,
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
details: undefined,
|
details: undefined,
|
||||||
wireInfo: undefined,
|
wireInfo: undefined,
|
||||||
updateStatus: ExchangeUpdateStatus.FETCH_KEYS,
|
updateStatus: ExchangeUpdateStatus.FetchKeys,
|
||||||
updateStarted: now,
|
updateStarted: now,
|
||||||
updateReason: "initial",
|
updateReason: ExchangeUpdateReason.Initial,
|
||||||
timestampAdded: getTimestampNow(),
|
timestampAdded: getTimestampNow(),
|
||||||
termsOfServiceAcceptedEtag: undefined,
|
termsOfServiceAcceptedEtag: undefined,
|
||||||
termsOfServiceAcceptedTimestamp: undefined,
|
termsOfServiceAcceptedTimestamp: undefined,
|
||||||
@ -409,14 +439,14 @@ async function updateExchangeFromUrlImpl(
|
|||||||
if (!rec) {
|
if (!rec) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && !forceNow) {
|
if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && !forceNow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && forceNow) {
|
if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && forceNow) {
|
||||||
rec.updateReason = "forced";
|
rec.updateReason = ExchangeUpdateReason.Forced;
|
||||||
}
|
}
|
||||||
rec.updateStarted = now;
|
rec.updateStarted = now;
|
||||||
rec.updateStatus = ExchangeUpdateStatus.FETCH_KEYS;
|
rec.updateStatus = ExchangeUpdateStatus.FetchKeys;
|
||||||
rec.lastError = undefined;
|
rec.lastError = undefined;
|
||||||
t.put(Stores.exchanges, rec);
|
t.put(Stores.exchanges, rec);
|
||||||
});
|
});
|
||||||
@ -425,6 +455,7 @@ async function updateExchangeFromUrlImpl(
|
|||||||
await updateExchangeWithKeys(ws, baseUrl);
|
await updateExchangeWithKeys(ws, baseUrl);
|
||||||
await updateExchangeWithWireInfo(ws, baseUrl);
|
await updateExchangeWithWireInfo(ws, baseUrl);
|
||||||
await updateExchangeWithTermsOfService(ws, baseUrl);
|
await updateExchangeWithTermsOfService(ws, baseUrl);
|
||||||
|
await updateExchangeFinalize(ws, baseUrl);
|
||||||
|
|
||||||
const updatedExchange = await ws.db.get(Stores.exchanges, baseUrl);
|
const updatedExchange = await ws.db.get(Stores.exchanges, baseUrl);
|
||||||
|
|
||||||
|
@ -18,10 +18,132 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { Stores, TipRecord } from "../types/dbTypes";
|
import {
|
||||||
|
Stores,
|
||||||
|
TipRecord,
|
||||||
|
ProposalStatus,
|
||||||
|
ProposalRecord,
|
||||||
|
} from "../types/dbTypes";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import { AmountJson } from "../util/amounts";
|
import { AmountJson } from "../util/amounts";
|
||||||
import { HistoryQuery, HistoryEvent, HistoryEventType } from "../types/history";
|
import {
|
||||||
|
HistoryQuery,
|
||||||
|
HistoryEvent,
|
||||||
|
HistoryEventType,
|
||||||
|
OrderShortInfo,
|
||||||
|
ReserveType,
|
||||||
|
ReserveCreationDetail,
|
||||||
|
} from "../types/history";
|
||||||
|
import { assertUnreachable } from "../util/assertUnreachable";
|
||||||
|
import { TransactionHandle, Store } from "../util/query";
|
||||||
|
import { ReserveTransactionType } from "../types/ReserveTransaction";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an event ID from the type and the primary key for the event.
|
||||||
|
*/
|
||||||
|
function makeEventId(type: HistoryEventType, ...args: string[]) {
|
||||||
|
return type + ";" + args.map(x => encodeURIComponent(x)).join(";");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrderShortInfo(
|
||||||
|
proposal: ProposalRecord,
|
||||||
|
): OrderShortInfo | undefined {
|
||||||
|
const download = proposal.download;
|
||||||
|
if (!download) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
amount: download.contractTerms.amount,
|
||||||
|
orderId: download.contractTerms.order_id,
|
||||||
|
merchantBaseUrl: download.contractTerms.merchant_base_url,
|
||||||
|
proposalId: proposal.proposalId,
|
||||||
|
summary: download.contractTerms.summary || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function collectProposalHistory(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
history: HistoryEvent[],
|
||||||
|
historyQuery?: HistoryQuery,
|
||||||
|
) {
|
||||||
|
tx.iter(Stores.proposals).forEachAsync(async proposal => {
|
||||||
|
const status = proposal.proposalStatus;
|
||||||
|
switch (status) {
|
||||||
|
case ProposalStatus.ACCEPTED:
|
||||||
|
{
|
||||||
|
const shortInfo = getOrderShortInfo(proposal);
|
||||||
|
if (!shortInfo) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.OrderAccepted,
|
||||||
|
eventId: makeEventId(
|
||||||
|
HistoryEventType.OrderAccepted,
|
||||||
|
proposal.proposalId,
|
||||||
|
),
|
||||||
|
orderShortInfo: shortInfo,
|
||||||
|
timestamp: proposal.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProposalStatus.DOWNLOADING:
|
||||||
|
case ProposalStatus.PROPOSED:
|
||||||
|
// no history event needed
|
||||||
|
break;
|
||||||
|
case ProposalStatus.REJECTED:
|
||||||
|
{
|
||||||
|
const shortInfo = getOrderShortInfo(proposal);
|
||||||
|
if (!shortInfo) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.OrderRefused,
|
||||||
|
eventId: makeEventId(
|
||||||
|
HistoryEventType.OrderRefused,
|
||||||
|
proposal.proposalId,
|
||||||
|
),
|
||||||
|
orderShortInfo: shortInfo,
|
||||||
|
timestamp: proposal.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProposalStatus.REPURCHASE:
|
||||||
|
{
|
||||||
|
const alreadyPaidProposal = await tx.get(
|
||||||
|
Stores.proposals,
|
||||||
|
proposal.repurchaseProposalId,
|
||||||
|
);
|
||||||
|
if (!alreadyPaidProposal) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const alreadyPaidOrderShortInfo = getOrderShortInfo(
|
||||||
|
alreadyPaidProposal,
|
||||||
|
);
|
||||||
|
if (!alreadyPaidOrderShortInfo) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const newOrderShortInfo = getOrderShortInfo(proposal);
|
||||||
|
if (!newOrderShortInfo) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.OrderRedirected,
|
||||||
|
eventId: makeEventId(
|
||||||
|
HistoryEventType.OrderRedirected,
|
||||||
|
proposal.proposalId,
|
||||||
|
),
|
||||||
|
alreadyPaidOrderShortInfo,
|
||||||
|
newOrderShortInfo,
|
||||||
|
timestamp: proposal.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrive the full event history for this wallet.
|
* Retrive the full event history for this wallet.
|
||||||
@ -40,19 +162,222 @@ export async function getHistory(
|
|||||||
await ws.db.runWithReadTransaction(
|
await ws.db.runWithReadTransaction(
|
||||||
[
|
[
|
||||||
Stores.currencies,
|
Stores.currencies,
|
||||||
Stores.coins,
|
|
||||||
Stores.denominations,
|
|
||||||
Stores.exchanges,
|
Stores.exchanges,
|
||||||
|
Stores.exchangeUpdatedEvents,
|
||||||
Stores.proposals,
|
Stores.proposals,
|
||||||
Stores.purchases,
|
Stores.purchases,
|
||||||
Stores.refreshGroups,
|
Stores.refreshGroups,
|
||||||
Stores.reserves,
|
Stores.reserves,
|
||||||
Stores.tips,
|
Stores.tips,
|
||||||
Stores.withdrawalSession,
|
Stores.withdrawalSession,
|
||||||
|
Stores.payEvents,
|
||||||
|
Stores.refundEvents,
|
||||||
|
Stores.reserveUpdatedEvents,
|
||||||
],
|
],
|
||||||
async tx => {
|
async tx => {
|
||||||
// FIXME: implement new history schema!!
|
tx.iter(Stores.exchanges).forEach(exchange => {
|
||||||
}
|
history.push({
|
||||||
|
type: HistoryEventType.ExchangeAdded,
|
||||||
|
builtIn: false,
|
||||||
|
eventId: makeEventId(
|
||||||
|
HistoryEventType.ExchangeAdded,
|
||||||
|
exchange.baseUrl,
|
||||||
|
),
|
||||||
|
exchangeBaseUrl: exchange.baseUrl,
|
||||||
|
timestamp: exchange.timestampAdded,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.iter(Stores.exchangeUpdatedEvents).forEach(eu => {
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.ExchangeUpdated,
|
||||||
|
eventId: makeEventId(
|
||||||
|
HistoryEventType.ExchangeUpdated,
|
||||||
|
eu.exchangeBaseUrl,
|
||||||
|
),
|
||||||
|
exchangeBaseUrl: eu.exchangeBaseUrl,
|
||||||
|
timestamp: eu.timestamp,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.iter(Stores.withdrawalSession).forEach(wsr => {
|
||||||
|
if (wsr.finishTimestamp) {
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.Withdrawn,
|
||||||
|
withdrawSessionId: wsr.withdrawSessionId,
|
||||||
|
eventId: makeEventId(
|
||||||
|
HistoryEventType.Withdrawn,
|
||||||
|
wsr.withdrawSessionId,
|
||||||
|
),
|
||||||
|
amountWithdrawnEffective: Amounts.toString(wsr.totalCoinValue),
|
||||||
|
amountWithdrawnRaw: Amounts.toString(wsr.rawWithdrawalAmount),
|
||||||
|
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
||||||
|
timestamp: wsr.finishTimestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await collectProposalHistory(tx, history, historyQuery);
|
||||||
|
|
||||||
|
await tx.iter(Stores.payEvents).forEachAsync(async (pe) => {
|
||||||
|
const proposal = await tx.get(Stores.proposals, pe.proposalId);
|
||||||
|
if (!proposal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const orderShortInfo = getOrderShortInfo(proposal);
|
||||||
|
if (!orderShortInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.PaymentSent,
|
||||||
|
eventId: makeEventId(HistoryEventType.PaymentSent, pe.proposalId),
|
||||||
|
orderShortInfo,
|
||||||
|
replay: pe.isReplay,
|
||||||
|
sessionId: pe.sessionId,
|
||||||
|
timestamp: pe.timestamp,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.iter(Stores.refreshGroups).forEachAsync(async (rg) => {
|
||||||
|
if (!rg.finishedTimestamp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let numInputCoins = 0;
|
||||||
|
let numRefreshedInputCoins = 0;
|
||||||
|
let numOutputCoins = 0;
|
||||||
|
const amountsRaw: AmountJson[] = [];
|
||||||
|
const amountsEffective: AmountJson[] = [];
|
||||||
|
for (let i = 0; i < rg.refreshSessionPerCoin.length; i++) {
|
||||||
|
const session = rg.refreshSessionPerCoin[i];
|
||||||
|
numInputCoins++;
|
||||||
|
if (session) {
|
||||||
|
numRefreshedInputCoins++;
|
||||||
|
amountsRaw.push(session.valueWithFee);
|
||||||
|
amountsEffective.push(session.valueOutput);
|
||||||
|
numOutputCoins += session.newDenoms.length;
|
||||||
|
} else {
|
||||||
|
const c = await tx.get(Stores.coins, rg.oldCoinPubs[i]);
|
||||||
|
if (!c) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
amountsRaw.push(c.currentAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let amountRefreshedRaw = Amounts.sum(amountsRaw).amount;
|
||||||
|
let amountRefreshedEffective: AmountJson;
|
||||||
|
if (amountsEffective.length == 0) {
|
||||||
|
amountRefreshedEffective = Amounts.getZero(amountRefreshedRaw.currency);
|
||||||
|
} else {
|
||||||
|
amountRefreshedEffective = Amounts.sum(amountsEffective).amount;
|
||||||
|
}
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.Refreshed,
|
||||||
|
refreshGroupId: rg.refreshGroupId,
|
||||||
|
eventId: makeEventId(HistoryEventType.Refreshed, rg.refreshGroupId),
|
||||||
|
timestamp: rg.finishedTimestamp,
|
||||||
|
refreshReason: rg.reason,
|
||||||
|
amountRefreshedEffective: Amounts.toString(amountRefreshedEffective),
|
||||||
|
amountRefreshedRaw: Amounts.toString(amountRefreshedRaw),
|
||||||
|
numInputCoins,
|
||||||
|
numOutputCoins,
|
||||||
|
numRefreshedInputCoins,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.iter(Stores.reserveUpdatedEvents).forEachAsync(async (ru) => {
|
||||||
|
const reserve = await tx.get(Stores.reserves, ru.reservePub);
|
||||||
|
if (!reserve) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let reserveCreationDetail: ReserveCreationDetail;
|
||||||
|
if (reserve.bankWithdrawStatusUrl) {
|
||||||
|
reserveCreationDetail = {
|
||||||
|
type: ReserveType.TalerBankWithdraw,
|
||||||
|
bankUrl: reserve.bankWithdrawStatusUrl,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reserveCreationDetail = {
|
||||||
|
type: ReserveType.Manual,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.ReserveBalanceUpdated,
|
||||||
|
eventId: makeEventId(HistoryEventType.ReserveBalanceUpdated, ru.reserveUpdateId),
|
||||||
|
amountExpected: ru.amountExpected,
|
||||||
|
amountReserveBalance: ru.amountReserveBalance,
|
||||||
|
timestamp: reserve.created,
|
||||||
|
newHistoryTransactions: ru.newHistoryTransactions,
|
||||||
|
reserveShortInfo: {
|
||||||
|
exchangeBaseUrl: reserve.exchangeBaseUrl,
|
||||||
|
reserveCreationDetail,
|
||||||
|
reservePub: reserve.reservePub,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.iter(Stores.tips).forEach((tip) => {
|
||||||
|
if (tip.acceptedTimestamp) {
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.TipAccepted,
|
||||||
|
eventId: makeEventId(HistoryEventType.TipAccepted, tip.tipId),
|
||||||
|
timestamp: tip.acceptedTimestamp,
|
||||||
|
tipId: tip.tipId,
|
||||||
|
tipAmount: Amounts.toString(tip.amount),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.iter(Stores.refundEvents).forEachAsync(async (re) => {
|
||||||
|
const proposal = await tx.get(Stores.proposals, re.proposalId);
|
||||||
|
if (!proposal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const purchase = await tx.get(Stores.purchases, re.proposalId);
|
||||||
|
if (!purchase) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const orderShortInfo = getOrderShortInfo(proposal);
|
||||||
|
if (!orderShortInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const purchaseAmount = Amounts.parseOrThrow(purchase.contractTerms.amount);
|
||||||
|
let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency);
|
||||||
|
let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency);
|
||||||
|
let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency);
|
||||||
|
Object.keys(purchase.refundState.refundsDone).forEach((x, i) => {
|
||||||
|
const r = purchase.refundState.refundsDone[x];
|
||||||
|
if (r.refundGroupId !== re.refundGroupId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const refundAmount = Amounts.parseOrThrow(r.perm.refund_amount);
|
||||||
|
const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
|
||||||
|
amountRefundedRaw = Amounts.add(amountRefundedRaw, refundAmount).amount;
|
||||||
|
amountRefundedEffective = Amounts.add(amountRefundedEffective, refundAmount).amount;
|
||||||
|
amountRefundedEffective = Amounts.sub(amountRefundedEffective, refundFee).amount;
|
||||||
|
});
|
||||||
|
Object.keys(purchase.refundState.refundsFailed).forEach((x, i) => {
|
||||||
|
const r = purchase.refundState.refundsFailed[x];
|
||||||
|
if (r.refundGroupId !== re.refundGroupId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ra = Amounts.parseOrThrow(r.perm.refund_amount);
|
||||||
|
const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
|
||||||
|
amountRefundedRaw = Amounts.add(amountRefundedRaw, ra).amount;
|
||||||
|
amountRefundedInvalid = Amounts.add(amountRefundedInvalid, ra).amount;
|
||||||
|
amountRefundedEffective = Amounts.sub(amountRefundedEffective, refundFee).amount;
|
||||||
|
});
|
||||||
|
history.push({
|
||||||
|
type: HistoryEventType.Refund,
|
||||||
|
eventId: makeEventId(HistoryEventType.Refund, re.refundGroupId),
|
||||||
|
refundGroupId: re.refundGroupId,
|
||||||
|
orderShortInfo,
|
||||||
|
timestamp: re.timestamp,
|
||||||
|
amountRefundedEffective: Amounts.toString(amountRefundedEffective),
|
||||||
|
amountRefundedRaw: Amounts.toString(amountRefundedRaw),
|
||||||
|
amountRefundedInvalid: Amounts.toString(amountRefundedInvalid),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));
|
history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));
|
||||||
|
@ -755,6 +755,7 @@ export async function submitPay(
|
|||||||
proposalId,
|
proposalId,
|
||||||
sessionId,
|
sessionId,
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
|
isReplay: !isFirst,
|
||||||
};
|
};
|
||||||
await tx.put(Stores.payEvents, payEvent);
|
await tx.put(Stores.payEvents, payEvent);
|
||||||
},
|
},
|
||||||
|
@ -54,7 +54,7 @@ async function gatherExchangePending(
|
|||||||
}
|
}
|
||||||
await tx.iter(Stores.exchanges).forEach(e => {
|
await tx.iter(Stores.exchanges).forEach(e => {
|
||||||
switch (e.updateStatus) {
|
switch (e.updateStatus) {
|
||||||
case ExchangeUpdateStatus.FINISHED:
|
case ExchangeUpdateStatus.Finished:
|
||||||
if (e.lastError) {
|
if (e.lastError) {
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.Bug,
|
type: PendingOperationType.Bug,
|
||||||
@ -89,7 +89,7 @@ async function gatherExchangePending(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ExchangeUpdateStatus.FETCH_KEYS:
|
case ExchangeUpdateStatus.FetchKeys:
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.ExchangeUpdate,
|
type: PendingOperationType.ExchangeUpdate,
|
||||||
givesLifeness: false,
|
givesLifeness: false,
|
||||||
@ -99,7 +99,7 @@ async function gatherExchangePending(
|
|||||||
reason: e.updateReason || "unknown",
|
reason: e.updateReason || "unknown",
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case ExchangeUpdateStatus.FETCH_WIRE:
|
case ExchangeUpdateStatus.FetchWire:
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.ExchangeUpdate,
|
type: PendingOperationType.ExchangeUpdate,
|
||||||
givesLifeness: false,
|
givesLifeness: false,
|
||||||
@ -109,6 +109,16 @@ async function gatherExchangePending(
|
|||||||
reason: e.updateReason || "unknown",
|
reason: e.updateReason || "unknown",
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case ExchangeUpdateStatus.FinalizeUpdate:
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: PendingOperationType.ExchangeUpdate,
|
||||||
|
givesLifeness: false,
|
||||||
|
stage: "finalize-update",
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
lastError: e.lastError,
|
||||||
|
reason: e.updateReason || "unknown",
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.Bug,
|
type: PendingOperationType.Bug,
|
||||||
@ -311,7 +321,7 @@ async function gatherTipPending(
|
|||||||
if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) {
|
if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (tip.accepted) {
|
if (tip.acceptedTimestamp) {
|
||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.TipPickup,
|
type: PendingOperationType.TipPickup,
|
||||||
givesLifeness: true,
|
givesLifeness: true,
|
||||||
|
@ -548,7 +548,7 @@ export async function createRefreshGroup(
|
|||||||
finishedTimestamp: undefined,
|
finishedTimestamp: undefined,
|
||||||
finishedPerCoin: oldCoinPubs.map(x => false),
|
finishedPerCoin: oldCoinPubs.map(x => false),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
lastErrorPerCoin: oldCoinPubs.map(x => undefined),
|
lastErrorPerCoin: {},
|
||||||
oldCoinPubs: oldCoinPubs.map(x => x.coinPub),
|
oldCoinPubs: oldCoinPubs.map(x => x.coinPub),
|
||||||
reason,
|
reason,
|
||||||
refreshGroupId,
|
refreshGroupId,
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
OperationError,
|
OperationError,
|
||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
|
CoinPublicKey,
|
||||||
} from "../types/walletTypes";
|
} from "../types/walletTypes";
|
||||||
import {
|
import {
|
||||||
Stores,
|
Stores,
|
||||||
@ -36,6 +37,7 @@ import {
|
|||||||
CoinStatus,
|
CoinStatus,
|
||||||
RefundReason,
|
RefundReason,
|
||||||
RefundEventRecord,
|
RefundEventRecord,
|
||||||
|
RefundInfo,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { parseRefundUri } from "../util/taleruri";
|
import { parseRefundUri } from "../util/taleruri";
|
||||||
@ -214,13 +216,6 @@ export async function acceptRefundResponse(
|
|||||||
timestampQueried: now,
|
timestampQueried: now,
|
||||||
reason,
|
reason,
|
||||||
});
|
});
|
||||||
|
|
||||||
const refundEvent: RefundEventRecord = {
|
|
||||||
proposalId,
|
|
||||||
refundGroupId,
|
|
||||||
timestamp: now,
|
|
||||||
};
|
|
||||||
await tx.put(Stores.refundEvents, refundEvent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.put(Stores.purchases, p);
|
await tx.put(Stores.purchases, p);
|
||||||
@ -406,6 +401,9 @@ async function processPurchaseApplyRefundImpl(
|
|||||||
console.log("no pending refunds");
|
console.log("no pending refunds");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newRefundsDone: { [sig: string]: RefundInfo } = {};
|
||||||
|
const newRefundsFailed: { [sig: string]: RefundInfo } = {};
|
||||||
for (const pk of pendingKeys) {
|
for (const pk of pendingKeys) {
|
||||||
const info = purchase.refundState.refundsPending[pk];
|
const info = purchase.refundState.refundsPending[pk];
|
||||||
const perm = info.perm;
|
const perm = info.perm;
|
||||||
@ -424,13 +422,13 @@ async function processPurchaseApplyRefundImpl(
|
|||||||
const reqUrl = new URL("refund", exchangeUrl);
|
const reqUrl = new URL("refund", exchangeUrl);
|
||||||
const resp = await ws.http.postJson(reqUrl.href, req);
|
const resp = await ws.http.postJson(reqUrl.href, req);
|
||||||
console.log("sent refund permission");
|
console.log("sent refund permission");
|
||||||
let refundGone = false;
|
|
||||||
switch (resp.status) {
|
switch (resp.status) {
|
||||||
case HttpResponseStatus.Ok:
|
case HttpResponseStatus.Ok:
|
||||||
|
newRefundsDone[pk] = info;
|
||||||
break;
|
break;
|
||||||
case HttpResponseStatus.Gone:
|
case HttpResponseStatus.Gone:
|
||||||
// We're too late, refund is expired.
|
// We're too late, refund is expired.
|
||||||
refundGone = true;
|
newRefundsFailed[pk] = info;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
let body: string | null = null;
|
let body: string | null = null;
|
||||||
@ -446,53 +444,89 @@ async function processPurchaseApplyRefundImpl(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
let allRefundsProcessed = false;
|
||||||
|
await ws.db.runWithWriteTransaction(
|
||||||
|
[Stores.purchases, Stores.coins, Stores.refreshGroups, Stores.refundEvents],
|
||||||
|
async tx => {
|
||||||
|
const p = await tx.get(Stores.purchases, proposalId);
|
||||||
|
if (!p) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let allRefundsProcessed = false;
|
// Groups that failed/succeeded
|
||||||
|
let groups: { [refundGroupId: string]: boolean } = {};
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
// Avoid duplicates
|
||||||
[Stores.purchases, Stores.coins, Stores.refreshGroups],
|
const refreshCoinsMap: { [coinPub: string]: CoinPublicKey } = {};
|
||||||
async tx => {
|
|
||||||
const p = await tx.get(Stores.purchases, proposalId);
|
const modCoin = async (perm: MerchantRefundPermission) => {
|
||||||
if (!p) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (p.refundState.refundsPending[pk]) {
|
|
||||||
if (refundGone) {
|
|
||||||
p.refundState.refundsFailed[pk] = p.refundState.refundsPending[pk];
|
|
||||||
} else {
|
|
||||||
p.refundState.refundsDone[pk] = p.refundState.refundsPending[pk];
|
|
||||||
}
|
|
||||||
delete p.refundState.refundsPending[pk];
|
|
||||||
}
|
|
||||||
if (Object.keys(p.refundState.refundsPending).length === 0) {
|
|
||||||
p.refundStatusRetryInfo = initRetryInfo();
|
|
||||||
p.lastRefundStatusError = undefined;
|
|
||||||
allRefundsProcessed = true;
|
|
||||||
}
|
|
||||||
await tx.put(Stores.purchases, p);
|
|
||||||
const c = await tx.get(Stores.coins, perm.coin_pub);
|
const c = await tx.get(Stores.coins, perm.coin_pub);
|
||||||
if (!c) {
|
if (!c) {
|
||||||
console.warn("coin not found, can't apply refund");
|
console.warn("coin not found, can't apply refund");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
refreshCoinsMap[c.coinPub] = { coinPub: c.coinPub };
|
||||||
const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
|
const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
|
||||||
const refundFee = Amounts.parseOrThrow(perm.refund_fee);
|
const refundFee = Amounts.parseOrThrow(perm.refund_fee);
|
||||||
c.status = CoinStatus.Dormant;
|
c.status = CoinStatus.Dormant;
|
||||||
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
|
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
|
||||||
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
|
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
|
||||||
await tx.put(Stores.coins, c);
|
await tx.put(Stores.coins, c);
|
||||||
await createRefreshGroup(
|
};
|
||||||
tx,
|
|
||||||
[{ coinPub: perm.coin_pub }],
|
for (const pk of Object.keys(newRefundsFailed)) {
|
||||||
RefreshReason.Refund,
|
const r = newRefundsFailed[pk];
|
||||||
);
|
groups[r.refundGroupId] = true;
|
||||||
},
|
delete p.refundState.refundsPending[pk];
|
||||||
);
|
p.refundState.refundsFailed[pk] = r;
|
||||||
if (allRefundsProcessed) {
|
await modCoin(r.perm);
|
||||||
ws.notify({
|
}
|
||||||
type: NotificationType.RefundFinished,
|
|
||||||
});
|
for (const pk of Object.keys(newRefundsDone)) {
|
||||||
}
|
const r = newRefundsDone[pk];
|
||||||
|
groups[r.refundGroupId] = true;
|
||||||
|
delete p.refundState.refundsPending[pk];
|
||||||
|
p.refundState.refundsDone[pk] = r;
|
||||||
|
await modCoin(r.perm);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = getTimestampNow();
|
||||||
|
for (const g of Object.keys(groups)) {
|
||||||
|
let groupDone = true;
|
||||||
|
for (const pk of Object.keys(p.refundState.refundsPending)) {
|
||||||
|
const r = p.refundState.refundsPending[pk];
|
||||||
|
if (r.refundGroupId == g) {
|
||||||
|
groupDone = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (groupDone) {
|
||||||
|
const refundEvent: RefundEventRecord = {
|
||||||
|
proposalId,
|
||||||
|
refundGroupId: g,
|
||||||
|
timestamp: now,
|
||||||
|
}
|
||||||
|
await tx.put(Stores.refundEvents, refundEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(p.refundState.refundsPending).length === 0) {
|
||||||
|
p.refundStatusRetryInfo = initRetryInfo();
|
||||||
|
p.lastRefundStatusError = undefined;
|
||||||
|
allRefundsProcessed = true;
|
||||||
|
}
|
||||||
|
await tx.put(Stores.purchases, p);
|
||||||
|
await createRefreshGroup(
|
||||||
|
tx,
|
||||||
|
Object.values(refreshCoinsMap),
|
||||||
|
RefreshReason.Refund,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (allRefundsProcessed) {
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.RefundFinished,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.notify({
|
ws.notify({
|
||||||
|
@ -31,17 +31,17 @@ import {
|
|||||||
WithdrawalSessionRecord,
|
WithdrawalSessionRecord,
|
||||||
initRetryInfo,
|
initRetryInfo,
|
||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
|
ReserveUpdatedEventRecord,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import {
|
import {
|
||||||
Database,
|
|
||||||
TransactionAbort,
|
TransactionAbort,
|
||||||
} from "../util/query";
|
} from "../util/query";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
|
import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
|
||||||
import { WithdrawOperationStatusResponse, ReserveStatus } from "../types/talerTypes";
|
import { WithdrawOperationStatusResponse } from "../types/talerTypes";
|
||||||
import { assertUnreachable } from "../util/assertUnreachable";
|
import { assertUnreachable } from "../util/assertUnreachable";
|
||||||
import { encodeCrock } from "../crypto/talerCrypto";
|
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
||||||
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
||||||
import {
|
import {
|
||||||
getVerifiedWithdrawDenomList,
|
getVerifiedWithdrawDenomList,
|
||||||
@ -49,6 +49,7 @@ import {
|
|||||||
} from "./withdraw";
|
} from "./withdraw";
|
||||||
import { guardOperationException, OperationFailedAndReportedError } from "./errors";
|
import { guardOperationException, OperationFailedAndReportedError } from "./errors";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
|
import { codecForReserveStatus } from "../types/ReserveStatus";
|
||||||
|
|
||||||
const logger = new Logger("reserves.ts");
|
const logger = new Logger("reserves.ts");
|
||||||
|
|
||||||
@ -94,6 +95,7 @@ export async function createReserve(
|
|||||||
lastSuccessfulStatusQuery: undefined,
|
lastSuccessfulStatusQuery: undefined,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: initRetryInfo(),
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
|
reserveTransactions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const senderWire = req.senderWire;
|
const senderWire = req.senderWire;
|
||||||
@ -393,17 +395,35 @@ async function updateReserve(
|
|||||||
});
|
});
|
||||||
throw new OperationFailedAndReportedError(m);
|
throw new OperationFailedAndReportedError(m);
|
||||||
}
|
}
|
||||||
const reserveInfo = ReserveStatus.checked(await resp.json());
|
const respJson = await resp.json();
|
||||||
|
const reserveInfo = codecForReserveStatus.decode(respJson);
|
||||||
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
||||||
await ws.db.mutate(Stores.reserves, reserve.reservePub, r => {
|
await ws.db.runWithWriteTransaction([Stores.reserves, Stores.reserveUpdatedEvents], async (tx) => {
|
||||||
|
const r = await tx.get(Stores.reserves, reservePub);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
|
if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newHistoryTransactions = reserveInfo.history.slice(r.reserveTransactions.length);
|
||||||
|
|
||||||
|
const reserveUpdateId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
// FIXME: check / compare history!
|
// FIXME: check / compare history!
|
||||||
if (!r.lastSuccessfulStatusQuery) {
|
if (!r.lastSuccessfulStatusQuery) {
|
||||||
// FIXME: check if this matches initial expectations
|
// FIXME: check if this matches initial expectations
|
||||||
r.withdrawRemainingAmount = balance;
|
r.withdrawRemainingAmount = balance;
|
||||||
|
const reserveUpdate: ReserveUpdatedEventRecord = {
|
||||||
|
reservePub: r.reservePub,
|
||||||
|
timestamp: getTimestampNow(),
|
||||||
|
amountReserveBalance: Amounts.toString(balance),
|
||||||
|
amountExpected: Amounts.toString(reserve.initiallyRequestedAmount),
|
||||||
|
newHistoryTransactions,
|
||||||
|
reserveUpdateId,
|
||||||
|
};
|
||||||
|
await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
|
||||||
} else {
|
} else {
|
||||||
const expectedBalance = Amounts.sub(
|
const expectedBalance = Amounts.sub(
|
||||||
r.withdrawAllocatedAmount,
|
r.withdrawAllocatedAmount,
|
||||||
@ -423,11 +443,21 @@ async function updateReserve(
|
|||||||
} else {
|
} else {
|
||||||
// We're missing some money.
|
// We're missing some money.
|
||||||
}
|
}
|
||||||
|
const reserveUpdate: ReserveUpdatedEventRecord = {
|
||||||
|
reservePub: r.reservePub,
|
||||||
|
timestamp: getTimestampNow(),
|
||||||
|
amountReserveBalance: Amounts.toString(balance),
|
||||||
|
amountExpected: Amounts.toString(expectedBalance.amount),
|
||||||
|
newHistoryTransactions,
|
||||||
|
reserveUpdateId,
|
||||||
|
};
|
||||||
|
await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
|
||||||
}
|
}
|
||||||
r.lastSuccessfulStatusQuery = getTimestampNow();
|
r.lastSuccessfulStatusQuery = getTimestampNow();
|
||||||
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
|
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
|
||||||
r.retryInfo = initRetryInfo();
|
r.retryInfo = initRetryInfo();
|
||||||
return r;
|
r.reserveTransactions = reserveInfo.history;
|
||||||
|
await tx.put(Stores.reserves, r);
|
||||||
});
|
});
|
||||||
ws.notify( { type: NotificationType.ReserveUpdated });
|
ws.notify( { type: NotificationType.ReserveUpdated });
|
||||||
}
|
}
|
||||||
@ -561,7 +591,7 @@ async function depleteReserve(
|
|||||||
planchets: denomsForWithdraw.map(x => undefined),
|
planchets: denomsForWithdraw.map(x => undefined),
|
||||||
totalCoinValue,
|
totalCoinValue,
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: initRetryInfo(),
|
||||||
lastCoinErrors: denomsForWithdraw.map(x => undefined),
|
lastErrorPerCoin: {},
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -68,7 +68,8 @@ export async function getTipStatus(
|
|||||||
|
|
||||||
tipRecord = {
|
tipRecord = {
|
||||||
tipId,
|
tipId,
|
||||||
accepted: false,
|
acceptedTimestamp: undefined,
|
||||||
|
rejectedTimestamp: undefined,
|
||||||
amount,
|
amount,
|
||||||
deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire),
|
deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire),
|
||||||
exchangeUrl: tipPickupStatus.exchange_url,
|
exchangeUrl: tipPickupStatus.exchange_url,
|
||||||
@ -90,7 +91,7 @@ export async function getTipStatus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tipStatus: TipStatus = {
|
const tipStatus: TipStatus = {
|
||||||
accepted: !!tipRecord && tipRecord.accepted,
|
accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
|
||||||
amount: Amounts.parseOrThrow(tipPickupStatus.amount),
|
amount: Amounts.parseOrThrow(tipPickupStatus.amount),
|
||||||
amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
|
amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
|
||||||
exchangeUrl: tipPickupStatus.exchange_url,
|
exchangeUrl: tipPickupStatus.exchange_url,
|
||||||
@ -259,7 +260,7 @@ async function processTipImpl(
|
|||||||
rawWithdrawalAmount: tipRecord.amount,
|
rawWithdrawalAmount: tipRecord.amount,
|
||||||
withdrawn: planchets.map((x) => false),
|
withdrawn: planchets.map((x) => false),
|
||||||
totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
|
totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
|
||||||
lastCoinErrors: planchets.map((x) => undefined),
|
lastErrorPerCoin: {},
|
||||||
retryInfo: initRetryInfo(),
|
retryInfo: initRetryInfo(),
|
||||||
finishTimestamp: undefined,
|
finishTimestamp: undefined,
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
@ -296,7 +297,7 @@ export async function acceptTip(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tipRecord.accepted = true;
|
tipRecord.acceptedTimestamp = getTimestampNow();
|
||||||
await ws.db.put(Stores.tips, tipRecord);
|
await ws.db.put(Stores.tips, tipRecord);
|
||||||
|
|
||||||
await processTip(ws, tipId);
|
await processTip(ws, tipId);
|
||||||
|
@ -272,7 +272,7 @@ async function processPlanchet(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ws.withdrawn[coinIdx] = true;
|
ws.withdrawn[coinIdx] = true;
|
||||||
ws.lastCoinErrors[coinIdx] = undefined;
|
delete ws.lastErrorPerCoin[coinIdx];
|
||||||
let numDone = 0;
|
let numDone = 0;
|
||||||
for (let i = 0; i < ws.withdrawn.length; i++) {
|
for (let i = 0; i < ws.withdrawn.length; i++) {
|
||||||
if (ws.withdrawn[i]) {
|
if (ws.withdrawn[i]) {
|
||||||
|
@ -43,6 +43,7 @@ import {
|
|||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
} from "./walletTypes";
|
} from "./walletTypes";
|
||||||
|
import { ReserveTransaction } from "./ReserveTransaction";
|
||||||
|
|
||||||
export enum ReserveRecordStatus {
|
export enum ReserveRecordStatus {
|
||||||
/**
|
/**
|
||||||
@ -130,6 +131,7 @@ export function initRetryInfo(
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reserve record as stored in the wallet's database.
|
* A reserve record as stored in the wallet's database.
|
||||||
*/
|
*/
|
||||||
@ -237,6 +239,8 @@ export interface ReserveRecord {
|
|||||||
* (either talking to the bank or the exchange).
|
* (either talking to the bank or the exchange).
|
||||||
*/
|
*/
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationError | undefined;
|
||||||
|
|
||||||
|
reserveTransactions: ReserveTransaction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -449,10 +453,11 @@ export interface ExchangeDetails {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const enum ExchangeUpdateStatus {
|
export const enum ExchangeUpdateStatus {
|
||||||
FETCH_KEYS = "fetch_keys",
|
FetchKeys = "fetch-keys",
|
||||||
FETCH_WIRE = "fetch_wire",
|
FetchWire = "fetch-wire",
|
||||||
FETCH_TERMS = "fetch_terms",
|
FetchTerms = "fetch-terms",
|
||||||
FINISHED = "finished",
|
FinalizeUpdate = "finalize-update",
|
||||||
|
Finished = "finished",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExchangeBankAccount {
|
export interface ExchangeBankAccount {
|
||||||
@ -464,6 +469,12 @@ export interface ExchangeWireInfo {
|
|||||||
accounts: ExchangeBankAccount[];
|
accounts: ExchangeBankAccount[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum ExchangeUpdateReason {
|
||||||
|
Initial = "initial",
|
||||||
|
Forced = "forced",
|
||||||
|
Scheduled = "scheduled",
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exchange record as stored in the wallet's database.
|
* Exchange record as stored in the wallet's database.
|
||||||
*/
|
*/
|
||||||
@ -473,6 +484,11 @@ export interface ExchangeRecord {
|
|||||||
*/
|
*/
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was the exchange added as a built-in exchange?
|
||||||
|
*/
|
||||||
|
builtIn: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Details, once known.
|
* Details, once known.
|
||||||
*/
|
*/
|
||||||
@ -514,7 +530,7 @@ export interface ExchangeRecord {
|
|||||||
*/
|
*/
|
||||||
updateStarted: Timestamp | undefined;
|
updateStarted: Timestamp | undefined;
|
||||||
updateStatus: ExchangeUpdateStatus;
|
updateStatus: ExchangeUpdateStatus;
|
||||||
updateReason?: "initial" | "forced";
|
updateReason?: ExchangeUpdateReason;
|
||||||
|
|
||||||
lastError?: OperationError;
|
lastError?: OperationError;
|
||||||
}
|
}
|
||||||
@ -660,7 +676,7 @@ export interface CoinRecord {
|
|||||||
status: CoinStatus;
|
status: CoinStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProposalStatus {
|
export const enum ProposalStatus {
|
||||||
/**
|
/**
|
||||||
* Not downloaded yet.
|
* Not downloaded yet.
|
||||||
*/
|
*/
|
||||||
@ -777,11 +793,17 @@ export class ProposalRecord {
|
|||||||
*/
|
*/
|
||||||
export interface TipRecord {
|
export interface TipRecord {
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationError | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Has the user accepted the tip? Only after the tip has been accepted coins
|
* Has the user accepted the tip? Only after the tip has been accepted coins
|
||||||
* withdrawn from the tip may be used.
|
* withdrawn from the tip may be used.
|
||||||
*/
|
*/
|
||||||
accepted: boolean;
|
acceptedTimestamp: Timestamp | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has the user rejected the tip?
|
||||||
|
*/
|
||||||
|
rejectedTimestamp: Timestamp | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Have we picked up the tip record from the merchant already?
|
* Have we picked up the tip record from the merchant already?
|
||||||
@ -855,7 +877,7 @@ export interface RefreshGroupRecord {
|
|||||||
|
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationError | undefined;
|
||||||
|
|
||||||
lastErrorPerCoin: (OperationError | undefined)[];
|
lastErrorPerCoin: { [coinIndex: number]: OperationError };
|
||||||
|
|
||||||
refreshGroupId: string;
|
refreshGroupId: string;
|
||||||
|
|
||||||
@ -1066,9 +1088,24 @@ export interface PurchaseRefundState {
|
|||||||
export interface PayEventRecord {
|
export interface PayEventRecord {
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
sessionId: string | undefined;
|
sessionId: string | undefined;
|
||||||
|
isReplay: boolean;
|
||||||
timestamp: Timestamp;
|
timestamp: Timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExchangeUpdatedEventRecord {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
timestamp: Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReserveUpdatedEventRecord {
|
||||||
|
amountReserveBalance: string;
|
||||||
|
amountExpected: string;
|
||||||
|
reservePub: string;
|
||||||
|
timestamp: Timestamp;
|
||||||
|
reserveUpdateId: string;
|
||||||
|
newHistoryTransactions: ReserveTransaction[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record that stores status information about one purchase, starting from when
|
* Record that stores status information about one purchase, starting from when
|
||||||
* the customer accepts a proposal. Includes refund status if applicable.
|
* the customer accepts a proposal. Includes refund status if applicable.
|
||||||
@ -1298,7 +1335,7 @@ export interface WithdrawalSessionRecord {
|
|||||||
* Last error per coin/planchet, or undefined if no error occured for
|
* Last error per coin/planchet, or undefined if no error occured for
|
||||||
* the coin/planchet.
|
* the coin/planchet.
|
||||||
*/
|
*/
|
||||||
lastCoinErrors: (OperationError | undefined)[];
|
lastErrorPerCoin: { [coinIndex: number]: OperationError };
|
||||||
|
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationError | undefined;
|
||||||
}
|
}
|
||||||
@ -1448,6 +1485,18 @@ export namespace Stores {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ExchangeUpdatedEventsStore extends Store<ExchangeUpdatedEventRecord> {
|
||||||
|
constructor() {
|
||||||
|
super("exchangeUpdatedEvents", { keyPath: "exchangeBaseUrl" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReserveUpdatedEventsStore extends Store<ReserveUpdatedEventRecord> {
|
||||||
|
constructor() {
|
||||||
|
super("reserveUpdatedEvents", { keyPath: "reservePub" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class BankWithdrawUrisStore extends Store<BankWithdrawUriRecord> {
|
class BankWithdrawUrisStore extends Store<BankWithdrawUriRecord> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
|
super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
|
||||||
@ -1474,6 +1523,8 @@ export namespace Stores {
|
|||||||
export const bankWithdrawUris = new BankWithdrawUrisStore();
|
export const bankWithdrawUris = new BankWithdrawUrisStore();
|
||||||
export const refundEvents = new RefundEventsStore();
|
export const refundEvents = new RefundEventsStore();
|
||||||
export const payEvents = new PayEventsStore();
|
export const payEvents = new PayEventsStore();
|
||||||
|
export const reserveUpdatedEvents = new ReserveUpdatedEventsStore();
|
||||||
|
export const exchangeUpdatedEvents = new ExchangeUpdatedEventsStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tslint:enable:completed-docs */
|
/* tslint:enable:completed-docs */
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Timestamp, RefreshReason } from "./walletTypes";
|
import { Timestamp, RefreshReason } from "./walletTypes";
|
||||||
|
import { ReserveTransaction } from "./ReserveTransaction";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This file is part of GNU Taler
|
This file is part of GNU Taler
|
||||||
@ -140,10 +141,7 @@ export interface HistoryReserveBalanceUpdatedEvent {
|
|||||||
*/
|
*/
|
||||||
timestamp: Timestamp;
|
timestamp: Timestamp;
|
||||||
|
|
||||||
/**
|
newHistoryTransactions: ReserveTransaction[];
|
||||||
* Unique identifier to query more information about this update.
|
|
||||||
*/
|
|
||||||
reserveUpdateId: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Condensed information about the reserve.
|
* Condensed information about the reserve.
|
||||||
@ -210,13 +208,7 @@ export interface HistoryTipAcceptedEvent {
|
|||||||
/**
|
/**
|
||||||
* Raw amount of the tip, without extra fees that apply.
|
* Raw amount of the tip, without extra fees that apply.
|
||||||
*/
|
*/
|
||||||
tipRawAmount: string;
|
tipRaw: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that the user effectively adds to their balance when
|
|
||||||
* the tip is accepted.
|
|
||||||
*/
|
|
||||||
tipEffectiveAmount: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -238,13 +230,7 @@ export interface HistoryTipDeclinedEvent {
|
|||||||
/**
|
/**
|
||||||
* Raw amount of the tip, without extra fees that apply.
|
* Raw amount of the tip, without extra fees that apply.
|
||||||
*/
|
*/
|
||||||
tipRawAmount: string;
|
tipAmount: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that the user effectively adds to their balance when
|
|
||||||
* the tip is accepted.
|
|
||||||
*/
|
|
||||||
tipEffectiveAmount: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -454,14 +440,7 @@ export interface OrderShortInfo {
|
|||||||
/**
|
/**
|
||||||
* Amount that must be paid for the contract.
|
* Amount that must be paid for the contract.
|
||||||
*/
|
*/
|
||||||
amountRequested: string;
|
amount: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that would be subtracted from the wallet when paying,
|
|
||||||
* includes fees and funds lost due to refreshing or left-over
|
|
||||||
* amounts too small to refresh.
|
|
||||||
*/
|
|
||||||
amountEffective: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Summary of the proposal, given by the merchant.
|
* Summary of the proposal, given by the merchant.
|
||||||
@ -548,7 +527,7 @@ export interface HistoryPaymentSent {
|
|||||||
/**
|
/**
|
||||||
* Type tag.
|
* Type tag.
|
||||||
*/
|
*/
|
||||||
type: HistoryEventType.PaymentAborted;
|
type: HistoryEventType.PaymentSent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Condensed info about the order that we already paid for.
|
* Condensed info about the order that we already paid for.
|
||||||
@ -584,7 +563,7 @@ export interface HistoryRefund {
|
|||||||
* Unique identifier for this refund.
|
* Unique identifier for this refund.
|
||||||
* (Identifies multiple refund permissions that were obtained at once.)
|
* (Identifies multiple refund permissions that were obtained at once.)
|
||||||
*/
|
*/
|
||||||
refundId: string;
|
refundGroupId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Part of the refund that couldn't be applied because
|
* Part of the refund that couldn't be applied because
|
||||||
@ -616,13 +595,22 @@ export interface HistoryRefreshedEvent {
|
|||||||
* Amount that is now available again because it has
|
* Amount that is now available again because it has
|
||||||
* been refreshed.
|
* been refreshed.
|
||||||
*/
|
*/
|
||||||
amountRefreshed: string;
|
amountRefreshedEffective: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that we spent for refreshing.
|
||||||
|
*/
|
||||||
|
amountRefreshedRaw: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Why was the refreshing done?
|
* Why was the refreshing done?
|
||||||
*/
|
*/
|
||||||
refreshReason: RefreshReason;
|
refreshReason: RefreshReason;
|
||||||
|
|
||||||
|
numInputCoins: number;
|
||||||
|
numRefreshedInputCoins: number;
|
||||||
|
numOutputCoins: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifier for a refresh group, contains one or
|
* Identifier for a refresh group, contains one or
|
||||||
* more refresh session IDs.
|
* more refresh session IDs.
|
||||||
|
@ -32,6 +32,7 @@ export const enum PendingOperationType {
|
|||||||
ProposalDownload = "proposal-download",
|
ProposalDownload = "proposal-download",
|
||||||
Refresh = "refresh",
|
Refresh = "refresh",
|
||||||
Reserve = "reserve",
|
Reserve = "reserve",
|
||||||
|
Recoup = "recoup",
|
||||||
RefundApply = "refund-apply",
|
RefundApply = "refund-apply",
|
||||||
RefundQuery = "refund-query",
|
RefundQuery = "refund-query",
|
||||||
TipChoice = "tip-choice",
|
TipChoice = "tip-choice",
|
||||||
@ -53,6 +54,7 @@ export type PendingOperationInfo = PendingOperationInfoCommon &
|
|||||||
| PendingRefundApplyOperation
|
| PendingRefundApplyOperation
|
||||||
| PendingRefundQueryOperation
|
| PendingRefundQueryOperation
|
||||||
| PendingReserveOperation
|
| PendingReserveOperation
|
||||||
|
| PendingTipChoiceOperation
|
||||||
| PendingTipPickupOperation
|
| PendingTipPickupOperation
|
||||||
| PendingWithdrawOperation
|
| PendingWithdrawOperation
|
||||||
);
|
);
|
||||||
@ -115,6 +117,13 @@ export interface PendingTipPickupOperation {
|
|||||||
merchantTipId: string;
|
merchantTipId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PendingTipChoiceOperation {
|
||||||
|
type: PendingOperationType.TipChoice;
|
||||||
|
tipId: string;
|
||||||
|
merchantBaseUrl: string;
|
||||||
|
merchantTipId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PendingPayOperation {
|
export interface PendingPayOperation {
|
||||||
type: PendingOperationType.Pay;
|
type: PendingOperationType.Pay;
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
@ -147,8 +156,18 @@ export interface PendingWithdrawOperation {
|
|||||||
numCoinsTotal: number;
|
numCoinsTotal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PendingOperationFlags {
|
||||||
|
isWaitingUser: boolean;
|
||||||
|
isError: boolean;
|
||||||
|
givesLifeness: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PendingOperationInfoCommon {
|
export interface PendingOperationInfoCommon {
|
||||||
|
/**
|
||||||
|
* Type of the pending operation.
|
||||||
|
*/
|
||||||
type: PendingOperationType;
|
type: PendingOperationType;
|
||||||
|
|
||||||
givesLifeness: boolean;
|
givesLifeness: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -639,28 +639,6 @@ export class ReserveSigSingleton {
|
|||||||
static checked: (obj: any) => ReserveSigSingleton;
|
static checked: (obj: any) => ReserveSigSingleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Response to /reserve/status
|
|
||||||
*/
|
|
||||||
@Checkable.Class()
|
|
||||||
export class ReserveStatus {
|
|
||||||
/**
|
|
||||||
* Reserve signature.
|
|
||||||
*/
|
|
||||||
@Checkable.String()
|
|
||||||
balance: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reserve history, currently not used by the wallet.
|
|
||||||
*/
|
|
||||||
@Checkable.Any()
|
|
||||||
history: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a ReserveSigSingleton from untyped JSON.
|
|
||||||
*/
|
|
||||||
static checked: (obj: any) => ReserveStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response of the merchant
|
* Response of the merchant
|
||||||
@ -942,3 +920,11 @@ export class TipPickupGetResponse {
|
|||||||
*/
|
*/
|
||||||
static checked: (obj: any) => TipPickupGetResponse;
|
static checked: (obj: any) => TipPickupGetResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type AmountString = string;
|
||||||
|
export type Base32String = string;
|
||||||
|
export type EddsaSignatureString = string;
|
||||||
|
export type EddsaPublicKeyString = string;
|
||||||
|
export type CoinPublicKeyString = string;
|
||||||
|
export type TimestampString = string;
|
@ -22,7 +22,6 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { Checkable } from "./checkable";
|
import { Checkable } from "./checkable";
|
||||||
import { objectCodec, numberCodec, stringCodec, Codec } from "./codec";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of fractional units that one value unit represents.
|
* Number of fractional units that one value unit represents.
|
||||||
@ -68,12 +67,6 @@ export class AmountJson {
|
|||||||
static checked: (obj: any) => AmountJson;
|
static checked: (obj: any) => AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
const amountJsonCodec: Codec<AmountJson> = objectCodec<AmountJson>()
|
|
||||||
.property("value", numberCodec)
|
|
||||||
.property("fraction", numberCodec)
|
|
||||||
.property("currency", stringCodec)
|
|
||||||
.build("AmountJson");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of a possibly overflowing operation.
|
* Result of a possibly overflowing operation.
|
||||||
*/
|
*/
|
||||||
|
@ -19,13 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import test from "ava";
|
import test from "ava";
|
||||||
import {
|
import { Codec, makeCodecForObject, makeCodecForConstString, codecForString, makeCodecForUnion } from "./codec";
|
||||||
stringCodec,
|
|
||||||
objectCodec,
|
|
||||||
unionCodec,
|
|
||||||
Codec,
|
|
||||||
stringConstCodec,
|
|
||||||
} from "./codec";
|
|
||||||
|
|
||||||
interface MyObj {
|
interface MyObj {
|
||||||
foo: string;
|
foo: string;
|
||||||
@ -44,8 +38,8 @@ interface AltTwo {
|
|||||||
type MyUnion = AltOne | AltTwo;
|
type MyUnion = AltOne | AltTwo;
|
||||||
|
|
||||||
test("basic codec", t => {
|
test("basic codec", t => {
|
||||||
const myObjCodec = objectCodec<MyObj>()
|
const myObjCodec = makeCodecForObject<MyObj>()
|
||||||
.property("foo", stringCodec)
|
.property("foo", codecForString)
|
||||||
.build("MyObj");
|
.build("MyObj");
|
||||||
const res = myObjCodec.decode({ foo: "hello" });
|
const res = myObjCodec.decode({ foo: "hello" });
|
||||||
t.assert(res.foo === "hello");
|
t.assert(res.foo === "hello");
|
||||||
@ -56,15 +50,15 @@ test("basic codec", t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("union", t => {
|
test("union", t => {
|
||||||
const altOneCodec: Codec<AltOne> = objectCodec<AltOne>()
|
const altOneCodec: Codec<AltOne> = makeCodecForObject<AltOne>()
|
||||||
.property("type", stringConstCodec("one"))
|
.property("type", makeCodecForConstString("one"))
|
||||||
.property("foo", stringCodec)
|
.property("foo", codecForString)
|
||||||
.build("AltOne");
|
.build("AltOne");
|
||||||
const altTwoCodec: Codec<AltTwo> = objectCodec<AltTwo>()
|
const altTwoCodec: Codec<AltTwo> = makeCodecForObject<AltTwo>()
|
||||||
.property("type", stringConstCodec("two"))
|
.property("type", makeCodecForConstString("two"))
|
||||||
.property("bar", stringCodec)
|
.property("bar", codecForString)
|
||||||
.build("AltTwo");
|
.build("AltTwo");
|
||||||
const myUnionCodec: Codec<MyUnion> = unionCodec<MyUnion>()
|
const myUnionCodec: Codec<MyUnion> = makeCodecForUnion<MyUnion>()
|
||||||
.discriminateOn("type")
|
.discriminateOn("type")
|
||||||
.alternative("one", altOneCodec)
|
.alternative("one", altOneCodec)
|
||||||
.alternative("two", altTwoCodec)
|
.alternative("two", altTwoCodec)
|
||||||
|
@ -74,16 +74,16 @@ interface Alternative {
|
|||||||
codec: Codec<any>;
|
codec: Codec<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ObjectCodecBuilder<T, TC> {
|
class ObjectCodecBuilder<OutputType, PartialOutputType> {
|
||||||
private propList: Prop[] = [];
|
private propList: Prop[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a property for the object.
|
* Define a property for the object.
|
||||||
*/
|
*/
|
||||||
property<K extends keyof T & string, V extends T[K]>(
|
property<K extends keyof OutputType & string, V extends OutputType[K]>(
|
||||||
x: K,
|
x: K,
|
||||||
codec: Codec<V>,
|
codec: Codec<V>,
|
||||||
): ObjectCodecBuilder<T, TC & SingletonRecord<K, V>> {
|
): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> {
|
||||||
this.propList.push({ name: x, codec: codec });
|
this.propList.push({ name: x, codec: codec });
|
||||||
return this as any;
|
return this as any;
|
||||||
}
|
}
|
||||||
@ -94,10 +94,10 @@ class ObjectCodecBuilder<T, TC> {
|
|||||||
* @param objectDisplayName name of the object that this codec operates on,
|
* @param objectDisplayName name of the object that this codec operates on,
|
||||||
* used in error messages.
|
* used in error messages.
|
||||||
*/
|
*/
|
||||||
build(objectDisplayName: string): Codec<TC> {
|
build(objectDisplayName: string): Codec<PartialOutputType> {
|
||||||
const propList = this.propList;
|
const propList = this.propList;
|
||||||
return {
|
return {
|
||||||
decode(x: any, c?: Context): TC {
|
decode(x: any, c?: Context): PartialOutputType {
|
||||||
if (!c) {
|
if (!c) {
|
||||||
c = {
|
c = {
|
||||||
path: [`(${objectDisplayName})`],
|
path: [`(${objectDisplayName})`],
|
||||||
@ -112,24 +112,37 @@ class ObjectCodecBuilder<T, TC> {
|
|||||||
);
|
);
|
||||||
obj[prop.name] = propVal;
|
obj[prop.name] = propVal;
|
||||||
}
|
}
|
||||||
return obj as TC;
|
return obj as PartialOutputType;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnionCodecBuilder<T, D extends keyof T, B, TC> {
|
class UnionCodecBuilder<
|
||||||
|
TargetType,
|
||||||
|
TagPropertyLabel extends keyof TargetType,
|
||||||
|
CommonBaseType,
|
||||||
|
PartialTargetType
|
||||||
|
> {
|
||||||
private alternatives = new Map<any, Alternative>();
|
private alternatives = new Map<any, Alternative>();
|
||||||
|
|
||||||
constructor(private discriminator: D, private baseCodec?: Codec<B>) {}
|
constructor(
|
||||||
|
private discriminator: TagPropertyLabel,
|
||||||
|
private baseCodec?: Codec<CommonBaseType>,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a property for the object.
|
* Define a property for the object.
|
||||||
*/
|
*/
|
||||||
alternative<V>(
|
alternative<V>(
|
||||||
tagValue: T[D],
|
tagValue: TargetType[TagPropertyLabel],
|
||||||
codec: Codec<V>,
|
codec: Codec<V>,
|
||||||
): UnionCodecBuilder<T, D, B, TC | V> {
|
): UnionCodecBuilder<
|
||||||
|
TargetType,
|
||||||
|
TagPropertyLabel,
|
||||||
|
CommonBaseType,
|
||||||
|
PartialTargetType | V
|
||||||
|
> {
|
||||||
this.alternatives.set(tagValue, { codec, tagValue });
|
this.alternatives.set(tagValue, { codec, tagValue });
|
||||||
return this as any;
|
return this as any;
|
||||||
}
|
}
|
||||||
@ -140,7 +153,9 @@ class UnionCodecBuilder<T, D extends keyof T, B, TC> {
|
|||||||
* @param objectDisplayName name of the object that this codec operates on,
|
* @param objectDisplayName name of the object that this codec operates on,
|
||||||
* used in error messages.
|
* used in error messages.
|
||||||
*/
|
*/
|
||||||
build<R extends TC & B>(objectDisplayName: string): Codec<R> {
|
build<R extends PartialTargetType & CommonBaseType = never>(
|
||||||
|
objectDisplayName: string,
|
||||||
|
): Codec<R> {
|
||||||
const alternatives = this.alternatives;
|
const alternatives = this.alternatives;
|
||||||
const discriminator = this.discriminator;
|
const discriminator = this.discriminator;
|
||||||
const baseCodec = this.baseCodec;
|
const baseCodec = this.baseCodec;
|
||||||
@ -174,50 +189,50 @@ class UnionCodecBuilder<T, D extends keyof T, B, TC> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export class UnionCodecPreBuilder<T> {
|
||||||
* Return a codec for a value that must be a string.
|
discriminateOn<D extends keyof T, B = {}>(
|
||||||
*/
|
discriminator: D,
|
||||||
export const stringCodec: Codec<string> = {
|
baseCodec?: Codec<B>,
|
||||||
decode(x: any, c?: Context): string {
|
): UnionCodecBuilder<T, D, B, never> {
|
||||||
if (typeof x === "string") {
|
return new UnionCodecBuilder<T, D, B, never>(discriminator, baseCodec);
|
||||||
return x;
|
}
|
||||||
}
|
}
|
||||||
throw new DecodingError(`expected string at ${renderContext(c)}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a codec for a value that must be a string.
|
* Return a builder for a codec that decodes an object with properties.
|
||||||
*/
|
*/
|
||||||
export function stringConstCodec<V extends string>(s: V): Codec<V> {
|
export function makeCodecForObject<T>(): ObjectCodecBuilder<T, {}> {
|
||||||
|
return new ObjectCodecBuilder<T, {}>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeCodecForUnion<T>(): UnionCodecPreBuilder<T> {
|
||||||
|
return new UnionCodecPreBuilder<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a codec for a mapping from a string to values described by the inner codec.
|
||||||
|
*/
|
||||||
|
export function makeCodecForMap<T>(
|
||||||
|
innerCodec: Codec<T>,
|
||||||
|
): Codec<{ [x: string]: T }> {
|
||||||
return {
|
return {
|
||||||
decode(x: any, c?: Context): V {
|
decode(x: any, c?: Context): { [x: string]: T } {
|
||||||
if (x === s) {
|
const map: { [x: string]: T } = {};
|
||||||
return x;
|
if (typeof x !== "object") {
|
||||||
|
throw new DecodingError(`expected object at ${renderContext(c)}`);
|
||||||
}
|
}
|
||||||
throw new DecodingError(
|
for (const i in x) {
|
||||||
`expected string constant "${s}" at ${renderContext(c)}`,
|
map[i] = innerCodec.decode(x[i], joinContext(c, `[${i}]`));
|
||||||
);
|
}
|
||||||
|
return map;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a codec for a value that must be a number.
|
|
||||||
*/
|
|
||||||
export const numberCodec: Codec<number> = {
|
|
||||||
decode(x: any, c?: Context): number {
|
|
||||||
if (typeof x === "number") {
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
throw new DecodingError(`expected number at ${renderContext(c)}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a codec for a list, containing values described by the inner codec.
|
* Return a codec for a list, containing values described by the inner codec.
|
||||||
*/
|
*/
|
||||||
export function listCodec<T>(innerCodec: Codec<T>): Codec<T[]> {
|
export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> {
|
||||||
return {
|
return {
|
||||||
decode(x: any, c?: Context): T[] {
|
decode(x: any, c?: Context): T[] {
|
||||||
const arr: T[] = [];
|
const arr: T[] = [];
|
||||||
@ -233,39 +248,45 @@ export function listCodec<T>(innerCodec: Codec<T>): Codec<T[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a codec for a mapping from a string to values described by the inner codec.
|
* Return a codec for a value that must be a number.
|
||||||
*/
|
*/
|
||||||
export function mapCodec<T>(innerCodec: Codec<T>): Codec<{ [x: string]: T }> {
|
export const codecForNumber: Codec<number> = {
|
||||||
|
decode(x: any, c?: Context): number {
|
||||||
|
if (typeof x === "number") {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
throw new DecodingError(`expected number at ${renderContext(c)}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a codec for a value that must be a string.
|
||||||
|
*/
|
||||||
|
export const codecForString: Codec<string> = {
|
||||||
|
decode(x: any, c?: Context): string {
|
||||||
|
if (typeof x === "string") {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
throw new DecodingError(`expected string at ${renderContext(c)}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a codec for a value that must be a string.
|
||||||
|
*/
|
||||||
|
export function makeCodecForConstString<V extends string>(s: V): Codec<V> {
|
||||||
return {
|
return {
|
||||||
decode(x: any, c?: Context): { [x: string]: T } {
|
decode(x: any, c?: Context): V {
|
||||||
const map: { [x: string]: T } = {};
|
if (x === s) {
|
||||||
if (typeof x !== "object") {
|
return x;
|
||||||
throw new DecodingError(`expected object at ${renderContext(c)}`);
|
|
||||||
}
|
}
|
||||||
for (const i in x) {
|
throw new DecodingError(
|
||||||
map[i] = innerCodec.decode(x[i], joinContext(c, `[${i}]`));
|
`expected string constant "${s}" at ${renderContext(c)}`,
|
||||||
}
|
);
|
||||||
return map;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnionCodecPreBuilder<T> {
|
export function typecheckedCodec<T = undefined>(c: Codec<T>): Codec<T> {
|
||||||
discriminateOn<D extends keyof T, B>(
|
return c;
|
||||||
discriminator: D,
|
|
||||||
baseCodec?: Codec<B>,
|
|
||||||
): UnionCodecBuilder<T, D, B, never> {
|
|
||||||
return new UnionCodecBuilder<T, D, B, never>(discriminator, baseCodec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a builder for a codec that decodes an object with properties.
|
|
||||||
*/
|
|
||||||
export function objectCodec<T>(): ObjectCodecBuilder<T, {}> {
|
|
||||||
return new ObjectCodecBuilder<T, {}>();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unionCodec<T>(): UnionCodecPreBuilder<T> {
|
|
||||||
return new UnionCodecPreBuilder<T>();
|
|
||||||
}
|
}
|
||||||
|
@ -214,3 +214,13 @@ export function strcmp(s1: string, s2: string): number {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a function and return its result.
|
||||||
|
*
|
||||||
|
* Used as a nicer-looking way to do immediately invoked function
|
||||||
|
* expressions (IFFEs).
|
||||||
|
*/
|
||||||
|
export function runBlock<T>(f: () => T) {
|
||||||
|
return f();
|
||||||
|
}
|
@ -176,6 +176,17 @@ class ResultStream<T> {
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async forEachAsync(f: (x: T) => Promise<void>): Promise<void> {
|
||||||
|
while (true) {
|
||||||
|
const x = await this.next();
|
||||||
|
if (x.hasValue) {
|
||||||
|
await f(x.value);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async forEach(f: (x: T) => void): Promise<void> {
|
async forEach(f: (x: T) => void): Promise<void> {
|
||||||
while (true) {
|
while (true) {
|
||||||
const x = await this.next();
|
const x = await this.next();
|
||||||
|
@ -24,9 +24,7 @@
|
|||||||
*/
|
*/
|
||||||
import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
|
import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
|
||||||
import { HttpRequestLibrary } from "./util/http";
|
import { HttpRequestLibrary } from "./util/http";
|
||||||
import {
|
import { Database } from "./util/query";
|
||||||
Database
|
|
||||||
} from "./util/query";
|
|
||||||
|
|
||||||
import { AmountJson } from "./util/amounts";
|
import { AmountJson } from "./util/amounts";
|
||||||
import * as Amounts from "./util/amounts";
|
import * as Amounts from "./util/amounts";
|
||||||
@ -99,10 +97,19 @@ import { payback } from "./operations/payback";
|
|||||||
import { TimerGroup } from "./util/timer";
|
import { TimerGroup } from "./util/timer";
|
||||||
import { AsyncCondition } from "./util/promiseUtils";
|
import { AsyncCondition } from "./util/promiseUtils";
|
||||||
import { AsyncOpMemoSingle } from "./util/asyncMemo";
|
import { AsyncOpMemoSingle } from "./util/asyncMemo";
|
||||||
import { PendingOperationInfo, PendingOperationsResponse, PendingOperationType } from "./types/pending";
|
import {
|
||||||
|
PendingOperationInfo,
|
||||||
|
PendingOperationsResponse,
|
||||||
|
PendingOperationType,
|
||||||
|
} from "./types/pending";
|
||||||
import { WalletNotification, NotificationType } from "./types/notifications";
|
import { WalletNotification, NotificationType } from "./types/notifications";
|
||||||
import { HistoryQuery, HistoryEvent } from "./types/history";
|
import { HistoryQuery, HistoryEvent } from "./types/history";
|
||||||
import { processPurchaseQueryRefund, processPurchaseApplyRefund, getFullRefundFees, applyRefund } from "./operations/refund";
|
import {
|
||||||
|
processPurchaseQueryRefund,
|
||||||
|
processPurchaseApplyRefund,
|
||||||
|
getFullRefundFees,
|
||||||
|
applyRefund,
|
||||||
|
} from "./operations/refund";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wallet protocol version spoken with the exchange
|
* Wallet protocol version spoken with the exchange
|
||||||
@ -184,11 +191,7 @@ export class Wallet {
|
|||||||
await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow);
|
await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow);
|
||||||
break;
|
break;
|
||||||
case PendingOperationType.Refresh:
|
case PendingOperationType.Refresh:
|
||||||
await processRefreshGroup(
|
await processRefreshGroup(this.ws, pending.refreshGroupId, forceNow);
|
||||||
this.ws,
|
|
||||||
pending.refreshGroupId,
|
|
||||||
forceNow,
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case PendingOperationType.Reserve:
|
case PendingOperationType.Reserve:
|
||||||
await processReserve(this.ws, pending.reservePub, forceNow);
|
await processReserve(this.ws, pending.reservePub, forceNow);
|
||||||
@ -203,9 +206,12 @@ export class Wallet {
|
|||||||
case PendingOperationType.ProposalChoice:
|
case PendingOperationType.ProposalChoice:
|
||||||
// Nothing to do, user needs to accept/reject
|
// Nothing to do, user needs to accept/reject
|
||||||
break;
|
break;
|
||||||
case PendingOperationType.ProposalDownload:
|
case PendingOperationType.ProposalDownload:
|
||||||
await processDownloadProposal(this.ws, pending.proposalId, forceNow);
|
await processDownloadProposal(this.ws, pending.proposalId, forceNow);
|
||||||
break;
|
break;
|
||||||
|
case PendingOperationType.TipChoice:
|
||||||
|
// Nothing to do, user needs to accept/reject
|
||||||
|
break;
|
||||||
case PendingOperationType.TipPickup:
|
case PendingOperationType.TipPickup:
|
||||||
await processTip(this.ws, pending.tipId, forceNow);
|
await processTip(this.ws, pending.tipId, forceNow);
|
||||||
break;
|
break;
|
||||||
@ -470,9 +476,16 @@ export class Wallet {
|
|||||||
|
|
||||||
async refresh(oldCoinPub: string): Promise<void> {
|
async refresh(oldCoinPub: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const refreshGroupId = await this.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
|
const refreshGroupId = await this.db.runWithWriteTransaction(
|
||||||
return await createRefreshGroup(tx, [{ coinPub: oldCoinPub }], RefreshReason.Manual);
|
[Stores.refreshGroups],
|
||||||
});
|
async tx => {
|
||||||
|
return await createRefreshGroup(
|
||||||
|
tx,
|
||||||
|
[{ coinPub: oldCoinPub }],
|
||||||
|
RefreshReason.Manual,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
|
await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.latch.trigger();
|
this.latch.trigger();
|
||||||
@ -510,10 +523,9 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
|
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
|
||||||
const denoms = await this.db.iterIndex(
|
const denoms = await this.db
|
||||||
Stores.denominations.exchangeBaseUrlIndex,
|
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
|
||||||
exchangeUrl,
|
.toArray();
|
||||||
).toArray();
|
|
||||||
return denoms;
|
return denoms;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,15 +548,15 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
|
async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
|
||||||
return await this.db.iter(Stores.reserves).filter(
|
return await this.db
|
||||||
r => r.exchangeBaseUrl === exchangeBaseUrl,
|
.iter(Stores.reserves)
|
||||||
);
|
.filter(r => r.exchangeBaseUrl === exchangeBaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
|
async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
|
||||||
return await this.db.iter(Stores.coins).filter(
|
return await this.db
|
||||||
c => c.exchangeBaseUrl === exchangeBaseUrl,
|
.iter(Stores.coins)
|
||||||
);
|
.filter(c => c.exchangeBaseUrl === exchangeBaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCoins(): Promise<CoinRecord[]> {
|
async getCoins(): Promise<CoinRecord[]> {
|
||||||
@ -556,9 +568,7 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPaybackReserves(): Promise<ReserveRecord[]> {
|
async getPaybackReserves(): Promise<ReserveRecord[]> {
|
||||||
return await this.db.iter(Stores.reserves).filter(
|
return await this.db.iter(Stores.reserves).filter(r => r.hasPayback);
|
||||||
r => r.hasPayback,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -691,9 +701,9 @@ export class Wallet {
|
|||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
throw Error("unknown purchase");
|
throw Error("unknown purchase");
|
||||||
}
|
}
|
||||||
const refundsDoneAmounts = Object.values(purchase.refundState.refundsDone).map(x =>
|
const refundsDoneAmounts = Object.values(
|
||||||
Amounts.parseOrThrow(x.perm.refund_amount),
|
purchase.refundState.refundsDone,
|
||||||
);
|
).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
|
||||||
const refundsPendingAmounts = Object.values(
|
const refundsPendingAmounts = Object.values(
|
||||||
purchase.refundState.refundsPending,
|
purchase.refundState.refundsPending,
|
||||||
).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
|
).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
|
||||||
@ -701,12 +711,12 @@ export class Wallet {
|
|||||||
...refundsDoneAmounts,
|
...refundsDoneAmounts,
|
||||||
...refundsPendingAmounts,
|
...refundsPendingAmounts,
|
||||||
]).amount;
|
]).amount;
|
||||||
const refundsDoneFees = Object.values(purchase.refundState.refundsDone).map(x =>
|
const refundsDoneFees = Object.values(
|
||||||
Amounts.parseOrThrow(x.perm.refund_amount),
|
purchase.refundState.refundsDone,
|
||||||
);
|
).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
|
||||||
const refundsPendingFees = Object.values(purchase.refundState.refundsPending).map(x =>
|
const refundsPendingFees = Object.values(
|
||||||
Amounts.parseOrThrow(x.perm.refund_amount),
|
purchase.refundState.refundsPending,
|
||||||
);
|
).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
|
||||||
const totalRefundFees = Amounts.sum([
|
const totalRefundFees = Amounts.sum([
|
||||||
...refundsDoneFees,
|
...refundsDoneFees,
|
||||||
...refundsPendingFees,
|
...refundsPendingFees,
|
||||||
|
@ -60,6 +60,8 @@
|
|||||||
"src/operations/state.ts",
|
"src/operations/state.ts",
|
||||||
"src/operations/tip.ts",
|
"src/operations/tip.ts",
|
||||||
"src/operations/withdraw.ts",
|
"src/operations/withdraw.ts",
|
||||||
|
"src/types/ReserveStatus.ts",
|
||||||
|
"src/types/ReserveTransaction.ts",
|
||||||
"src/types/dbTypes.ts",
|
"src/types/dbTypes.ts",
|
||||||
"src/types/history.ts",
|
"src/types/history.ts",
|
||||||
"src/types/notifications.ts",
|
"src/types/notifications.ts",
|
||||||
|
Loading…
Reference in New Issue
Block a user