aboutsummaryrefslogtreecommitdiff
path: root/src/operations
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-12-16 12:53:22 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-12-16 12:53:22 +0100
commitfa4621e70c48500a372504eb8ae9b9481531c555 (patch)
tree50c457c8c2133dfec32cb465e1b3902ce88fb209 /src/operations
parent1b9c5855a8afb6833ff7a706f5bed5650e1191ad (diff)
history events WIP
Diffstat (limited to 'src/operations')
-rw-r--r--src/operations/exchanges.ts75
-rw-r--r--src/operations/history.ts337
-rw-r--r--src/operations/pay.ts1
-rw-r--r--src/operations/pending.ts18
-rw-r--r--src/operations/refresh.ts2
-rw-r--r--src/operations/refund.ts120
-rw-r--r--src/operations/reserves.ts44
-rw-r--r--src/operations/tip.ts9
-rw-r--r--src/operations/withdraw.ts2
9 files changed, 520 insertions, 88 deletions
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index 6c4c1aa0c..fc1a50f00 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -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);
diff --git a/src/operations/history.ts b/src/operations/history.ts
index 8b225ea07..7e985d218 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -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));
diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index 363688dbd..664524695 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -755,6 +755,7 @@ export async function submitPay(
proposalId,
sessionId,
timestamp: now,
+ isReplay: !isFirst,
};
await tx.put(Stores.payEvents, payEvent);
},
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index b9b2c664e..252c9e98a 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -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,
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index be23a5bb0..d9a080bd8 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -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,
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
index a2b4dbe24..589418571 100644
--- a/src/operations/refund.ts
+++ b/src/operations/refund.ts
@@ -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({
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 559d3ab08..56e9c25d6 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -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,
};
diff --git a/src/operations/tip.ts b/src/operations/tip.ts
index f9953b513..ba4b80974 100644
--- a/src/operations/tip.ts
+++ b/src/operations/tip.ts
@@ -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);
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index a34eec5a1..c7c91494c 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -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]) {