history events WIP
This commit is contained in:
parent
1b9c5855a8
commit
fa4621e70c
@ -25,15 +25,15 @@ import {
|
||||
DenominationRecord,
|
||||
DenominationStatus,
|
||||
WireFee,
|
||||
ExchangeUpdateReason,
|
||||
ExchangeUpdatedEventRecord,
|
||||
} from "../types/dbTypes";
|
||||
import {
|
||||
canonicalizeBaseUrl,
|
||||
extractTalerStamp,
|
||||
extractTalerStampOrThrow,
|
||||
} from "../util/helpers";
|
||||
import {
|
||||
Database
|
||||
} from "../util/query";
|
||||
import { Database } from "../util/query";
|
||||
import * as Amounts from "../util/amounts";
|
||||
import { parsePaytoUri } from "../util/payto";
|
||||
import {
|
||||
@ -78,7 +78,7 @@ async function setExchangeError(
|
||||
exchange.lastError = err;
|
||||
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,
|
||||
baseUrl: string,
|
||||
): Promise<void> {
|
||||
const existingExchangeRecord = await ws.db.get(
|
||||
Stores.exchanges,
|
||||
baseUrl,
|
||||
);
|
||||
const existingExchangeRecord = await ws.db.get(Stores.exchanges, baseUrl);
|
||||
|
||||
if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FETCH_KEYS) {
|
||||
if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
|
||||
return;
|
||||
}
|
||||
const keysUrl = new URL("keys", baseUrl);
|
||||
@ -194,7 +191,7 @@ async function updateExchangeWithKeys(
|
||||
masterPublicKey: exchangeKeysJson.master_public_key,
|
||||
protocolVersion: protocolVersion,
|
||||
};
|
||||
r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
|
||||
r.updateStatus = ExchangeUpdateStatus.FetchWire;
|
||||
r.lastError = undefined;
|
||||
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(
|
||||
ws: InternalWalletState,
|
||||
exchangeBaseUrl: string,
|
||||
@ -221,7 +250,7 @@ async function updateExchangeWithTermsOfService(
|
||||
if (!exchange) {
|
||||
return;
|
||||
}
|
||||
if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_TERMS) {
|
||||
if (exchange.updateStatus != ExchangeUpdateStatus.FetchTerms) {
|
||||
return;
|
||||
}
|
||||
const reqUrl = new URL("terms", exchangeBaseUrl);
|
||||
@ -243,12 +272,12 @@ async function updateExchangeWithTermsOfService(
|
||||
if (!r) {
|
||||
return;
|
||||
}
|
||||
if (r.updateStatus != ExchangeUpdateStatus.FETCH_TERMS) {
|
||||
if (r.updateStatus != ExchangeUpdateStatus.FetchTerms) {
|
||||
return;
|
||||
}
|
||||
r.termsOfServiceText = tosText;
|
||||
r.termsOfServiceLastEtag = tosEtag;
|
||||
r.updateStatus = ExchangeUpdateStatus.FINISHED;
|
||||
r.updateStatus = ExchangeUpdateStatus.FinalizeUpdate;
|
||||
await tx.put(Stores.exchanges, r);
|
||||
});
|
||||
}
|
||||
@ -282,7 +311,7 @@ async function updateExchangeWithWireInfo(
|
||||
if (!exchange) {
|
||||
return;
|
||||
}
|
||||
if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
|
||||
if (exchange.updateStatus != ExchangeUpdateStatus.FetchWire) {
|
||||
return;
|
||||
}
|
||||
const details = exchange.details;
|
||||
@ -349,14 +378,14 @@ async function updateExchangeWithWireInfo(
|
||||
if (!r) {
|
||||
return;
|
||||
}
|
||||
if (r.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
|
||||
if (r.updateStatus != ExchangeUpdateStatus.FetchWire) {
|
||||
return;
|
||||
}
|
||||
r.wireInfo = {
|
||||
accounts: wireInfo.accounts,
|
||||
feesForType: feesForType,
|
||||
};
|
||||
r.updateStatus = ExchangeUpdateStatus.FETCH_TERMS;
|
||||
r.updateStatus = ExchangeUpdateStatus.FetchTerms;
|
||||
r.lastError = undefined;
|
||||
await tx.put(Stores.exchanges, r);
|
||||
});
|
||||
@ -390,12 +419,13 @@ async function updateExchangeFromUrlImpl(
|
||||
const r = await ws.db.get(Stores.exchanges, baseUrl);
|
||||
if (!r) {
|
||||
const newExchangeRecord: ExchangeRecord = {
|
||||
builtIn: false,
|
||||
baseUrl: baseUrl,
|
||||
details: undefined,
|
||||
wireInfo: undefined,
|
||||
updateStatus: ExchangeUpdateStatus.FETCH_KEYS,
|
||||
updateStatus: ExchangeUpdateStatus.FetchKeys,
|
||||
updateStarted: now,
|
||||
updateReason: "initial",
|
||||
updateReason: ExchangeUpdateReason.Initial,
|
||||
timestampAdded: getTimestampNow(),
|
||||
termsOfServiceAcceptedEtag: undefined,
|
||||
termsOfServiceAcceptedTimestamp: undefined,
|
||||
@ -409,14 +439,14 @@ async function updateExchangeFromUrlImpl(
|
||||
if (!rec) {
|
||||
return;
|
||||
}
|
||||
if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && !forceNow) {
|
||||
if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && !forceNow) {
|
||||
return;
|
||||
}
|
||||
if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && forceNow) {
|
||||
rec.updateReason = "forced";
|
||||
if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && forceNow) {
|
||||
rec.updateReason = ExchangeUpdateReason.Forced;
|
||||
}
|
||||
rec.updateStarted = now;
|
||||
rec.updateStatus = ExchangeUpdateStatus.FETCH_KEYS;
|
||||
rec.updateStatus = ExchangeUpdateStatus.FetchKeys;
|
||||
rec.lastError = undefined;
|
||||
t.put(Stores.exchanges, rec);
|
||||
});
|
||||
@ -425,6 +455,7 @@ async function updateExchangeFromUrlImpl(
|
||||
await updateExchangeWithKeys(ws, baseUrl);
|
||||
await updateExchangeWithWireInfo(ws, baseUrl);
|
||||
await updateExchangeWithTermsOfService(ws, baseUrl);
|
||||
await updateExchangeFinalize(ws, baseUrl);
|
||||
|
||||
const updatedExchange = await ws.db.get(Stores.exchanges, baseUrl);
|
||||
|
||||
|
@ -18,10 +18,132 @@
|
||||
* Imports.
|
||||
*/
|
||||
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 { 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.
|
||||
@ -40,19 +162,222 @@ export async function getHistory(
|
||||
await ws.db.runWithReadTransaction(
|
||||
[
|
||||
Stores.currencies,
|
||||
Stores.coins,
|
||||
Stores.denominations,
|
||||
Stores.exchanges,
|
||||
Stores.exchangeUpdatedEvents,
|
||||
Stores.proposals,
|
||||
Stores.purchases,
|
||||
Stores.refreshGroups,
|
||||
Stores.reserves,
|
||||
Stores.tips,
|
||||
Stores.withdrawalSession,
|
||||
Stores.payEvents,
|
||||
Stores.refundEvents,
|
||||
Stores.reserveUpdatedEvents,
|
||||
],
|
||||
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));
|
||||
|
@ -755,6 +755,7 @@ export async function submitPay(
|
||||
proposalId,
|
||||
sessionId,
|
||||
timestamp: now,
|
||||
isReplay: !isFirst,
|
||||
};
|
||||
await tx.put(Stores.payEvents, payEvent);
|
||||
},
|
||||
|
@ -54,7 +54,7 @@ async function gatherExchangePending(
|
||||
}
|
||||
await tx.iter(Stores.exchanges).forEach(e => {
|
||||
switch (e.updateStatus) {
|
||||
case ExchangeUpdateStatus.FINISHED:
|
||||
case ExchangeUpdateStatus.Finished:
|
||||
if (e.lastError) {
|
||||
resp.pendingOperations.push({
|
||||
type: PendingOperationType.Bug,
|
||||
@ -89,7 +89,7 @@ async function gatherExchangePending(
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ExchangeUpdateStatus.FETCH_KEYS:
|
||||
case ExchangeUpdateStatus.FetchKeys:
|
||||
resp.pendingOperations.push({
|
||||
type: PendingOperationType.ExchangeUpdate,
|
||||
givesLifeness: false,
|
||||
@ -99,7 +99,7 @@ async function gatherExchangePending(
|
||||
reason: e.updateReason || "unknown",
|
||||
});
|
||||
break;
|
||||
case ExchangeUpdateStatus.FETCH_WIRE:
|
||||
case ExchangeUpdateStatus.FetchWire:
|
||||
resp.pendingOperations.push({
|
||||
type: PendingOperationType.ExchangeUpdate,
|
||||
givesLifeness: false,
|
||||
@ -109,6 +109,16 @@ async function gatherExchangePending(
|
||||
reason: e.updateReason || "unknown",
|
||||
});
|
||||
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:
|
||||
resp.pendingOperations.push({
|
||||
type: PendingOperationType.Bug,
|
||||
@ -311,7 +321,7 @@ async function gatherTipPending(
|
||||
if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||
return;
|
||||
}
|
||||
if (tip.accepted) {
|
||||
if (tip.acceptedTimestamp) {
|
||||
resp.pendingOperations.push({
|
||||
type: PendingOperationType.TipPickup,
|
||||
givesLifeness: true,
|
||||
|
@ -548,7 +548,7 @@ export async function createRefreshGroup(
|
||||
finishedTimestamp: undefined,
|
||||
finishedPerCoin: oldCoinPubs.map(x => false),
|
||||
lastError: undefined,
|
||||
lastErrorPerCoin: oldCoinPubs.map(x => undefined),
|
||||
lastErrorPerCoin: {},
|
||||
oldCoinPubs: oldCoinPubs.map(x => x.coinPub),
|
||||
reason,
|
||||
refreshGroupId,
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
OperationError,
|
||||
getTimestampNow,
|
||||
RefreshReason,
|
||||
CoinPublicKey,
|
||||
} from "../types/walletTypes";
|
||||
import {
|
||||
Stores,
|
||||
@ -36,6 +37,7 @@ import {
|
||||
CoinStatus,
|
||||
RefundReason,
|
||||
RefundEventRecord,
|
||||
RefundInfo,
|
||||
} from "../types/dbTypes";
|
||||
import { NotificationType } from "../types/notifications";
|
||||
import { parseRefundUri } from "../util/taleruri";
|
||||
@ -214,13 +216,6 @@ export async function acceptRefundResponse(
|
||||
timestampQueried: now,
|
||||
reason,
|
||||
});
|
||||
|
||||
const refundEvent: RefundEventRecord = {
|
||||
proposalId,
|
||||
refundGroupId,
|
||||
timestamp: now,
|
||||
};
|
||||
await tx.put(Stores.refundEvents, refundEvent);
|
||||
}
|
||||
|
||||
await tx.put(Stores.purchases, p);
|
||||
@ -406,6 +401,9 @@ async function processPurchaseApplyRefundImpl(
|
||||
console.log("no pending refunds");
|
||||
return;
|
||||
}
|
||||
|
||||
const newRefundsDone: { [sig: string]: RefundInfo } = {};
|
||||
const newRefundsFailed: { [sig: string]: RefundInfo } = {};
|
||||
for (const pk of pendingKeys) {
|
||||
const info = purchase.refundState.refundsPending[pk];
|
||||
const perm = info.perm;
|
||||
@ -424,13 +422,13 @@ async function processPurchaseApplyRefundImpl(
|
||||
const reqUrl = new URL("refund", exchangeUrl);
|
||||
const resp = await ws.http.postJson(reqUrl.href, req);
|
||||
console.log("sent refund permission");
|
||||
let refundGone = false;
|
||||
switch (resp.status) {
|
||||
case HttpResponseStatus.Ok:
|
||||
newRefundsDone[pk] = info;
|
||||
break;
|
||||
case HttpResponseStatus.Gone:
|
||||
// We're too late, refund is expired.
|
||||
refundGone = true;
|
||||
newRefundsFailed[pk] = info;
|
||||
break;
|
||||
default:
|
||||
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(
|
||||
[Stores.purchases, Stores.coins, Stores.refreshGroups],
|
||||
async tx => {
|
||||
const p = await tx.get(Stores.purchases, proposalId);
|
||||
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);
|
||||
// Avoid duplicates
|
||||
const refreshCoinsMap: { [coinPub: string]: CoinPublicKey } = {};
|
||||
|
||||
const modCoin = async (perm: MerchantRefundPermission) => {
|
||||
const c = await tx.get(Stores.coins, perm.coin_pub);
|
||||
if (!c) {
|
||||
console.warn("coin not found, can't apply refund");
|
||||
return;
|
||||
}
|
||||
refreshCoinsMap[c.coinPub] = { coinPub: c.coinPub };
|
||||
const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
|
||||
const refundFee = Amounts.parseOrThrow(perm.refund_fee);
|
||||
c.status = CoinStatus.Dormant;
|
||||
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
|
||||
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
|
||||
await tx.put(Stores.coins, c);
|
||||
await createRefreshGroup(
|
||||
tx,
|
||||
[{ coinPub: perm.coin_pub }],
|
||||
RefreshReason.Refund,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (allRefundsProcessed) {
|
||||
ws.notify({
|
||||
type: NotificationType.RefundFinished,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for (const pk of Object.keys(newRefundsFailed)) {
|
||||
const r = newRefundsFailed[pk];
|
||||
groups[r.refundGroupId] = true;
|
||||
delete p.refundState.refundsPending[pk];
|
||||
p.refundState.refundsFailed[pk] = r;
|
||||
await modCoin(r.perm);
|
||||
}
|
||||
|
||||
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({
|
||||
|
@ -31,17 +31,17 @@ import {
|
||||
WithdrawalSessionRecord,
|
||||
initRetryInfo,
|
||||
updateRetryInfoTimeout,
|
||||
ReserveUpdatedEventRecord,
|
||||
} from "../types/dbTypes";
|
||||
import {
|
||||
Database,
|
||||
TransactionAbort,
|
||||
} from "../util/query";
|
||||
import { Logger } from "../util/logging";
|
||||
import * as Amounts from "../util/amounts";
|
||||
import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
|
||||
import { WithdrawOperationStatusResponse, ReserveStatus } from "../types/talerTypes";
|
||||
import { WithdrawOperationStatusResponse } from "../types/talerTypes";
|
||||
import { assertUnreachable } from "../util/assertUnreachable";
|
||||
import { encodeCrock } from "../crypto/talerCrypto";
|
||||
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
||||
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
||||
import {
|
||||
getVerifiedWithdrawDenomList,
|
||||
@ -49,6 +49,7 @@ import {
|
||||
} from "./withdraw";
|
||||
import { guardOperationException, OperationFailedAndReportedError } from "./errors";
|
||||
import { NotificationType } from "../types/notifications";
|
||||
import { codecForReserveStatus } from "../types/ReserveStatus";
|
||||
|
||||
const logger = new Logger("reserves.ts");
|
||||
|
||||
@ -94,6 +95,7 @@ export async function createReserve(
|
||||
lastSuccessfulStatusQuery: undefined,
|
||||
retryInfo: initRetryInfo(),
|
||||
lastError: undefined,
|
||||
reserveTransactions: [],
|
||||
};
|
||||
|
||||
const senderWire = req.senderWire;
|
||||
@ -393,17 +395,35 @@ async function updateReserve(
|
||||
});
|
||||
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);
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newHistoryTransactions = reserveInfo.history.slice(r.reserveTransactions.length);
|
||||
|
||||
const reserveUpdateId = encodeCrock(getRandomBytes(32));
|
||||
|
||||
// FIXME: check / compare history!
|
||||
if (!r.lastSuccessfulStatusQuery) {
|
||||
// FIXME: check if this matches initial expectations
|
||||
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 {
|
||||
const expectedBalance = Amounts.sub(
|
||||
r.withdrawAllocatedAmount,
|
||||
@ -423,11 +443,21 @@ async function updateReserve(
|
||||
} else {
|
||||
// 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.reserveStatus = ReserveRecordStatus.WITHDRAWING;
|
||||
r.retryInfo = initRetryInfo();
|
||||
return r;
|
||||
r.reserveTransactions = reserveInfo.history;
|
||||
await tx.put(Stores.reserves, r);
|
||||
});
|
||||
ws.notify( { type: NotificationType.ReserveUpdated });
|
||||
}
|
||||
@ -561,7 +591,7 @@ async function depleteReserve(
|
||||
planchets: denomsForWithdraw.map(x => undefined),
|
||||
totalCoinValue,
|
||||
retryInfo: initRetryInfo(),
|
||||
lastCoinErrors: denomsForWithdraw.map(x => undefined),
|
||||
lastErrorPerCoin: {},
|
||||
lastError: undefined,
|
||||
};
|
||||
|
||||
|
@ -68,7 +68,8 @@ export async function getTipStatus(
|
||||
|
||||
tipRecord = {
|
||||
tipId,
|
||||
accepted: false,
|
||||
acceptedTimestamp: undefined,
|
||||
rejectedTimestamp: undefined,
|
||||
amount,
|
||||
deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire),
|
||||
exchangeUrl: tipPickupStatus.exchange_url,
|
||||
@ -90,7 +91,7 @@ export async function getTipStatus(
|
||||
}
|
||||
|
||||
const tipStatus: TipStatus = {
|
||||
accepted: !!tipRecord && tipRecord.accepted,
|
||||
accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
|
||||
amount: Amounts.parseOrThrow(tipPickupStatus.amount),
|
||||
amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
|
||||
exchangeUrl: tipPickupStatus.exchange_url,
|
||||
@ -259,7 +260,7 @@ async function processTipImpl(
|
||||
rawWithdrawalAmount: tipRecord.amount,
|
||||
withdrawn: planchets.map((x) => false),
|
||||
totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
|
||||
lastCoinErrors: planchets.map((x) => undefined),
|
||||
lastErrorPerCoin: {},
|
||||
retryInfo: initRetryInfo(),
|
||||
finishTimestamp: undefined,
|
||||
lastError: undefined,
|
||||
@ -296,7 +297,7 @@ export async function acceptTip(
|
||||
return;
|
||||
}
|
||||
|
||||
tipRecord.accepted = true;
|
||||
tipRecord.acceptedTimestamp = getTimestampNow();
|
||||
await ws.db.put(Stores.tips, tipRecord);
|
||||
|
||||
await processTip(ws, tipId);
|
||||
|
@ -272,7 +272,7 @@ async function processPlanchet(
|
||||
return false;
|
||||
}
|
||||
ws.withdrawn[coinIdx] = true;
|
||||
ws.lastCoinErrors[coinIdx] = undefined;
|
||||
delete ws.lastErrorPerCoin[coinIdx];
|
||||
let numDone = 0;
|
||||
for (let i = 0; i < ws.withdrawn.length; i++) {
|
||||
if (ws.withdrawn[i]) {
|
||||
|
@ -43,6 +43,7 @@ import {
|
||||
getTimestampNow,
|
||||
RefreshReason,
|
||||
} from "./walletTypes";
|
||||
import { ReserveTransaction } from "./ReserveTransaction";
|
||||
|
||||
export enum ReserveRecordStatus {
|
||||
/**
|
||||
@ -130,6 +131,7 @@ export function initRetryInfo(
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
lastError: OperationError | undefined;
|
||||
|
||||
reserveTransactions: ReserveTransaction[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -449,10 +453,11 @@ export interface ExchangeDetails {
|
||||
}
|
||||
|
||||
export const enum ExchangeUpdateStatus {
|
||||
FETCH_KEYS = "fetch_keys",
|
||||
FETCH_WIRE = "fetch_wire",
|
||||
FETCH_TERMS = "fetch_terms",
|
||||
FINISHED = "finished",
|
||||
FetchKeys = "fetch-keys",
|
||||
FetchWire = "fetch-wire",
|
||||
FetchTerms = "fetch-terms",
|
||||
FinalizeUpdate = "finalize-update",
|
||||
Finished = "finished",
|
||||
}
|
||||
|
||||
export interface ExchangeBankAccount {
|
||||
@ -464,6 +469,12 @@ export interface ExchangeWireInfo {
|
||||
accounts: ExchangeBankAccount[];
|
||||
}
|
||||
|
||||
export const enum ExchangeUpdateReason {
|
||||
Initial = "initial",
|
||||
Forced = "forced",
|
||||
Scheduled = "scheduled",
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange record as stored in the wallet's database.
|
||||
*/
|
||||
@ -473,6 +484,11 @@ export interface ExchangeRecord {
|
||||
*/
|
||||
baseUrl: string;
|
||||
|
||||
/**
|
||||
* Was the exchange added as a built-in exchange?
|
||||
*/
|
||||
builtIn: boolean;
|
||||
|
||||
/**
|
||||
* Details, once known.
|
||||
*/
|
||||
@ -514,7 +530,7 @@ export interface ExchangeRecord {
|
||||
*/
|
||||
updateStarted: Timestamp | undefined;
|
||||
updateStatus: ExchangeUpdateStatus;
|
||||
updateReason?: "initial" | "forced";
|
||||
updateReason?: ExchangeUpdateReason;
|
||||
|
||||
lastError?: OperationError;
|
||||
}
|
||||
@ -660,7 +676,7 @@ export interface CoinRecord {
|
||||
status: CoinStatus;
|
||||
}
|
||||
|
||||
export enum ProposalStatus {
|
||||
export const enum ProposalStatus {
|
||||
/**
|
||||
* Not downloaded yet.
|
||||
*/
|
||||
@ -777,11 +793,17 @@ export class ProposalRecord {
|
||||
*/
|
||||
export interface TipRecord {
|
||||
lastError: OperationError | undefined;
|
||||
|
||||
/**
|
||||
* Has the user accepted the tip? Only after the tip has been accepted coins
|
||||
* 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?
|
||||
@ -855,7 +877,7 @@ export interface RefreshGroupRecord {
|
||||
|
||||
lastError: OperationError | undefined;
|
||||
|
||||
lastErrorPerCoin: (OperationError | undefined)[];
|
||||
lastErrorPerCoin: { [coinIndex: number]: OperationError };
|
||||
|
||||
refreshGroupId: string;
|
||||
|
||||
@ -1066,9 +1088,24 @@ export interface PurchaseRefundState {
|
||||
export interface PayEventRecord {
|
||||
proposalId: string;
|
||||
sessionId: string | undefined;
|
||||
isReplay: boolean;
|
||||
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
|
||||
* 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
|
||||
* the coin/planchet.
|
||||
*/
|
||||
lastCoinErrors: (OperationError | undefined)[];
|
||||
lastErrorPerCoin: { [coinIndex: number]: OperationError };
|
||||
|
||||
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> {
|
||||
constructor() {
|
||||
super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
|
||||
@ -1474,6 +1523,8 @@ export namespace Stores {
|
||||
export const bankWithdrawUris = new BankWithdrawUrisStore();
|
||||
export const refundEvents = new RefundEventsStore();
|
||||
export const payEvents = new PayEventsStore();
|
||||
export const reserveUpdatedEvents = new ReserveUpdatedEventsStore();
|
||||
export const exchangeUpdatedEvents = new ExchangeUpdatedEventsStore();
|
||||
}
|
||||
|
||||
/* tslint:enable:completed-docs */
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Timestamp, RefreshReason } from "./walletTypes";
|
||||
import { ReserveTransaction } from "./ReserveTransaction";
|
||||
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
@ -140,10 +141,7 @@ export interface HistoryReserveBalanceUpdatedEvent {
|
||||
*/
|
||||
timestamp: Timestamp;
|
||||
|
||||
/**
|
||||
* Unique identifier to query more information about this update.
|
||||
*/
|
||||
reserveUpdateId: string;
|
||||
newHistoryTransactions: ReserveTransaction[];
|
||||
|
||||
/**
|
||||
* Condensed information about the reserve.
|
||||
@ -210,13 +208,7 @@ export interface HistoryTipAcceptedEvent {
|
||||
/**
|
||||
* Raw amount of the tip, without extra fees that apply.
|
||||
*/
|
||||
tipRawAmount: string;
|
||||
|
||||
/**
|
||||
* Amount that the user effectively adds to their balance when
|
||||
* the tip is accepted.
|
||||
*/
|
||||
tipEffectiveAmount: string;
|
||||
tipRaw: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -238,13 +230,7 @@ export interface HistoryTipDeclinedEvent {
|
||||
/**
|
||||
* Raw amount of the tip, without extra fees that apply.
|
||||
*/
|
||||
tipRawAmount: string;
|
||||
|
||||
/**
|
||||
* Amount that the user effectively adds to their balance when
|
||||
* the tip is accepted.
|
||||
*/
|
||||
tipEffectiveAmount: string;
|
||||
tipAmount: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -454,14 +440,7 @@ export interface OrderShortInfo {
|
||||
/**
|
||||
* Amount that must be paid for the contract.
|
||||
*/
|
||||
amountRequested: 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;
|
||||
amount: string;
|
||||
|
||||
/**
|
||||
* Summary of the proposal, given by the merchant.
|
||||
@ -548,7 +527,7 @@ export interface HistoryPaymentSent {
|
||||
/**
|
||||
* Type tag.
|
||||
*/
|
||||
type: HistoryEventType.PaymentAborted;
|
||||
type: HistoryEventType.PaymentSent;
|
||||
|
||||
/**
|
||||
* Condensed info about the order that we already paid for.
|
||||
@ -584,7 +563,7 @@ export interface HistoryRefund {
|
||||
* Unique identifier for this refund.
|
||||
* (Identifies multiple refund permissions that were obtained at once.)
|
||||
*/
|
||||
refundId: string;
|
||||
refundGroupId: string;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* been refreshed.
|
||||
*/
|
||||
amountRefreshed: string;
|
||||
amountRefreshedEffective: string;
|
||||
|
||||
/**
|
||||
* Amount that we spent for refreshing.
|
||||
*/
|
||||
amountRefreshedRaw: string;
|
||||
|
||||
/**
|
||||
* Why was the refreshing done?
|
||||
*/
|
||||
refreshReason: RefreshReason;
|
||||
|
||||
numInputCoins: number;
|
||||
numRefreshedInputCoins: number;
|
||||
numOutputCoins: number;
|
||||
|
||||
/**
|
||||
* Identifier for a refresh group, contains one or
|
||||
* more refresh session IDs.
|
||||
|
@ -32,6 +32,7 @@ export const enum PendingOperationType {
|
||||
ProposalDownload = "proposal-download",
|
||||
Refresh = "refresh",
|
||||
Reserve = "reserve",
|
||||
Recoup = "recoup",
|
||||
RefundApply = "refund-apply",
|
||||
RefundQuery = "refund-query",
|
||||
TipChoice = "tip-choice",
|
||||
@ -53,6 +54,7 @@ export type PendingOperationInfo = PendingOperationInfoCommon &
|
||||
| PendingRefundApplyOperation
|
||||
| PendingRefundQueryOperation
|
||||
| PendingReserveOperation
|
||||
| PendingTipChoiceOperation
|
||||
| PendingTipPickupOperation
|
||||
| PendingWithdrawOperation
|
||||
);
|
||||
@ -115,6 +117,13 @@ export interface PendingTipPickupOperation {
|
||||
merchantTipId: string;
|
||||
}
|
||||
|
||||
export interface PendingTipChoiceOperation {
|
||||
type: PendingOperationType.TipChoice;
|
||||
tipId: string;
|
||||
merchantBaseUrl: string;
|
||||
merchantTipId: string;
|
||||
}
|
||||
|
||||
export interface PendingPayOperation {
|
||||
type: PendingOperationType.Pay;
|
||||
proposalId: string;
|
||||
@ -147,8 +156,18 @@ export interface PendingWithdrawOperation {
|
||||
numCoinsTotal: number;
|
||||
}
|
||||
|
||||
export interface PendingOperationFlags {
|
||||
isWaitingUser: boolean;
|
||||
isError: boolean;
|
||||
givesLifeness: boolean;
|
||||
}
|
||||
|
||||
export interface PendingOperationInfoCommon {
|
||||
/**
|
||||
* Type of the pending operation.
|
||||
*/
|
||||
type: PendingOperationType;
|
||||
|
||||
givesLifeness: boolean;
|
||||
}
|
||||
|
||||
|
@ -639,28 +639,6 @@ export class 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
|
||||
@ -942,3 +920,11 @@ export class 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.
|
||||
*/
|
||||
import { Checkable } from "./checkable";
|
||||
import { objectCodec, numberCodec, stringCodec, Codec } from "./codec";
|
||||
|
||||
/**
|
||||
* Number of fractional units that one value unit represents.
|
||||
@ -68,12 +67,6 @@ export class 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.
|
||||
*/
|
||||
|
@ -19,13 +19,7 @@
|
||||
*/
|
||||
|
||||
import test from "ava";
|
||||
import {
|
||||
stringCodec,
|
||||
objectCodec,
|
||||
unionCodec,
|
||||
Codec,
|
||||
stringConstCodec,
|
||||
} from "./codec";
|
||||
import { Codec, makeCodecForObject, makeCodecForConstString, codecForString, makeCodecForUnion } from "./codec";
|
||||
|
||||
interface MyObj {
|
||||
foo: string;
|
||||
@ -44,8 +38,8 @@ interface AltTwo {
|
||||
type MyUnion = AltOne | AltTwo;
|
||||
|
||||
test("basic codec", t => {
|
||||
const myObjCodec = objectCodec<MyObj>()
|
||||
.property("foo", stringCodec)
|
||||
const myObjCodec = makeCodecForObject<MyObj>()
|
||||
.property("foo", codecForString)
|
||||
.build("MyObj");
|
||||
const res = myObjCodec.decode({ foo: "hello" });
|
||||
t.assert(res.foo === "hello");
|
||||
@ -56,15 +50,15 @@ test("basic codec", t => {
|
||||
});
|
||||
|
||||
test("union", t => {
|
||||
const altOneCodec: Codec<AltOne> = objectCodec<AltOne>()
|
||||
.property("type", stringConstCodec("one"))
|
||||
.property("foo", stringCodec)
|
||||
const altOneCodec: Codec<AltOne> = makeCodecForObject<AltOne>()
|
||||
.property("type", makeCodecForConstString("one"))
|
||||
.property("foo", codecForString)
|
||||
.build("AltOne");
|
||||
const altTwoCodec: Codec<AltTwo> = objectCodec<AltTwo>()
|
||||
.property("type", stringConstCodec("two"))
|
||||
.property("bar", stringCodec)
|
||||
const altTwoCodec: Codec<AltTwo> = makeCodecForObject<AltTwo>()
|
||||
.property("type", makeCodecForConstString("two"))
|
||||
.property("bar", codecForString)
|
||||
.build("AltTwo");
|
||||
const myUnionCodec: Codec<MyUnion> = unionCodec<MyUnion>()
|
||||
const myUnionCodec: Codec<MyUnion> = makeCodecForUnion<MyUnion>()
|
||||
.discriminateOn("type")
|
||||
.alternative("one", altOneCodec)
|
||||
.alternative("two", altTwoCodec)
|
||||
|
@ -74,16 +74,16 @@ interface Alternative {
|
||||
codec: Codec<any>;
|
||||
}
|
||||
|
||||
class ObjectCodecBuilder<T, TC> {
|
||||
class ObjectCodecBuilder<OutputType, PartialOutputType> {
|
||||
private propList: Prop[] = [];
|
||||
|
||||
/**
|
||||
* 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,
|
||||
codec: Codec<V>,
|
||||
): ObjectCodecBuilder<T, TC & SingletonRecord<K, V>> {
|
||||
): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> {
|
||||
this.propList.push({ name: x, codec: codec });
|
||||
return this as any;
|
||||
}
|
||||
@ -94,10 +94,10 @@ class ObjectCodecBuilder<T, TC> {
|
||||
* @param objectDisplayName name of the object that this codec operates on,
|
||||
* used in error messages.
|
||||
*/
|
||||
build(objectDisplayName: string): Codec<TC> {
|
||||
build(objectDisplayName: string): Codec<PartialOutputType> {
|
||||
const propList = this.propList;
|
||||
return {
|
||||
decode(x: any, c?: Context): TC {
|
||||
decode(x: any, c?: Context): PartialOutputType {
|
||||
if (!c) {
|
||||
c = {
|
||||
path: [`(${objectDisplayName})`],
|
||||
@ -112,24 +112,37 @@ class ObjectCodecBuilder<T, TC> {
|
||||
);
|
||||
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>();
|
||||
|
||||
constructor(private discriminator: D, private baseCodec?: Codec<B>) {}
|
||||
constructor(
|
||||
private discriminator: TagPropertyLabel,
|
||||
private baseCodec?: Codec<CommonBaseType>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Define a property for the object.
|
||||
*/
|
||||
alternative<V>(
|
||||
tagValue: T[D],
|
||||
tagValue: TargetType[TagPropertyLabel],
|
||||
codec: Codec<V>,
|
||||
): UnionCodecBuilder<T, D, B, TC | V> {
|
||||
): UnionCodecBuilder<
|
||||
TargetType,
|
||||
TagPropertyLabel,
|
||||
CommonBaseType,
|
||||
PartialTargetType | V
|
||||
> {
|
||||
this.alternatives.set(tagValue, { codec, tagValue });
|
||||
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,
|
||||
* 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 discriminator = this.discriminator;
|
||||
const baseCodec = this.baseCodec;
|
||||
@ -174,50 +189,50 @@ class UnionCodecBuilder<T, D extends keyof T, B, TC> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a codec for a value that must be a string.
|
||||
*/
|
||||
export const stringCodec: Codec<string> = {
|
||||
decode(x: any, c?: Context): string {
|
||||
if (typeof x === "string") {
|
||||
return x;
|
||||
}
|
||||
throw new DecodingError(`expected string at ${renderContext(c)}`);
|
||||
},
|
||||
};
|
||||
export class UnionCodecPreBuilder<T> {
|
||||
discriminateOn<D extends keyof T, B = {}>(
|
||||
discriminator: D,
|
||||
baseCodec?: Codec<B>,
|
||||
): UnionCodecBuilder<T, D, B, never> {
|
||||
return new UnionCodecBuilder<T, D, B, never>(discriminator, baseCodec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
decode(x: any, c?: Context): V {
|
||||
if (x === s) {
|
||||
return x;
|
||||
decode(x: any, c?: Context): { [x: string]: T } {
|
||||
const map: { [x: string]: T } = {};
|
||||
if (typeof x !== "object") {
|
||||
throw new DecodingError(`expected object at ${renderContext(c)}`);
|
||||
}
|
||||
throw new DecodingError(
|
||||
`expected string constant "${s}" at ${renderContext(c)}`,
|
||||
);
|
||||
for (const i in x) {
|
||||
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.
|
||||
*/
|
||||
export function listCodec<T>(innerCodec: Codec<T>): Codec<T[]> {
|
||||
export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> {
|
||||
return {
|
||||
decode(x: any, c?: Context): 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 {
|
||||
decode(x: any, c?: Context): { [x: string]: T } {
|
||||
const map: { [x: string]: T } = {};
|
||||
if (typeof x !== "object") {
|
||||
throw new DecodingError(`expected object at ${renderContext(c)}`);
|
||||
decode(x: any, c?: Context): V {
|
||||
if (x === s) {
|
||||
return x;
|
||||
}
|
||||
for (const i in x) {
|
||||
map[i] = innerCodec.decode(x[i], joinContext(c, `[${i}]`));
|
||||
}
|
||||
return map;
|
||||
throw new DecodingError(
|
||||
`expected string constant "${s}" at ${renderContext(c)}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class UnionCodecPreBuilder<T> {
|
||||
discriminateOn<D extends keyof T, B>(
|
||||
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>();
|
||||
export function typecheckedCodec<T = undefined>(c: Codec<T>): Codec<T> {
|
||||
return c;
|
||||
}
|
||||
|
@ -214,3 +214,13 @@ export function strcmp(s1: string, s2: string): number {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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> {
|
||||
while (true) {
|
||||
const x = await this.next();
|
||||
|
@ -24,9 +24,7 @@
|
||||
*/
|
||||
import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
|
||||
import { HttpRequestLibrary } from "./util/http";
|
||||
import {
|
||||
Database
|
||||
} from "./util/query";
|
||||
import { Database } from "./util/query";
|
||||
|
||||
import { AmountJson } from "./util/amounts";
|
||||
import * as Amounts from "./util/amounts";
|
||||
@ -99,10 +97,19 @@ import { payback } from "./operations/payback";
|
||||
import { TimerGroup } from "./util/timer";
|
||||
import { AsyncCondition } from "./util/promiseUtils";
|
||||
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 { 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
|
||||
@ -184,11 +191,7 @@ export class Wallet {
|
||||
await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow);
|
||||
break;
|
||||
case PendingOperationType.Refresh:
|
||||
await processRefreshGroup(
|
||||
this.ws,
|
||||
pending.refreshGroupId,
|
||||
forceNow,
|
||||
);
|
||||
await processRefreshGroup(this.ws, pending.refreshGroupId, forceNow);
|
||||
break;
|
||||
case PendingOperationType.Reserve:
|
||||
await processReserve(this.ws, pending.reservePub, forceNow);
|
||||
@ -203,9 +206,12 @@ export class Wallet {
|
||||
case PendingOperationType.ProposalChoice:
|
||||
// Nothing to do, user needs to accept/reject
|
||||
break;
|
||||
case PendingOperationType.ProposalDownload:
|
||||
case PendingOperationType.ProposalDownload:
|
||||
await processDownloadProposal(this.ws, pending.proposalId, forceNow);
|
||||
break;
|
||||
case PendingOperationType.TipChoice:
|
||||
// Nothing to do, user needs to accept/reject
|
||||
break;
|
||||
case PendingOperationType.TipPickup:
|
||||
await processTip(this.ws, pending.tipId, forceNow);
|
||||
break;
|
||||
@ -470,9 +476,16 @@ export class Wallet {
|
||||
|
||||
async refresh(oldCoinPub: string): Promise<void> {
|
||||
try {
|
||||
const refreshGroupId = await this.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
|
||||
return await createRefreshGroup(tx, [{ coinPub: oldCoinPub }], RefreshReason.Manual);
|
||||
});
|
||||
const refreshGroupId = await this.db.runWithWriteTransaction(
|
||||
[Stores.refreshGroups],
|
||||
async tx => {
|
||||
return await createRefreshGroup(
|
||||
tx,
|
||||
[{ coinPub: oldCoinPub }],
|
||||
RefreshReason.Manual,
|
||||
);
|
||||
},
|
||||
);
|
||||
await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
|
||||
} catch (e) {
|
||||
this.latch.trigger();
|
||||
@ -510,10 +523,9 @@ export class Wallet {
|
||||
}
|
||||
|
||||
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
|
||||
const denoms = await this.db.iterIndex(
|
||||
Stores.denominations.exchangeBaseUrlIndex,
|
||||
exchangeUrl,
|
||||
).toArray();
|
||||
const denoms = await this.db
|
||||
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
|
||||
.toArray();
|
||||
return denoms;
|
||||
}
|
||||
|
||||
@ -536,15 +548,15 @@ export class Wallet {
|
||||
}
|
||||
|
||||
async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
|
||||
return await this.db.iter(Stores.reserves).filter(
|
||||
r => r.exchangeBaseUrl === exchangeBaseUrl,
|
||||
);
|
||||
return await this.db
|
||||
.iter(Stores.reserves)
|
||||
.filter(r => r.exchangeBaseUrl === exchangeBaseUrl);
|
||||
}
|
||||
|
||||
async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
|
||||
return await this.db.iter(Stores.coins).filter(
|
||||
c => c.exchangeBaseUrl === exchangeBaseUrl,
|
||||
);
|
||||
return await this.db
|
||||
.iter(Stores.coins)
|
||||
.filter(c => c.exchangeBaseUrl === exchangeBaseUrl);
|
||||
}
|
||||
|
||||
async getCoins(): Promise<CoinRecord[]> {
|
||||
@ -556,9 +568,7 @@ export class Wallet {
|
||||
}
|
||||
|
||||
async getPaybackReserves(): Promise<ReserveRecord[]> {
|
||||
return await this.db.iter(Stores.reserves).filter(
|
||||
r => r.hasPayback,
|
||||
);
|
||||
return await this.db.iter(Stores.reserves).filter(r => r.hasPayback);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -691,9 +701,9 @@ export class Wallet {
|
||||
if (!purchase) {
|
||||
throw Error("unknown purchase");
|
||||
}
|
||||
const refundsDoneAmounts = Object.values(purchase.refundState.refundsDone).map(x =>
|
||||
Amounts.parseOrThrow(x.perm.refund_amount),
|
||||
);
|
||||
const refundsDoneAmounts = Object.values(
|
||||
purchase.refundState.refundsDone,
|
||||
).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
|
||||
const refundsPendingAmounts = Object.values(
|
||||
purchase.refundState.refundsPending,
|
||||
).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
|
||||
@ -701,12 +711,12 @@ export class Wallet {
|
||||
...refundsDoneAmounts,
|
||||
...refundsPendingAmounts,
|
||||
]).amount;
|
||||
const refundsDoneFees = Object.values(purchase.refundState.refundsDone).map(x =>
|
||||
Amounts.parseOrThrow(x.perm.refund_amount),
|
||||
);
|
||||
const refundsPendingFees = Object.values(purchase.refundState.refundsPending).map(x =>
|
||||
Amounts.parseOrThrow(x.perm.refund_amount),
|
||||
);
|
||||
const refundsDoneFees = Object.values(
|
||||
purchase.refundState.refundsDone,
|
||||
).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
|
||||
const refundsPendingFees = Object.values(
|
||||
purchase.refundState.refundsPending,
|
||||
).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
|
||||
const totalRefundFees = Amounts.sum([
|
||||
...refundsDoneFees,
|
||||
...refundsPendingFees,
|
||||
|
@ -60,6 +60,8 @@
|
||||
"src/operations/state.ts",
|
||||
"src/operations/tip.ts",
|
||||
"src/operations/withdraw.ts",
|
||||
"src/types/ReserveStatus.ts",
|
||||
"src/types/ReserveTransaction.ts",
|
||||
"src/types/dbTypes.ts",
|
||||
"src/types/history.ts",
|
||||
"src/types/notifications.ts",
|
||||
|
Loading…
Reference in New Issue
Block a user