history events WIP

This commit is contained in:
Florian Dold 2019-12-16 12:53:22 +01:00
parent 1b9c5855a8
commit fa4621e70c
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
20 changed files with 795 additions and 278 deletions

View File

@ -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);

View File

@ -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));

View File

@ -755,6 +755,7 @@ export async function submitPay(
proposalId,
sessionId,
timestamp: now,
isReplay: !isFirst,
};
await tx.put(Stores.payEvents, payEvent);
},

View File

@ -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,

View File

@ -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,

View File

@ -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({

View File

@ -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,
};

View File

@ -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);

View File

@ -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]) {

View File

@ -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 */

View File

@ -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.

View File

@ -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;
}

View File

@ -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;

View File

@ -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.
*/

View File

@ -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)

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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();

View File

@ -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,

View File

@ -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",