pending ops / history / notification tweaks

This commit is contained in:
Florian Dold 2019-12-06 02:52:16 +01:00
parent d8170c8159
commit 4159367d8c
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
12 changed files with 302 additions and 148 deletions

View File

@ -31,6 +31,8 @@ import { HttpRequestLibrary, HttpResponse } from "../util/http";
// @ts-ignore: special built-in module
//import akono = require("akono");
export { handleWorkerError, handleWorkerMessage } from "../crypto/workers/nodeThreadWorker";
export class AndroidHttpLib implements HttpRequestLibrary {
useNfcTunnel: boolean = false;

View File

@ -1003,10 +1003,7 @@ export interface PurchaseRecord {
*/
merchantSig: string;
/**
* A successful payment has been made.
*/
payFinished: boolean;
firstSuccessfulPayTimestamp: Timestamp | undefined;
/**
* Pending refunds for the purchase.

View File

@ -32,7 +32,8 @@ import { Bank } from "./bank";
import fs = require("fs");
import { Logger } from "../util/logging";
import { NodeThreadCryptoWorkerFactory } from "../crypto/workers/nodeThreadWorker";
import { NotificationType } from "../walletTypes";
import { NotificationType, WalletNotification } from "../walletTypes";
import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker";
const logger = new Logger("helpers.ts");
@ -87,7 +88,7 @@ export interface DefaultNodeWalletArgs {
/**
* Handler for asynchronous notifications from the wallet.
*/
notifyHandler?: (reason: string) => void;
notifyHandler?: (n: WalletNotification) => void;
/**
* If specified, use this as HTTP request library instead
@ -163,11 +164,15 @@ export async function getDefaultNodeWallet(
const worker = new NodeThreadCryptoWorkerFactory();
return new Wallet(
const w = new Wallet(
myDb,
myHttpLib,
worker,
);
if (args.notifyHandler) {
w.addNotificationListener(args.notifyHandler);
}
return w;
}
export async function withdrawTestBalance(

View File

@ -138,7 +138,7 @@ export async function getBalances(
});
await tx.iter(Stores.purchases).forEach(t => {
if (t.payFinished) {
if (t.firstSuccessfulPayTimestamp) {
return;
}
for (const c of t.payReq.coins) {

View File

@ -14,11 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
/**
* Imports.
*/
import { HistoryQuery, HistoryEvent } from "../walletTypes";
import { oneShotIter } from "../util/query";
import { oneShotIter, runWithReadTransaction } from "../util/query";
import { InternalWalletState } from "./state";
import { Stores, TipRecord } from "../dbTypes";
import * as Amounts from "../util/amounts";
@ -34,139 +34,186 @@ export async function getHistory(
const history: HistoryEvent[] = [];
// FIXME: do pagination instead of generating the full history
// We uniquely identify history rows via their timestamp.
// This works as timestamps are guaranteed to be monotonically
// increasing even
/*
const proposals = await oneShotIter(ws.db, Stores.proposals).toArray();
for (const p of proposals) {
history.push({
detail: {
contractTermsHash: p.contractTermsHash,
merchantName: p.contractTerms.merchant.name,
},
timestamp: p.timestamp,
type: "claim-order",
explicit: false,
});
}
*/
const withdrawals = await oneShotIter(
await runWithReadTransaction(
ws.db,
Stores.withdrawalSession,
).toArray();
for (const w of withdrawals) {
history.push({
detail: {
withdrawalAmount: w.rawWithdrawalAmount,
},
timestamp: w.startTimestamp,
type: "withdraw",
explicit: false,
});
}
const purchases = await oneShotIter(ws.db, Stores.purchases).toArray();
for (const p of purchases) {
history.push({
detail: {
amount: p.contractTerms.amount,
contractTermsHash: p.contractTermsHash,
fulfillmentUrl: p.contractTerms.fulfillment_url,
merchantName: p.contractTerms.merchant.name,
},
timestamp: p.acceptTimestamp,
type: "pay",
explicit: false,
});
if (p.lastRefundStatusTimestamp) {
const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
const amountsPending = Object.keys(p.refundsPending).map(x =>
Amounts.parseOrThrow(p.refundsPending[x].refund_amount),
);
const amountsDone = Object.keys(p.refundsDone).map(x =>
Amounts.parseOrThrow(p.refundsDone[x].refund_amount),
);
const amounts: AmountJson[] = amountsPending.concat(amountsDone);
const amount = Amounts.add(
Amounts.getZero(contractAmount.currency),
...amounts,
).amount;
history.push({
detail: {
contractTermsHash: p.contractTermsHash,
fulfillmentUrl: p.contractTerms.fulfillment_url,
merchantName: p.contractTerms.merchant.name,
refundAmount: amount,
},
timestamp: p.lastRefundStatusTimestamp,
type: "refund",
explicit: false,
[
Stores.currencies,
Stores.coins,
Stores.denominations,
Stores.exchanges,
Stores.proposals,
Stores.purchases,
Stores.refresh,
Stores.reserves,
Stores.tips,
Stores.withdrawalSession,
],
async tx => {
await tx.iter(Stores.proposals).forEach(p => {
history.push({
detail: {},
timestamp: p.timestamp,
type: "claim-order",
explicit: false,
});
});
}
}
const reserves = await oneShotIter(ws.db, Stores.reserves).toArray();
for (const r of reserves) {
const reserveType = r.bankWithdrawStatusUrl ? "taler-bank" : "manual";
history.push({
detail: {
exchangeBaseUrl: r.exchangeBaseUrl,
requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
reservePub: r.reservePub,
reserveType,
bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
},
timestamp: r.created,
type: "reserve-created",
explicit: false,
});
if (r.timestampConfirmed) {
history.push({
detail: {
exchangeBaseUrl: r.exchangeBaseUrl,
requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
reservePub: r.reservePub,
reserveType,
bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
},
timestamp: r.created,
type: "reserve-confirmed",
explicit: false,
await tx.iter(Stores.withdrawalSession).forEach(w => {
history.push({
detail: {
withdrawalAmount: w.rawWithdrawalAmount,
},
timestamp: w.startTimestamp,
type: "withdraw-started",
explicit: false,
});
if (w.finishTimestamp) {
history.push({
detail: {
withdrawalAmount: w.rawWithdrawalAmount,
},
timestamp: w.finishTimestamp,
type: "withdraw-finished",
explicit: false,
});
}
});
}
}
const tips: TipRecord[] = await oneShotIter(ws.db, Stores.tips).toArray();
for (const tip of tips) {
history.push({
detail: {
accepted: tip.accepted,
amount: tip.amount,
merchantBaseUrl: tip.merchantBaseUrl,
tipId: tip.merchantTipId,
},
timestamp: tip.createdTimestamp,
explicit: false,
type: "tip",
});
}
await tx.iter(Stores.purchases).forEach(p => {
history.push({
detail: {
amount: p.contractTerms.amount,
contractTermsHash: p.contractTermsHash,
fulfillmentUrl: p.contractTerms.fulfillment_url,
merchantName: p.contractTerms.merchant.name,
},
timestamp: p.acceptTimestamp,
type: "pay-started",
explicit: false,
});
if (p.firstSuccessfulPayTimestamp) {
history.push({
detail: {
amount: p.contractTerms.amount,
contractTermsHash: p.contractTermsHash,
fulfillmentUrl: p.contractTerms.fulfillment_url,
merchantName: p.contractTerms.merchant.name,
},
timestamp: p.firstSuccessfulPayTimestamp,
type: "pay-finished",
explicit: false,
});
}
if (p.lastRefundStatusTimestamp) {
const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
const amountsPending = Object.keys(p.refundsPending).map(x =>
Amounts.parseOrThrow(p.refundsPending[x].refund_amount),
);
const amountsDone = Object.keys(p.refundsDone).map(x =>
Amounts.parseOrThrow(p.refundsDone[x].refund_amount),
);
const amounts: AmountJson[] = amountsPending.concat(amountsDone);
const amount = Amounts.add(
Amounts.getZero(contractAmount.currency),
...amounts,
).amount;
await oneShotIter(ws.db, Stores.exchanges).forEach(exchange => {
history.push({
type: "exchange-added",
explicit: false,
timestamp: exchange.timestampAdded,
detail: {
exchangeBaseUrl: exchange.baseUrl,
},
});
});
history.push({
detail: {
contractTermsHash: p.contractTermsHash,
fulfillmentUrl: p.contractTerms.fulfillment_url,
merchantName: p.contractTerms.merchant.name,
refundAmount: amount,
},
timestamp: p.lastRefundStatusTimestamp,
type: "refund",
explicit: false,
});
}
});
await tx.iter(Stores.reserves).forEach(r => {
const reserveType = r.bankWithdrawStatusUrl ? "taler-bank" : "manual";
history.push({
detail: {
exchangeBaseUrl: r.exchangeBaseUrl,
requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
reservePub: r.reservePub,
reserveType,
bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
},
timestamp: r.created,
type: "reserve-created",
explicit: false,
});
if (r.timestampConfirmed) {
history.push({
detail: {
exchangeBaseUrl: r.exchangeBaseUrl,
requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
reservePub: r.reservePub,
reserveType,
bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
},
timestamp: r.created,
type: "reserve-confirmed",
explicit: false,
});
}
});
await tx.iter(Stores.tips).forEach(tip => {
history.push({
detail: {
accepted: tip.accepted,
amount: tip.amount,
merchantBaseUrl: tip.merchantBaseUrl,
tipId: tip.merchantTipId,
},
timestamp: tip.createdTimestamp,
explicit: false,
type: "tip",
});
});
await tx.iter(Stores.exchanges).forEach(exchange => {
history.push({
type: "exchange-added",
explicit: false,
timestamp: exchange.timestampAdded,
detail: {
exchangeBaseUrl: exchange.baseUrl,
},
});
});
await tx.iter(Stores.refresh).forEach((r) => {
history.push({
type: "refresh-started",
explicit: false,
timestamp: r.created,
detail: {
refreshSessionId: r.refreshSessionId,
},
});
if (r.finishedTimestamp) {
history.push({
type: "refresh-finished",
explicit: false,
timestamp: r.finishedTimestamp,
detail: {
refreshSessionId: r.refreshSessionId,
},
});
}
});
},
);
history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));

View File

@ -343,7 +343,6 @@ async function recordConfirmPay(
abortRequested: false,
contractTerms: d.contractTerms,
contractTermsHash: d.contractTermsHash,
payFinished: false,
lastSessionId: undefined,
merchantSig: d.merchantSig,
payReq,
@ -359,6 +358,7 @@ async function recordConfirmPay(
refundStatusRequested: false,
lastRefundApplyError: undefined,
refundApplyRetryInfo: initRetryInfo(),
firstSuccessfulPayTimestamp: undefined,
};
await runWithWriteTransaction(
@ -405,7 +405,7 @@ export async function abortFailedPayment(
if (!purchase) {
throw Error("Purchase not found, unable to abort with refund");
}
if (purchase.payFinished) {
if (purchase.firstSuccessfulPayTimestamp) {
throw Error("Purchase already finished, not aborting");
}
if (purchase.abortDone) {
@ -465,6 +465,7 @@ async function incrementProposalRetry(
pr.lastError = err;
await tx.put(Stores.proposals, pr);
});
ws.notify({ type: NotificationType.ProposalOperationError });
}
async function incrementPurchasePayRetry(
@ -486,6 +487,7 @@ async function incrementPurchasePayRetry(
pr.lastPayError = err;
await tx.put(Stores.purchases, pr);
});
ws.notify({ type: NotificationType.PayOperationError });
}
async function incrementPurchaseQueryRefundRetry(
@ -507,6 +509,7 @@ async function incrementPurchaseQueryRefundRetry(
pr.lastRefundStatusError = err;
await tx.put(Stores.purchases, pr);
});
ws.notify({ type: NotificationType.RefundStatusOperationError });
}
async function incrementPurchaseApplyRefundRetry(
@ -528,6 +531,7 @@ async function incrementPurchaseApplyRefundRetry(
pr.lastRefundApplyError = err;
await tx.put(Stores.purchases, pr);
});
ws.notify({ type: NotificationType.RefundApplyOperationError });
}
export async function processDownloadProposal(
@ -698,7 +702,7 @@ export async function submitPay(
// FIXME: properly display error
throw Error("merchant payment signature invalid");
}
purchase.payFinished = true;
purchase.firstSuccessfulPayTimestamp = getTimestampNow();
purchase.lastPayError = undefined;
purchase.payRetryInfo = initRetryInfo(false);
const modifiedCoins: CoinRecord[] = [];
@ -1044,10 +1048,9 @@ async function acceptRefundResponse(
throw Error("empty refund");
}
let numNewRefunds = 0;
await runWithWriteTransaction(ws.db, [Stores.purchases], async (tx) => {
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
const p = await tx.get(Stores.purchases, proposalId);
if (!p) {
console.error("purchase not found, not adding refunds");
@ -1080,6 +1083,9 @@ async function acceptRefundResponse(
await tx.put(Stores.purchases, p);
});
ws.notify({
type: NotificationType.RefundQueried,
});
if (numNewRefunds > 0) {
await processPurchaseApplyRefund(ws, proposalId);
}
@ -1099,7 +1105,6 @@ async function startRefundQuery(
return false;
}
if (p.refundStatusRequested) {
}
p.refundStatusRequested = true;
p.lastRefundStatusError = undefined;
@ -1113,6 +1118,10 @@ async function startRefundQuery(
return;
}
ws.notify({
type: NotificationType.RefundStarted,
});
await processPurchaseQueryRefund(ws, proposalId);
}
@ -1169,7 +1178,7 @@ async function processPurchasePayImpl(
return;
}
logger.trace(`processing purchase pay ${proposalId}`);
if (purchase.payFinished) {
if (purchase.firstSuccessfulPayTimestamp) {
return;
}
await submitPay(ws, proposalId, purchase.lastSessionId);

View File

@ -353,7 +353,7 @@ async function gatherPurchasePending(
onlyDue: boolean = false,
): Promise<void> {
await tx.iter(Stores.purchases).forEach((pr) => {
if (!pr.payFinished) {
if (!pr.firstSuccessfulPayTimestamp) {
resp.nextRetryDelay = updateRetryDelay(
resp.nextRetryDelay,
now,

View File

@ -312,6 +312,7 @@ async function incrementRefreshRetry(
r.lastError = err;
await tx.put(Stores.refresh, r);
});
ws.notify({ type: NotificationType.RefreshOperationError });
}

View File

@ -342,6 +342,7 @@ async function incrementReserveRetry(
r.lastError = err;
await tx.put(Stores.reserves, r);
});
ws.notify({ type: NotificationType.ReserveOperationError });
}
/**
@ -606,6 +607,10 @@ async function depleteReserve(
if (success) {
console.log("processing new withdraw session");
ws.notify({
type: NotificationType.WithdrawSessionCreated,
withdrawSessionId: withdrawalSessionId,
});
await processWithdrawSession(ws, withdrawalSessionId);
} else {
console.trace("withdraw session already existed");

View File

@ -18,7 +18,7 @@
import { oneShotGet, oneShotPut, oneShotMutate, runWithWriteTransaction } from "../util/query";
import { InternalWalletState } from "./state";
import { parseTipUri } from "../util/taleruri";
import { TipStatus, getTimestampNow, OperationError } from "../walletTypes";
import { TipStatus, getTimestampNow, OperationError, NotificationType } from "../walletTypes";
import { TipPickupGetResponse, TipPlanchetDetail, TipResponse } from "../talerTypes";
import * as Amounts from "../util/amounts";
import { Stores, PlanchetRecord, WithdrawalSessionRecord, initRetryInfo, updateRetryInfoTimeout } from "../dbTypes";
@ -122,6 +122,7 @@ async function incrementTipRetry(
t.lastError = err;
await tx.put(Stores.tips, t);
});
ws.notify({ type: NotificationType.TipOperationError });
}
export async function processTip(

View File

@ -260,17 +260,17 @@ async function processPlanchet(
let withdrawSessionFinished = false;
let reserveDepleted = false;
await runWithWriteTransaction(
const success = await runWithWriteTransaction(
ws.db,
[Stores.coins, Stores.withdrawalSession, Stores.reserves],
async tx => {
const ws = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
if (!ws) {
return;
return false;
}
if (ws.withdrawn[coinIdx]) {
// Already withdrawn
return;
return false;
}
ws.withdrawn[coinIdx] = true;
ws.lastCoinErrors[coinIdx] = undefined;
@ -301,9 +301,16 @@ async function processPlanchet(
}
}
await tx.add(Stores.coins, coin);
return true;
},
);
if (success) {
ws.notify( {
type: NotificationType.CoinWithdrawn,
} );
}
if (withdrawSessionFinished) {
ws.notify({
type: NotificationType.WithdrawSessionFinished,
@ -503,6 +510,7 @@ async function incrementWithdrawalRetry(
wsr.lastError = err;
await tx.put(Stores.withdrawalSession, wsr);
});
ws.notify({ type: NotificationType.WithdrawOperationError });
}
export async function processWithdrawSession(

View File

@ -506,6 +506,7 @@ export interface PendingPayOperation {
}
export const enum NotificationType {
CoinWithdrawn = "coin-withdrawn",
ProposalAccepted = "proposal-accepted",
ProposalDownloaded = "proposal-downloaded",
RefundsSubmitted = "refunds-submitted",
@ -518,9 +519,21 @@ export const enum NotificationType {
ReserveUpdated = "reserve-updated",
ReserveConfirmed = "reserve-confirmed",
ReserveDepleted = "reserve-depleted",
WithdrawSessionCreated = "withdraw-session-created",
WithdrawSessionFinished = "withdraw-session-finished",
WaitingForRetry = "waiting-for-retry",
RefundStarted = "refund-started",
RefundQueried = "refund-queried",
RefundFinished = "refund-finished",
ExchangeOperationError = "exchange-operation-error",
RefreshOperationError = "refresh-operation-error",
RefundApplyOperationError = "refund-apply-error",
RefundStatusOperationError = "refund-status-error",
ProposalOperationError = "proposal-error",
TipOperationError = "tip-error",
PayOperationError = "pay-error",
WithdrawOperationError = "withdraw-error",
ReserveOperationError = "reserve-error",
}
export interface ProposalAcceptedNotification {
@ -528,6 +541,18 @@ export interface ProposalAcceptedNotification {
proposalId: string;
}
export interface CoinWithdrawnNotification {
type: NotificationType.CoinWithdrawn;
}
export interface RefundStartedNotification {
type: NotificationType.RefundStarted;
}
export interface RefundQueriedNotification {
type: NotificationType.RefundQueried;
}
export interface ProposalDownloadedNotification {
type: NotificationType.ProposalDownloaded;
proposalId: string;
@ -570,6 +595,11 @@ export interface ReserveConfirmedNotification {
type: NotificationType.ReserveConfirmed;
}
export interface WithdrawSessionCreatedNotification {
type: NotificationType.WithdrawSessionCreated;
withdrawSessionId: string;
}
export interface WithdrawSessionFinishedNotification {
type: NotificationType.WithdrawSessionFinished;
withdrawSessionId: string;
@ -590,7 +620,52 @@ export interface RefundFinishedNotification {
type: NotificationType.RefundFinished;
}
export interface ExchangeOperationErrorNotification {
type: NotificationType.ExchangeOperationError;
}
export interface RefreshOperationErrorNotification {
type: NotificationType.RefreshOperationError;
}
export interface RefundStatusOperationErrorNotification {
type: NotificationType.RefundStatusOperationError;
}
export interface RefundApplyOperationErrorNotification {
type: NotificationType.RefundApplyOperationError;
}
export interface PayOperationErrorNotification {
type: NotificationType.PayOperationError;
}
export interface ProposalOperationErrorNotification {
type: NotificationType.ProposalOperationError;
}
export interface TipOperationErrorNotification {
type: NotificationType.TipOperationError;
}
export interface WithdrawOperationErrorNotification {
type: NotificationType.WithdrawOperationError;
}
export interface ReserveOperationErrorNotification {
type: NotificationType.ReserveOperationError;
}
export type WalletNotification =
| WithdrawOperationErrorNotification
| ReserveOperationErrorNotification
| ExchangeOperationErrorNotification
| RefreshOperationErrorNotification
| RefundStatusOperationErrorNotification
| RefundApplyOperationErrorNotification
| ProposalOperationErrorNotification
| PayOperationErrorNotification
| TipOperationErrorNotification
| ProposalAcceptedNotification
| ProposalDownloadedNotification
| RefundsSubmittedNotification
@ -605,7 +680,11 @@ export type WalletNotification =
| WithdrawSessionFinishedNotification
| ReserveDepletedNotification
| WaitingForRetryNotification
| RefundFinishedNotification;
| RefundStartedNotification
| RefundFinishedNotification
| RefundQueriedNotification
| WithdrawSessionCreatedNotification
| CoinWithdrawnNotification;
export interface OperationError {
type: string;