threads, retries and notifications WIP
This commit is contained in:
parent
829acdd3d9
commit
f67d7f54f9
@ -29,4 +29,28 @@ const walletCli = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export default [walletCli];
|
const walletAndroid = {
|
||||||
|
input: 'dist/node/android/index.js',
|
||||||
|
output: {
|
||||||
|
file: 'dist/standalone/taler-wallet-android.js',
|
||||||
|
format: 'cjs'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
json(),
|
||||||
|
|
||||||
|
nodeResolve({
|
||||||
|
external: builtins,
|
||||||
|
preferBuiltins: true
|
||||||
|
}),
|
||||||
|
|
||||||
|
commonjs({
|
||||||
|
include: ['node_modules/**', 'dist/node/**'],
|
||||||
|
extensions: [ '.js' ],
|
||||||
|
ignoreGlobal: false, // Default: false
|
||||||
|
sourceMap: false,
|
||||||
|
ignore: [ 'taler-wallet' ]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default [walletCli, walletAndroid];
|
||||||
|
@ -125,6 +125,7 @@ export function installAndroidWalletListener() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = msg.id;
|
const id = msg.id;
|
||||||
|
console.log(`android listener: got request for ${operation} (${id})`);
|
||||||
let result;
|
let result;
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case "init": {
|
case "init": {
|
||||||
@ -137,7 +138,7 @@ export function installAndroidWalletListener() {
|
|||||||
};
|
};
|
||||||
const w = await getDefaultNodeWallet(walletArgs);
|
const w = await getDefaultNodeWallet(walletArgs);
|
||||||
maybeWallet = w;
|
maybeWallet = w;
|
||||||
w.runLoopScheduledRetries().catch((e) => {
|
w.runRetryLoop().catch((e) => {
|
||||||
console.error("Error during wallet retry loop", e);
|
console.error("Error during wallet retry loop", e);
|
||||||
});
|
});
|
||||||
wp.resolve(w);
|
wp.resolve(w);
|
||||||
@ -156,7 +157,11 @@ export function installAndroidWalletListener() {
|
|||||||
}
|
}
|
||||||
case "withdrawTestkudos": {
|
case "withdrawTestkudos": {
|
||||||
const wallet = await wp.promise;
|
const wallet = await wp.promise;
|
||||||
await withdrawTestBalance(wallet);
|
try {
|
||||||
|
await withdrawTestBalance(wallet);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error during withdrawTestBalance", e);
|
||||||
|
}
|
||||||
result = {};
|
result = {};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -221,7 +226,7 @@ export function installAndroidWalletListener() {
|
|||||||
maybeWallet = undefined;
|
maybeWallet = undefined;
|
||||||
const w = await getDefaultNodeWallet(walletArgs);
|
const w = await getDefaultNodeWallet(walletArgs);
|
||||||
maybeWallet = w;
|
maybeWallet = w;
|
||||||
w.runLoopScheduledRetries().catch((e) => {
|
w.runRetryLoop().catch((e) => {
|
||||||
console.error("Error during wallet retry loop", e);
|
console.error("Error during wallet retry loop", e);
|
||||||
});
|
});
|
||||||
wp.resolve(w);
|
wp.resolve(w);
|
||||||
@ -233,6 +238,8 @@ export function installAndroidWalletListener() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`android listener: sending response for ${operation} (${id})`);
|
||||||
|
|
||||||
const respMsg = { result, id, operation, type: "response" };
|
const respMsg = { result, id, operation, type: "response" };
|
||||||
sendMessage(JSON.stringify(respMsg));
|
sendMessage(JSON.stringify(respMsg));
|
||||||
};
|
};
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
import { CryptoWorkerFactory } from "./cryptoApi";
|
|
||||||
|
|
||||||
/*
|
|
||||||
This file is part of TALER
|
|
||||||
(C) 2016 GNUnet e.V.
|
|
||||||
|
|
||||||
TALER is free software; you can redistribute it and/or modify it under the
|
|
||||||
terms of the GNU General Public License as published by the Free Software
|
|
||||||
Foundation; either version 3, or (at your option) any later version.
|
|
||||||
|
|
||||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with
|
|
||||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// tslint:disable:no-var-requires
|
|
||||||
|
|
||||||
import { CryptoWorker } from "./cryptoWorker";
|
|
||||||
|
|
||||||
import path = require("path");
|
|
||||||
import child_process = require("child_process");
|
|
||||||
|
|
||||||
const nodeWorkerEntry = path.join(__dirname, "nodeWorkerEntry.js");
|
|
||||||
|
|
||||||
|
|
||||||
export class NodeCryptoWorkerFactory implements CryptoWorkerFactory {
|
|
||||||
startWorker(): CryptoWorker {
|
|
||||||
if (typeof require === "undefined") {
|
|
||||||
throw Error("cannot make worker, require(...) not defined");
|
|
||||||
}
|
|
||||||
const workerCtor = require("./nodeProcessWorker").Worker;
|
|
||||||
const workerPath = __dirname + "/cryptoWorker.js";
|
|
||||||
return new workerCtor(workerPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
getConcurrency(): number {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Worker implementation that uses node subprocesses.
|
|
||||||
*/
|
|
||||||
export class Worker {
|
|
||||||
private child: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to be called when we receive a message from the worker thread.
|
|
||||||
*/
|
|
||||||
onmessage: undefined | ((m: any) => void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to be called when we receive an error from the worker thread.
|
|
||||||
*/
|
|
||||||
onerror: undefined | ((m: any) => void);
|
|
||||||
|
|
||||||
private dispatchMessage(msg: any) {
|
|
||||||
if (this.onmessage) {
|
|
||||||
this.onmessage({ data: msg });
|
|
||||||
} else {
|
|
||||||
console.warn("no handler for worker event 'message' defined")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private dispatchError(msg: any) {
|
|
||||||
if (this.onerror) {
|
|
||||||
this.onerror({ data: msg });
|
|
||||||
} else {
|
|
||||||
console.warn("no handler for worker event 'error' defined")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.child = child_process.fork(nodeWorkerEntry);
|
|
||||||
this.onerror = undefined;
|
|
||||||
this.onmessage = undefined;
|
|
||||||
|
|
||||||
this.child.on("error", (e: any) => {
|
|
||||||
this.dispatchError(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.child.on("message", (msg: any) => {
|
|
||||||
this.dispatchMessage(msg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an event listener for either an "error" or "message" event.
|
|
||||||
*/
|
|
||||||
addEventListener(event: "message" | "error", fn: (x: any) => void): void {
|
|
||||||
switch (event) {
|
|
||||||
case "message":
|
|
||||||
this.onmessage = fn;
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
this.onerror = fn;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a message to the worker thread.
|
|
||||||
*/
|
|
||||||
postMessage (msg: any) {
|
|
||||||
this.child.send(JSON.stringify({data: msg}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forcibly terminate the worker thread.
|
|
||||||
*/
|
|
||||||
terminate () {
|
|
||||||
this.child.kill("SIGINT");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
This file is part of TALER
|
|
||||||
(C) 2016 GNUnet e.V.
|
|
||||||
|
|
||||||
TALER is free software; you can redistribute it and/or modify it under the
|
|
||||||
terms of the GNU General Public License as published by the Free Software
|
|
||||||
Foundation; either version 3, or (at your option) any later version.
|
|
||||||
|
|
||||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with
|
|
||||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
*/
|
|
||||||
|
|
||||||
// tslint:disable:no-var-requires
|
|
||||||
|
|
||||||
import { CryptoImplementation } from "./cryptoImplementation";
|
|
||||||
|
|
||||||
async function handleRequest(operation: string, id: number, args: string[]) {
|
|
||||||
|
|
||||||
const impl = new CryptoImplementation();
|
|
||||||
|
|
||||||
if (!(operation in impl)) {
|
|
||||||
console.error(`crypto operation '${operation}' not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = (impl as any)[operation](...args);
|
|
||||||
if (process.send) {
|
|
||||||
process.send({ result, id });
|
|
||||||
} else {
|
|
||||||
console.error("process.send not available");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("error during operation", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on("message", (msgStr: any) => {
|
|
||||||
const msg = JSON.parse(msgStr);
|
|
||||||
|
|
||||||
const args = msg.data.args;
|
|
||||||
if (!Array.isArray(args)) {
|
|
||||||
console.error("args must be array");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = msg.data.id;
|
|
||||||
if (typeof id !== "number") {
|
|
||||||
console.error("RPC id must be number");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const operation = msg.data.operation;
|
|
||||||
if (typeof operation !== "string") {
|
|
||||||
console.error("RPC operation must be string");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRequest(operation, id, args).catch((e) => {
|
|
||||||
console.error("error in node worker", e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("uncaughtException", (err: any) => {
|
|
||||||
console.error("uncaught exception in node worker entry", err);
|
|
||||||
});
|
|
@ -22,7 +22,7 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { AmountJson } from "../util/amounts";
|
import { AmountJson } from "../../util/amounts";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
@ -30,15 +30,21 @@ import {
|
|||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
TipPlanchet,
|
TipPlanchet,
|
||||||
WireFee,
|
WireFee,
|
||||||
} from "../dbTypes";
|
} from "../../dbTypes";
|
||||||
|
|
||||||
import { CryptoWorker } from "./cryptoWorker";
|
import { CryptoWorker } from "./cryptoWorker";
|
||||||
|
|
||||||
import { ContractTerms, PaybackRequest } from "../talerTypes";
|
import { ContractTerms, PaybackRequest } from "../../talerTypes";
|
||||||
|
|
||||||
import { BenchmarkResult, CoinWithDenom, PayCoinInfo, PlanchetCreationResult, PlanchetCreationRequest } from "../walletTypes";
|
import {
|
||||||
|
BenchmarkResult,
|
||||||
|
CoinWithDenom,
|
||||||
|
PayCoinInfo,
|
||||||
|
PlanchetCreationResult,
|
||||||
|
PlanchetCreationRequest,
|
||||||
|
} from "../../walletTypes";
|
||||||
|
|
||||||
import * as timer from "../util/timer";
|
import * as timer from "../../util/timer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State of a crypto worker.
|
* State of a crypto worker.
|
||||||
@ -172,7 +178,8 @@ export class CryptoApi {
|
|||||||
wake(ws: WorkerState, work: WorkItem): void {
|
wake(ws: WorkerState, work: WorkItem): void {
|
||||||
if (this.stopped) {
|
if (this.stopped) {
|
||||||
console.log("cryptoApi is stopped");
|
console.log("cryptoApi is stopped");
|
||||||
CryptoApi.enableTracing && console.log("not waking, as cryptoApi is stopped");
|
CryptoApi.enableTracing &&
|
||||||
|
console.log("not waking, as cryptoApi is stopped");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ws.currentWorkItem !== null) {
|
if (ws.currentWorkItem !== null) {
|
||||||
@ -333,7 +340,7 @@ export class CryptoApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createPlanchet(
|
createPlanchet(
|
||||||
req: PlanchetCreationRequest
|
req: PlanchetCreationRequest,
|
||||||
): Promise<PlanchetCreationResult> {
|
): Promise<PlanchetCreationResult> {
|
||||||
return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req);
|
return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req);
|
||||||
}
|
}
|
||||||
@ -398,6 +405,10 @@ export class CryptoApi {
|
|||||||
return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk);
|
return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rsaVerify(hm: string, sig: string, pk: string): Promise<boolean> {
|
||||||
|
return this.doRpc<boolean>("rsaVerify", 4, hm, sig, pk);
|
||||||
|
}
|
||||||
|
|
||||||
createPaybackRequest(coin: CoinRecord): Promise<PaybackRequest> {
|
createPaybackRequest(coin: CoinRecord): Promise<PaybackRequest> {
|
||||||
return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin);
|
return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin);
|
||||||
}
|
}
|
@ -30,12 +30,12 @@ import {
|
|||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
RefreshPlanchetRecord,
|
RefreshPlanchetRecord,
|
||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
ReserveRecord,
|
|
||||||
TipPlanchet,
|
TipPlanchet,
|
||||||
WireFee,
|
WireFee,
|
||||||
} from "../dbTypes";
|
initRetryInfo,
|
||||||
|
} from "../../dbTypes";
|
||||||
|
|
||||||
import { CoinPaySig, ContractTerms, PaybackRequest } from "../talerTypes";
|
import { CoinPaySig, ContractTerms, PaybackRequest } from "../../talerTypes";
|
||||||
import {
|
import {
|
||||||
BenchmarkResult,
|
BenchmarkResult,
|
||||||
CoinWithDenom,
|
CoinWithDenom,
|
||||||
@ -43,11 +43,12 @@ import {
|
|||||||
Timestamp,
|
Timestamp,
|
||||||
PlanchetCreationResult,
|
PlanchetCreationResult,
|
||||||
PlanchetCreationRequest,
|
PlanchetCreationRequest,
|
||||||
} from "../walletTypes";
|
getTimestampNow,
|
||||||
import { canonicalJson, getTalerStampSec } from "../util/helpers";
|
} from "../../walletTypes";
|
||||||
import { AmountJson } from "../util/amounts";
|
import { canonicalJson, getTalerStampSec } from "../../util/helpers";
|
||||||
import * as Amounts from "../util/amounts";
|
import { AmountJson } from "../../util/amounts";
|
||||||
import * as timer from "../util/timer";
|
import * as Amounts from "../../util/amounts";
|
||||||
|
import * as timer from "../../util/timer";
|
||||||
import {
|
import {
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
@ -64,8 +65,9 @@ import {
|
|||||||
createEcdheKeyPair,
|
createEcdheKeyPair,
|
||||||
keyExchangeEcdheEddsa,
|
keyExchangeEcdheEddsa,
|
||||||
setupRefreshPlanchet,
|
setupRefreshPlanchet,
|
||||||
} from "./talerCrypto";
|
rsaVerify,
|
||||||
import { randomBytes } from "./primitives/nacl-fast";
|
} from "../talerCrypto";
|
||||||
|
import { randomBytes } from "../primitives/nacl-fast";
|
||||||
|
|
||||||
enum SignaturePurpose {
|
enum SignaturePurpose {
|
||||||
RESERVE_WITHDRAW = 1200,
|
RESERVE_WITHDRAW = 1200,
|
||||||
@ -304,15 +306,22 @@ export class CryptoImplementation {
|
|||||||
/**
|
/**
|
||||||
* Unblind a blindly signed value.
|
* Unblind a blindly signed value.
|
||||||
*/
|
*/
|
||||||
rsaUnblind(sig: string, bk: string, pk: string): string {
|
rsaUnblind(blindedSig: string, bk: string, pk: string): string {
|
||||||
const denomSig = rsaUnblind(
|
const denomSig = rsaUnblind(
|
||||||
decodeCrock(sig),
|
decodeCrock(blindedSig),
|
||||||
decodeCrock(pk),
|
decodeCrock(pk),
|
||||||
decodeCrock(bk),
|
decodeCrock(bk),
|
||||||
);
|
);
|
||||||
return encodeCrock(denomSig);
|
return encodeCrock(denomSig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unblind a blindly signed value.
|
||||||
|
*/
|
||||||
|
rsaVerify(hm: string, sig: string, pk: string): boolean {
|
||||||
|
return rsaVerify(hash(decodeCrock(hm)), decodeCrock(sig), decodeCrock(pk));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate updated coins (to store in the database)
|
* Generate updated coins (to store in the database)
|
||||||
* and deposit permissions for each given coin.
|
* and deposit permissions for each given coin.
|
||||||
@ -488,7 +497,6 @@ export class CryptoImplementation {
|
|||||||
refreshSessionId,
|
refreshSessionId,
|
||||||
confirmSig: encodeCrock(confirmSig),
|
confirmSig: encodeCrock(confirmSig),
|
||||||
exchangeBaseUrl,
|
exchangeBaseUrl,
|
||||||
finished: false,
|
|
||||||
hash: encodeCrock(sessionHash),
|
hash: encodeCrock(sessionHash),
|
||||||
meltCoinPub: meltCoin.coinPub,
|
meltCoinPub: meltCoin.coinPub,
|
||||||
newDenomHashes: newCoinDenoms.map(d => d.denomPubHash),
|
newDenomHashes: newCoinDenoms.map(d => d.denomPubHash),
|
||||||
@ -499,6 +507,10 @@ export class CryptoImplementation {
|
|||||||
transferPubs,
|
transferPubs,
|
||||||
valueOutput,
|
valueOutput,
|
||||||
valueWithFee,
|
valueWithFee,
|
||||||
|
created: getTimestampNow(),
|
||||||
|
retryInfo: initRetryInfo(),
|
||||||
|
finishedTimestamp: undefined,
|
||||||
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return refreshSession;
|
return refreshSession;
|
@ -3,6 +3,6 @@ export interface CryptoWorker {
|
|||||||
|
|
||||||
terminate(): void;
|
terminate(): void;
|
||||||
|
|
||||||
onmessage: (m: any) => void;
|
onmessage: ((m: any) => void) | undefined;
|
||||||
onerror: (m: any) => void;
|
onerror: ((m: any) => void) | undefined;
|
||||||
}
|
}
|
175
src/crypto/workers/nodeThreadWorker.ts
Normal file
175
src/crypto/workers/nodeThreadWorker.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import { CryptoWorkerFactory } from "./cryptoApi";
|
||||||
|
|
||||||
|
/*
|
||||||
|
This file is part of TALER
|
||||||
|
(C) 2016 GNUnet e.V.
|
||||||
|
|
||||||
|
TALER is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
// tslint:disable:no-var-requires
|
||||||
|
|
||||||
|
import { CryptoWorker } from "./cryptoWorker";
|
||||||
|
|
||||||
|
import worker_threads = require("worker_threads");
|
||||||
|
import os = require("os");
|
||||||
|
import { CryptoImplementation } from "./cryptoImplementation";
|
||||||
|
|
||||||
|
const f = __filename;
|
||||||
|
|
||||||
|
const workerCode = `
|
||||||
|
const worker_threads = require('worker_threads');
|
||||||
|
const parentPort = worker_threads.parentPort;
|
||||||
|
let tw;
|
||||||
|
try {
|
||||||
|
tw = require("${f}");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("could not load from ${f}");
|
||||||
|
}
|
||||||
|
if (!tw) {
|
||||||
|
try {
|
||||||
|
tw = require("taler-wallet-android");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("could not load taler-wallet-android either");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parentPort.on("message", tw.handleWorkerMessage);
|
||||||
|
parentPort.on("error", tw.handleWorkerError);
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is executed in the worker thread to handle
|
||||||
|
* a message.
|
||||||
|
*/
|
||||||
|
export function handleWorkerMessage(msg: any) {
|
||||||
|
const args = msg.args;
|
||||||
|
if (!Array.isArray(args)) {
|
||||||
|
console.error("args must be array");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = msg.id;
|
||||||
|
if (typeof id !== "number") {
|
||||||
|
console.error("RPC id must be number");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const operation = msg.operation;
|
||||||
|
if (typeof operation !== "string") {
|
||||||
|
console.error("RPC operation must be string");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRequest = async () => {
|
||||||
|
const impl = new CryptoImplementation();
|
||||||
|
|
||||||
|
if (!(operation in impl)) {
|
||||||
|
console.error(`crypto operation '${operation}' not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = (impl as any)[operation](...args);
|
||||||
|
const p = worker_threads.parentPort;
|
||||||
|
worker_threads.parentPort?.postMessage;
|
||||||
|
if (p) {
|
||||||
|
p.postMessage({ data: { result, id } });
|
||||||
|
} else {
|
||||||
|
console.error("parent port not available (not running in thread?");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("error during operation", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRequest().catch(e => {
|
||||||
|
console.error("error in node worker", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleWorkerError(e: Error) {
|
||||||
|
console.log("got error from worker", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NodeThreadCryptoWorkerFactory implements CryptoWorkerFactory {
|
||||||
|
startWorker(): CryptoWorker {
|
||||||
|
if (typeof require === "undefined") {
|
||||||
|
throw Error("cannot make worker, require(...) not defined");
|
||||||
|
}
|
||||||
|
return new NodeThreadCryptoWorker();
|
||||||
|
}
|
||||||
|
|
||||||
|
getConcurrency(): number {
|
||||||
|
return Math.max(1, os.cpus().length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker implementation that uses node subprocesses.
|
||||||
|
*/
|
||||||
|
class NodeThreadCryptoWorker implements CryptoWorker {
|
||||||
|
/**
|
||||||
|
* Function to be called when we receive a message from the worker thread.
|
||||||
|
*/
|
||||||
|
onmessage: undefined | ((m: any) => void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be called when we receive an error from the worker thread.
|
||||||
|
*/
|
||||||
|
onerror: undefined | ((m: any) => void);
|
||||||
|
|
||||||
|
private nodeWorker: worker_threads.Worker;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.nodeWorker = new worker_threads.Worker(workerCode, { eval: true });
|
||||||
|
this.nodeWorker.on("error", (err: Error) => {
|
||||||
|
console.error("error in node worker:", err);
|
||||||
|
if (this.onerror) {
|
||||||
|
this.onerror(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.nodeWorker.on("message", (v: any) => {
|
||||||
|
if (this.onmessage) {
|
||||||
|
this.onmessage(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.nodeWorker.unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an event listener for either an "error" or "message" event.
|
||||||
|
*/
|
||||||
|
addEventListener(event: "message" | "error", fn: (x: any) => void): void {
|
||||||
|
switch (event) {
|
||||||
|
case "message":
|
||||||
|
this.onmessage = fn;
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
this.onerror = fn;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to the worker thread.
|
||||||
|
*/
|
||||||
|
postMessage(msg: any) {
|
||||||
|
this.nodeWorker.postMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcibly terminate the worker thread.
|
||||||
|
*/
|
||||||
|
terminate() {
|
||||||
|
this.nodeWorker.terminate();
|
||||||
|
}
|
||||||
|
}
|
166
src/dbTypes.ts
166
src/dbTypes.ts
@ -36,7 +36,12 @@ import {
|
|||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
|
|
||||||
import { Index, Store } from "./util/query";
|
import { Index, Store } from "./util/query";
|
||||||
import { Timestamp, OperationError } from "./walletTypes";
|
import {
|
||||||
|
Timestamp,
|
||||||
|
OperationError,
|
||||||
|
Duration,
|
||||||
|
getTimestampNow,
|
||||||
|
} from "./walletTypes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current database version, should be incremented
|
* Current database version, should be incremented
|
||||||
@ -83,6 +88,55 @@ export enum ReserveRecordStatus {
|
|||||||
DORMANT = "dormant",
|
DORMANT = "dormant",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RetryInfo {
|
||||||
|
firstTry: Timestamp;
|
||||||
|
nextRetry: Timestamp;
|
||||||
|
retryCounter: number;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RetryPolicy {
|
||||||
|
readonly backoffDelta: Duration;
|
||||||
|
readonly backoffBase: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultRetryPolicy: RetryPolicy = {
|
||||||
|
backoffBase: 1.5,
|
||||||
|
backoffDelta: { d_ms: 200 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function updateRetryInfoTimeout(
|
||||||
|
r: RetryInfo,
|
||||||
|
p: RetryPolicy = defaultRetryPolicy,
|
||||||
|
): void {
|
||||||
|
const now = getTimestampNow();
|
||||||
|
const t =
|
||||||
|
now.t_ms + p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
|
||||||
|
r.nextRetry = { t_ms: t };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initRetryInfo(
|
||||||
|
active: boolean = true,
|
||||||
|
p: RetryPolicy = defaultRetryPolicy,
|
||||||
|
): RetryInfo {
|
||||||
|
if (!active) {
|
||||||
|
return {
|
||||||
|
active: false,
|
||||||
|
firstTry: { t_ms: Number.MAX_SAFE_INTEGER },
|
||||||
|
nextRetry: { t_ms: Number.MAX_SAFE_INTEGER },
|
||||||
|
retryCounter: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const info = {
|
||||||
|
firstTry: getTimestampNow(),
|
||||||
|
active: true,
|
||||||
|
nextRetry: { t_ms: 0 },
|
||||||
|
retryCounter: 0,
|
||||||
|
};
|
||||||
|
updateRetryInfoTimeout(info, p);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reserve record as stored in the wallet's database.
|
* A reserve record as stored in the wallet's database.
|
||||||
*/
|
*/
|
||||||
@ -176,9 +230,20 @@ export interface ReserveRecord {
|
|||||||
/**
|
/**
|
||||||
* Time of the last successful status query.
|
* Time of the last successful status query.
|
||||||
*/
|
*/
|
||||||
lastStatusQuery: Timestamp | undefined;
|
lastSuccessfulStatusQuery: Timestamp | undefined;
|
||||||
|
|
||||||
lastError?: OperationError;
|
/**
|
||||||
|
* Retry info. This field is present even if no retry is scheduled,
|
||||||
|
* because we need it to be present for the index on the object store
|
||||||
|
* to work.
|
||||||
|
*/
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last error that happened in a reserve operation
|
||||||
|
* (either talking to the bank or the exchange).
|
||||||
|
*/
|
||||||
|
lastError: OperationError | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -682,17 +747,26 @@ export class ProposalRecord {
|
|||||||
@Checkable.Optional(Checkable.String())
|
@Checkable.Optional(Checkable.String())
|
||||||
downloadSessionId?: string;
|
downloadSessionId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry info, even present when the operation isn't active to allow indexing
|
||||||
|
* on the next retry timestamp.
|
||||||
|
*/
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that a value matches the schema of this class and convert it into a
|
* Verify that a value matches the schema of this class and convert it into a
|
||||||
* member.
|
* member.
|
||||||
*/
|
*/
|
||||||
static checked: (obj: any) => ProposalRecord;
|
static checked: (obj: any) => ProposalRecord;
|
||||||
|
|
||||||
|
lastError: OperationError | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of a tip we got from a merchant.
|
* Status of a tip we got from a merchant.
|
||||||
*/
|
*/
|
||||||
export interface TipRecord {
|
export interface TipRecord {
|
||||||
|
lastError: OperationError | undefined;
|
||||||
/**
|
/**
|
||||||
* Has the user accepted the tip? Only after the tip has been accepted coins
|
* Has the user accepted the tip? Only after the tip has been accepted coins
|
||||||
* withdrawn from the tip may be used.
|
* withdrawn from the tip may be used.
|
||||||
@ -753,13 +827,21 @@ export interface TipRecord {
|
|||||||
*/
|
*/
|
||||||
nextUrl?: string;
|
nextUrl?: string;
|
||||||
|
|
||||||
timestamp: Timestamp;
|
createdTimestamp: Timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry info, even present when the operation isn't active to allow indexing
|
||||||
|
* on the next retry timestamp.
|
||||||
|
*/
|
||||||
|
retryInfo: RetryInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ongoing refresh
|
* Ongoing refresh
|
||||||
*/
|
*/
|
||||||
export interface RefreshSessionRecord {
|
export interface RefreshSessionRecord {
|
||||||
|
lastError: OperationError | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public key that's being melted in this session.
|
* Public key that's being melted in this session.
|
||||||
*/
|
*/
|
||||||
@ -823,14 +905,25 @@ export interface RefreshSessionRecord {
|
|||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this session finished?
|
* Timestamp when the refresh session finished.
|
||||||
*/
|
*/
|
||||||
finished: boolean;
|
finishedTimestamp: Timestamp | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A 32-byte base32-crockford encoded random identifier.
|
* A 32-byte base32-crockford encoded random identifier.
|
||||||
*/
|
*/
|
||||||
refreshSessionId: string;
|
refreshSessionId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When has this refresh session been created?
|
||||||
|
*/
|
||||||
|
created: Timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry info, even present when the operation isn't active to allow indexing
|
||||||
|
* on the next retry timestamp.
|
||||||
|
*/
|
||||||
|
retryInfo: RetryInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -877,11 +970,35 @@ export interface WireFee {
|
|||||||
sig: string;
|
sig: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PurchaseStatus {
|
||||||
|
/**
|
||||||
|
* We're currently paying, either for the first
|
||||||
|
* time or as a re-play potentially with a different
|
||||||
|
* session ID.
|
||||||
|
*/
|
||||||
|
SubmitPay = "submit-pay",
|
||||||
|
QueryRefund = "query-refund",
|
||||||
|
ProcessRefund = "process-refund",
|
||||||
|
Abort = "abort",
|
||||||
|
Done = "done",
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record that stores status information about one purchase, starting from when
|
* Record that stores status information about one purchase, starting from when
|
||||||
* the customer accepts a proposal. Includes refund status if applicable.
|
* the customer accepts a proposal. Includes refund status if applicable.
|
||||||
*/
|
*/
|
||||||
export interface PurchaseRecord {
|
export interface PurchaseRecord {
|
||||||
|
/**
|
||||||
|
* Proposal ID for this purchase. Uniquely identifies the
|
||||||
|
* purchase and the proposal.
|
||||||
|
*/
|
||||||
|
proposalId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of this purchase.
|
||||||
|
*/
|
||||||
|
status: PurchaseStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash of the contract terms.
|
* Hash of the contract terms.
|
||||||
*/
|
*/
|
||||||
@ -923,13 +1040,13 @@ export interface PurchaseRecord {
|
|||||||
* When was the purchase made?
|
* When was the purchase made?
|
||||||
* Refers to the time that the user accepted.
|
* Refers to the time that the user accepted.
|
||||||
*/
|
*/
|
||||||
timestamp: Timestamp;
|
acceptTimestamp: Timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When was the last refund made?
|
* When was the last refund made?
|
||||||
* Set to 0 if no refund was made on the purchase.
|
* Set to 0 if no refund was made on the purchase.
|
||||||
*/
|
*/
|
||||||
timestamp_refund: Timestamp | undefined;
|
lastRefundTimestamp: Timestamp | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last session signature that we submitted to /pay (if any).
|
* Last session signature that we submitted to /pay (if any).
|
||||||
@ -946,11 +1063,9 @@ export interface PurchaseRecord {
|
|||||||
*/
|
*/
|
||||||
abortDone: boolean;
|
abortDone: boolean;
|
||||||
|
|
||||||
/**
|
retryInfo: RetryInfo;
|
||||||
* Proposal ID for this purchase. Uniquely identifies the
|
|
||||||
* purchase and the proposal.
|
lastError: OperationError | undefined;
|
||||||
*/
|
|
||||||
proposalId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1025,7 +1140,7 @@ export interface WithdrawalSourceReserve {
|
|||||||
reservePub: string;
|
reservePub: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve
|
export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve;
|
||||||
|
|
||||||
export interface WithdrawalSessionRecord {
|
export interface WithdrawalSessionRecord {
|
||||||
withdrawSessionId: string;
|
withdrawSessionId: string;
|
||||||
@ -1048,7 +1163,8 @@ export interface WithdrawalSessionRecord {
|
|||||||
totalCoinValue: AmountJson;
|
totalCoinValue: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Amount including fees.
|
* Amount including fees (i.e. the amount subtracted from the
|
||||||
|
* reserve to withdraw all coins in this withdrawal session).
|
||||||
*/
|
*/
|
||||||
rawWithdrawalAmount: AmountJson;
|
rawWithdrawalAmount: AmountJson;
|
||||||
|
|
||||||
@ -1060,6 +1176,19 @@ export interface WithdrawalSessionRecord {
|
|||||||
* Coins in this session that are withdrawn are set to true.
|
* Coins in this session that are withdrawn are set to true.
|
||||||
*/
|
*/
|
||||||
withdrawn: boolean[];
|
withdrawn: boolean[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry info, always present even on completed operations so that indexing works.
|
||||||
|
*/
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last error per coin/planchet, or undefined if no error occured for
|
||||||
|
* the coin/planchet.
|
||||||
|
*/
|
||||||
|
lastCoinErrors: (OperationError | undefined)[];
|
||||||
|
|
||||||
|
lastError: OperationError | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BankWithdrawUriRecord {
|
export interface BankWithdrawUriRecord {
|
||||||
@ -1125,11 +1254,10 @@ export namespace Stores {
|
|||||||
"fulfillmentUrlIndex",
|
"fulfillmentUrlIndex",
|
||||||
"contractTerms.fulfillment_url",
|
"contractTerms.fulfillment_url",
|
||||||
);
|
);
|
||||||
orderIdIndex = new Index<string, PurchaseRecord>(
|
orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", [
|
||||||
this,
|
"contractTerms.merchant_base_url",
|
||||||
"orderIdIndex",
|
|
||||||
"contractTerms.order_id",
|
"contractTerms.order_id",
|
||||||
);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DenominationsStore extends Store<DenominationRecord> {
|
class DenominationsStore extends Store<DenominationRecord> {
|
||||||
|
@ -21,35 +21,22 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { Wallet, OperationFailedAndReportedError } from "../wallet";
|
import { Wallet } from "../wallet";
|
||||||
import { Notifier, Badge } from "../walletTypes";
|
|
||||||
import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge";
|
import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge";
|
||||||
import { SynchronousCryptoWorkerFactory } from "../crypto/synchronousWorker";
|
|
||||||
import { openTalerDb } from "../db";
|
import { openTalerDb } from "../db";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import querystring = require("querystring");
|
|
||||||
import { HttpRequestLibrary } from "../util/http";
|
import { HttpRequestLibrary } from "../util/http";
|
||||||
import * as amounts from "../util/amounts";
|
import * as amounts from "../util/amounts";
|
||||||
import { Bank } from "./bank";
|
import { Bank } from "./bank";
|
||||||
|
|
||||||
import fs = require("fs");
|
import fs = require("fs");
|
||||||
import { NodeCryptoWorkerFactory } from "../crypto/nodeProcessWorker";
|
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
|
import { NodeThreadCryptoWorkerFactory } from "../crypto/workers/nodeThreadWorker";
|
||||||
|
import { NotificationType } from "../walletTypes";
|
||||||
|
|
||||||
const logger = new Logger("helpers.ts");
|
const logger = new Logger("helpers.ts");
|
||||||
|
|
||||||
|
|
||||||
class ConsoleBadge implements Badge {
|
|
||||||
startBusy(): void {
|
|
||||||
}
|
|
||||||
stopBusy(): void {
|
|
||||||
}
|
|
||||||
showNotification(): void {
|
|
||||||
}
|
|
||||||
clearNotification(): void {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NodeHttpLib implements HttpRequestLibrary {
|
export class NodeHttpLib implements HttpRequestLibrary {
|
||||||
async get(url: string): Promise<import("../util/http").HttpResponse> {
|
async get(url: string): Promise<import("../util/http").HttpResponse> {
|
||||||
try {
|
try {
|
||||||
@ -97,7 +84,6 @@ export interface DefaultNodeWalletArgs {
|
|||||||
*/
|
*/
|
||||||
persistentStoragePath?: string;
|
persistentStoragePath?: string;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for asynchronous notifications from the wallet.
|
* Handler for asynchronous notifications from the wallet.
|
||||||
*/
|
*/
|
||||||
@ -116,15 +102,7 @@ export interface DefaultNodeWalletArgs {
|
|||||||
export async function getDefaultNodeWallet(
|
export async function getDefaultNodeWallet(
|
||||||
args: DefaultNodeWalletArgs = {},
|
args: DefaultNodeWalletArgs = {},
|
||||||
): Promise<Wallet> {
|
): Promise<Wallet> {
|
||||||
const myNotifier: Notifier = {
|
|
||||||
notify() {
|
|
||||||
if (args.notifyHandler) {
|
|
||||||
args.notifyHandler("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const myBadge = new ConsoleBadge();
|
|
||||||
|
|
||||||
BridgeIDBFactory.enableTracing = false;
|
BridgeIDBFactory.enableTracing = false;
|
||||||
const myBackend = new MemoryBackend();
|
const myBackend = new MemoryBackend();
|
||||||
@ -180,14 +158,14 @@ export async function getDefaultNodeWallet(
|
|||||||
myUnsupportedUpgrade,
|
myUnsupportedUpgrade,
|
||||||
);
|
);
|
||||||
|
|
||||||
const worker = new SynchronousCryptoWorkerFactory();
|
//const worker = new SynchronousCryptoWorkerFactory();
|
||||||
//const worker = new NodeCryptoWorkerFactory();
|
//const worker = new NodeCryptoWorkerFactory();
|
||||||
|
|
||||||
|
const worker = new NodeThreadCryptoWorkerFactory();
|
||||||
|
|
||||||
return new Wallet(
|
return new Wallet(
|
||||||
myDb,
|
myDb,
|
||||||
myHttpLib,
|
myHttpLib,
|
||||||
myBadge,
|
|
||||||
myNotifier,
|
|
||||||
worker,
|
worker,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -217,6 +195,14 @@ export async function withdrawTestBalance(
|
|||||||
["x-taler-bank"],
|
["x-taler-bank"],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const donePromise = new Promise((resolve, reject) => {
|
||||||
|
myWallet.addNotificationListener((n) => {
|
||||||
|
if (n.type === NotificationType.ReserveDepleted && n.reservePub === reservePub ) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
await bank.createReserve(
|
await bank.createReserve(
|
||||||
bankUser,
|
bankUser,
|
||||||
amount,
|
amount,
|
||||||
@ -225,5 +211,5 @@ export async function withdrawTestBalance(
|
|||||||
);
|
);
|
||||||
|
|
||||||
await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub });
|
await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub });
|
||||||
await myWallet.runUntilReserveDepleted(reservePub);
|
await donePromise;
|
||||||
}
|
}
|
||||||
|
@ -82,9 +82,5 @@ export async function runIntegrationTest(args: {
|
|||||||
throw Error("payment did not succeed");
|
throw Error("payment did not succeed");
|
||||||
}
|
}
|
||||||
|
|
||||||
await myWallet.runPending();
|
await myWallet.runUntilDone();
|
||||||
//const refreshRes = await myWallet.refreshDirtyCoins();
|
|
||||||
//console.log(`waited to refresh ${refreshRes.numRefreshed} coins`);
|
|
||||||
|
|
||||||
myWallet.stop();
|
|
||||||
}
|
}
|
||||||
|
@ -19,14 +19,14 @@ import fs = require("fs");
|
|||||||
import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers";
|
import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers";
|
||||||
import { MerchantBackendConnection } from "./merchant";
|
import { MerchantBackendConnection } from "./merchant";
|
||||||
import { runIntegrationTest } from "./integrationtest";
|
import { runIntegrationTest } from "./integrationtest";
|
||||||
import { Wallet, OperationFailedAndReportedError } from "../wallet";
|
import { Wallet } from "../wallet";
|
||||||
import qrcodeGenerator = require("qrcode-generator");
|
import qrcodeGenerator = require("qrcode-generator");
|
||||||
import * as clk from "./clk";
|
import * as clk from "./clk";
|
||||||
import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
|
import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import { decodeCrock } from "../crypto/talerCrypto";
|
import { decodeCrock } from "../crypto/talerCrypto";
|
||||||
import { Bank } from "./bank";
|
import { OperationFailedAndReportedError } from "../wallet-impl/errors";
|
||||||
|
|
||||||
const logger = new Logger("taler-wallet-cli.ts");
|
const logger = new Logger("taler-wallet-cli.ts");
|
||||||
|
|
||||||
|
@ -14,39 +14,76 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface MemoEntry<T> {
|
interface MemoEntry<T> {
|
||||||
p: Promise<T>;
|
p: Promise<T>;
|
||||||
t: number;
|
t: number;
|
||||||
n: number;
|
n: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AsyncOpMemo<T> {
|
export class AsyncOpMemoMap<T> {
|
||||||
private n = 0;
|
private n = 0;
|
||||||
private memo: { [k: string]: MemoEntry<T> } = {};
|
private memoMap: { [k: string]: MemoEntry<T> } = {};
|
||||||
put(key: string, p: Promise<T>): Promise<T> {
|
|
||||||
|
private cleanUp(key: string, n: number) {
|
||||||
|
const r = this.memoMap[key];
|
||||||
|
if (r && r.n === n) {
|
||||||
|
delete this.memoMap[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memo(key: string, pg: () => Promise<T>): Promise<T> {
|
||||||
|
const res = this.memoMap[key];
|
||||||
|
if (res) {
|
||||||
|
return res.p;
|
||||||
|
}
|
||||||
const n = this.n++;
|
const n = this.n++;
|
||||||
this.memo[key] = {
|
// Wrap the operation in case it immediately throws
|
||||||
|
const p = Promise.resolve().then(() => pg());
|
||||||
|
p.finally(() => {
|
||||||
|
this.cleanUp(key, n);
|
||||||
|
});
|
||||||
|
this.memoMap[key] = {
|
||||||
p,
|
p,
|
||||||
n,
|
n,
|
||||||
t: new Date().getTime(),
|
t: new Date().getTime(),
|
||||||
};
|
};
|
||||||
p.finally(() => {
|
|
||||||
const r = this.memo[key];
|
|
||||||
if (r && r.n === n) {
|
|
||||||
delete this.memo[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
find(key: string): Promise<T> | undefined {
|
clear() {
|
||||||
const res = this.memo[key];
|
this.memoMap = {};
|
||||||
const tNow = new Date().getTime();
|
}
|
||||||
if (res && res.t < tNow - 10 * 1000) {
|
}
|
||||||
delete this.memo[key];
|
|
||||||
return;
|
|
||||||
} else if (res) {
|
export class AsyncOpMemoSingle<T> {
|
||||||
|
private n = 0;
|
||||||
|
private memoEntry: MemoEntry<T> | undefined;
|
||||||
|
|
||||||
|
private cleanUp(n: number) {
|
||||||
|
if (this.memoEntry && this.memoEntry.n === n) {
|
||||||
|
this.memoEntry = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memo(pg: () => Promise<T>): Promise<T> {
|
||||||
|
const res = this.memoEntry;
|
||||||
|
if (res) {
|
||||||
return res.p;
|
return res.p;
|
||||||
}
|
}
|
||||||
return;
|
const n = this.n++;
|
||||||
|
// Wrap the operation in case it immediately throws
|
||||||
|
const p = Promise.resolve().then(() => pg());
|
||||||
|
p.finally(() => {
|
||||||
|
this.cleanUp(n);
|
||||||
|
});
|
||||||
|
this.memoEntry = {
|
||||||
|
p,
|
||||||
|
n,
|
||||||
|
t: new Date().getTime(),
|
||||||
|
};
|
||||||
|
return p;
|
||||||
}
|
}
|
||||||
}
|
clear() {
|
||||||
|
this.memoEntry = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -316,7 +316,7 @@ export function oneShotIterIndex<S extends IDBValidKey, T>(
|
|||||||
return new ResultStream<T>(req);
|
return new ResultStream<T>(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TransactionHandle {
|
export class TransactionHandle {
|
||||||
constructor(private tx: IDBTransaction) {}
|
constructor(private tx: IDBTransaction) {}
|
||||||
|
|
||||||
put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
|
put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
|
||||||
@ -406,6 +406,7 @@ function runWithTransaction<T>(
|
|||||||
};
|
};
|
||||||
tx.onerror = () => {
|
tx.onerror = () => {
|
||||||
console.error("error in transaction");
|
console.error("error in transaction");
|
||||||
|
console.error(stack);
|
||||||
};
|
};
|
||||||
tx.onabort = () => {
|
tx.onabort = () => {
|
||||||
if (tx.error) {
|
if (tx.error) {
|
||||||
|
@ -169,10 +169,8 @@ test("taler refund uri parsing", t => {
|
|||||||
t.fail();
|
t.fail();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
t.is(
|
t.is(r1.merchantBaseUrl, "https://merchant.example.com/public/");
|
||||||
r1.refundUrl,
|
t.is(r1.orderId, "1234");
|
||||||
"https://merchant.example.com/public/refund?order_id=1234",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("taler refund uri parsing with instance", t => {
|
test("taler refund uri parsing with instance", t => {
|
||||||
@ -182,10 +180,8 @@ test("taler refund uri parsing with instance", t => {
|
|||||||
t.fail();
|
t.fail();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
t.is(
|
t.is(r1.orderId, "1234");
|
||||||
r1.refundUrl,
|
t.is(r1.merchantBaseUrl, "https://merchant.example.com/public/instances/myinst/");
|
||||||
"https://merchant.example.com/public/instances/myinst/refund?order_id=1234",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("taler tip pickup uri", t => {
|
test("taler tip pickup uri", t => {
|
||||||
@ -197,7 +193,7 @@ test("taler tip pickup uri", t => {
|
|||||||
}
|
}
|
||||||
t.is(
|
t.is(
|
||||||
r1.merchantBaseUrl,
|
r1.merchantBaseUrl,
|
||||||
"https://merchant.example.com/public/tip-pickup?tip_id=tipid",
|
"https://merchant.example.com/public/",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -24,7 +24,8 @@ export interface WithdrawUriResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RefundUriResult {
|
export interface RefundUriResult {
|
||||||
refundUrl: string;
|
merchantBaseUrl: string;
|
||||||
|
orderId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TipUriResult {
|
export interface TipUriResult {
|
||||||
@ -184,17 +185,13 @@ export function parseRefundUri(s: string): RefundUriResult | undefined {
|
|||||||
maybeInstancePath = `instances/${maybeInstance}/`;
|
maybeInstancePath = `instances/${maybeInstance}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const refundUrl =
|
const merchantBaseUrl = "https://" + host +
|
||||||
"https://" +
|
"/" +
|
||||||
host +
|
maybePath +
|
||||||
"/" +
|
maybeInstancePath
|
||||||
maybePath +
|
|
||||||
maybeInstancePath +
|
|
||||||
"refund" +
|
|
||||||
"?order_id=" +
|
|
||||||
orderId;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
refundUrl,
|
merchantBaseUrl,
|
||||||
|
orderId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ const logger = new Logger("withdraw.ts");
|
|||||||
export async function getBalances(
|
export async function getBalances(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
): Promise<WalletBalance> {
|
): Promise<WalletBalance> {
|
||||||
|
logger.trace("starting to compute balance");
|
||||||
/**
|
/**
|
||||||
* Add amount to a balance field, both for
|
* Add amount to a balance field, both for
|
||||||
* the slicing by exchange and currency.
|
* the slicing by exchange and currency.
|
||||||
@ -101,7 +102,7 @@ export async function getBalances(
|
|||||||
await tx.iter(Stores.refresh).forEach(r => {
|
await tx.iter(Stores.refresh).forEach(r => {
|
||||||
// Don't count finished refreshes, since the refresh already resulted
|
// Don't count finished refreshes, since the refresh already resulted
|
||||||
// in coins being added to the wallet.
|
// in coins being added to the wallet.
|
||||||
if (r.finished) {
|
if (r.finishedTimestamp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addTo(
|
addTo(
|
||||||
|
81
src/wallet-impl/errors.ts
Normal file
81
src/wallet-impl/errors.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { OperationError } from "../walletTypes";
|
||||||
|
|
||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2019 GNUnet e.V.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exception is there to let the caller know that an error happened,
|
||||||
|
* but the error has already been reported by writing it to the database.
|
||||||
|
*/
|
||||||
|
export class OperationFailedAndReportedError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
// Set the prototype explicitly.
|
||||||
|
Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exception is thrown when an error occured and the caller is
|
||||||
|
* responsible for recording the failure in the database.
|
||||||
|
*/
|
||||||
|
export class OperationFailedError extends Error {
|
||||||
|
constructor(message: string, public err: OperationError) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
// Set the prototype explicitly.
|
||||||
|
Object.setPrototypeOf(this, OperationFailedError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run an operation and call the onOpError callback
|
||||||
|
* when there was an exception or operation error that must be reported.
|
||||||
|
* The cause will be re-thrown to the caller.
|
||||||
|
*/
|
||||||
|
export async function guardOperationException<T>(
|
||||||
|
op: () => Promise<T>,
|
||||||
|
onOpError: (e: OperationError) => Promise<void>,
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
return op();
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof OperationFailedAndReportedError) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
if (e instanceof OperationFailedError) {
|
||||||
|
await onOpError(e.err);
|
||||||
|
throw new OperationFailedAndReportedError(e.message);
|
||||||
|
}
|
||||||
|
if (e instanceof Error) {
|
||||||
|
await onOpError({
|
||||||
|
type: "exception",
|
||||||
|
message: e.message,
|
||||||
|
details: {},
|
||||||
|
});
|
||||||
|
throw new OperationFailedAndReportedError(e.message);
|
||||||
|
}
|
||||||
|
await onOpError({
|
||||||
|
type: "exception",
|
||||||
|
message: "non-error exception thrown",
|
||||||
|
details: {
|
||||||
|
value: e.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
throw new OperationFailedAndReportedError(e.message);
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,6 @@
|
|||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import {
|
import {
|
||||||
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
||||||
OperationFailedAndReportedError,
|
|
||||||
} from "../wallet";
|
} from "../wallet";
|
||||||
import { KeysJson, Denomination, ExchangeWireJson } from "../talerTypes";
|
import { KeysJson, Denomination, ExchangeWireJson } from "../talerTypes";
|
||||||
import { getTimestampNow, OperationError } from "../walletTypes";
|
import { getTimestampNow, OperationError } from "../walletTypes";
|
||||||
@ -42,6 +41,7 @@ import {
|
|||||||
} from "../util/query";
|
} from "../util/query";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import { parsePaytoUri } from "../util/payto";
|
import { parsePaytoUri } from "../util/payto";
|
||||||
|
import { OperationFailedAndReportedError } from "./errors";
|
||||||
|
|
||||||
async function denominationRecordFromKeys(
|
async function denominationRecordFromKeys(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
|
@ -78,11 +78,11 @@ export async function getHistory(
|
|||||||
fulfillmentUrl: p.contractTerms.fulfillment_url,
|
fulfillmentUrl: p.contractTerms.fulfillment_url,
|
||||||
merchantName: p.contractTerms.merchant.name,
|
merchantName: p.contractTerms.merchant.name,
|
||||||
},
|
},
|
||||||
timestamp: p.timestamp,
|
timestamp: p.acceptTimestamp,
|
||||||
type: "pay",
|
type: "pay",
|
||||||
explicit: false,
|
explicit: false,
|
||||||
});
|
});
|
||||||
if (p.timestamp_refund) {
|
if (p.lastRefundTimestamp) {
|
||||||
const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
|
const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
|
||||||
const amountsPending = Object.keys(p.refundsPending).map(x =>
|
const amountsPending = Object.keys(p.refundsPending).map(x =>
|
||||||
Amounts.parseOrThrow(p.refundsPending[x].refund_amount),
|
Amounts.parseOrThrow(p.refundsPending[x].refund_amount),
|
||||||
@ -103,7 +103,7 @@ export async function getHistory(
|
|||||||
merchantName: p.contractTerms.merchant.name,
|
merchantName: p.contractTerms.merchant.name,
|
||||||
refundAmount: amount,
|
refundAmount: amount,
|
||||||
},
|
},
|
||||||
timestamp: p.timestamp_refund,
|
timestamp: p.lastRefundTimestamp,
|
||||||
type: "refund",
|
type: "refund",
|
||||||
explicit: false,
|
explicit: false,
|
||||||
});
|
});
|
||||||
@ -151,7 +151,7 @@ export async function getHistory(
|
|||||||
merchantBaseUrl: tip.merchantBaseUrl,
|
merchantBaseUrl: tip.merchantBaseUrl,
|
||||||
tipId: tip.merchantTipId,
|
tipId: tip.merchantTipId,
|
||||||
},
|
},
|
||||||
timestamp: tip.timestamp,
|
timestamp: tip.createdTimestamp,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
type: "tip",
|
type: "tip",
|
||||||
});
|
});
|
||||||
|
@ -33,6 +33,8 @@ import {
|
|||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
|
OperationError,
|
||||||
|
NotificationType,
|
||||||
} from "../walletTypes";
|
} from "../walletTypes";
|
||||||
import {
|
import {
|
||||||
oneShotIter,
|
oneShotIter,
|
||||||
@ -51,12 +53,14 @@ import {
|
|||||||
PurchaseRecord,
|
PurchaseRecord,
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
ProposalStatus,
|
ProposalStatus,
|
||||||
|
initRetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
|
PurchaseStatus,
|
||||||
} from "../dbTypes";
|
} from "../dbTypes";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import {
|
import {
|
||||||
amountToPretty,
|
amountToPretty,
|
||||||
strcmp,
|
strcmp,
|
||||||
extractTalerStamp,
|
|
||||||
canonicalJson,
|
canonicalJson,
|
||||||
extractTalerStampOrThrow,
|
extractTalerStampOrThrow,
|
||||||
} from "../util/helpers";
|
} from "../util/helpers";
|
||||||
@ -65,6 +69,8 @@ import { InternalWalletState } from "./state";
|
|||||||
import { parsePayUri, parseRefundUri } from "../util/taleruri";
|
import { parsePayUri, parseRefundUri } from "../util/taleruri";
|
||||||
import { getTotalRefreshCost, refresh } from "./refresh";
|
import { getTotalRefreshCost, refresh } from "./refresh";
|
||||||
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
||||||
|
import { guardOperationException } from "./errors";
|
||||||
|
import { assertUnreachable } from "../util/assertUnreachable";
|
||||||
|
|
||||||
export interface SpeculativePayData {
|
export interface SpeculativePayData {
|
||||||
payCoinInfo: PayCoinInfo;
|
payCoinInfo: PayCoinInfo;
|
||||||
@ -344,9 +350,12 @@ async function recordConfirmPay(
|
|||||||
payReq,
|
payReq,
|
||||||
refundsDone: {},
|
refundsDone: {},
|
||||||
refundsPending: {},
|
refundsPending: {},
|
||||||
timestamp: getTimestampNow(),
|
acceptTimestamp: getTimestampNow(),
|
||||||
timestamp_refund: undefined,
|
lastRefundTimestamp: undefined,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
|
retryInfo: initRetryInfo(),
|
||||||
|
lastError: undefined,
|
||||||
|
status: PurchaseStatus.SubmitPay,
|
||||||
};
|
};
|
||||||
|
|
||||||
await runWithWriteTransaction(
|
await runWithWriteTransaction(
|
||||||
@ -365,8 +374,10 @@ async function recordConfirmPay(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
ws.badge.showNotification();
|
ws.notify({
|
||||||
ws.notifier.notify();
|
type: NotificationType.ProposalAccepted,
|
||||||
|
proposalId: proposal.proposalId,
|
||||||
|
});
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,7 +430,7 @@ export async function abortFailedPayment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
|
const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
|
||||||
await acceptRefundResponse(ws, refundResponse);
|
await acceptRefundResponse(ws, purchase.proposalId, refundResponse);
|
||||||
|
|
||||||
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
|
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
|
||||||
const p = await tx.get(Stores.purchases, proposalId);
|
const p = await tx.get(Stores.purchases, proposalId);
|
||||||
@ -431,9 +442,61 @@ export async function abortFailedPayment(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function incrementProposalRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
|
err: OperationError | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
await runWithWriteTransaction(ws.db, [Stores.proposals], async tx => {
|
||||||
|
const pr = await tx.get(Stores.proposals, proposalId);
|
||||||
|
if (!pr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!pr.retryInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pr.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(pr.retryInfo);
|
||||||
|
pr.lastError = err;
|
||||||
|
await tx.put(Stores.proposals, pr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function incrementPurchaseRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
|
err: OperationError | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
|
||||||
|
const pr = await tx.get(Stores.purchases, proposalId);
|
||||||
|
if (!pr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!pr.retryInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pr.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(pr.retryInfo);
|
||||||
|
pr.lastError = err;
|
||||||
|
await tx.put(Stores.purchases, pr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function processDownloadProposal(
|
export async function processDownloadProposal(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const onOpErr = (err: OperationError) =>
|
||||||
|
incrementProposalRetry(ws, proposalId, err);
|
||||||
|
await guardOperationException(
|
||||||
|
() => processDownloadProposalImpl(ws, proposalId),
|
||||||
|
onOpErr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processDownloadProposalImpl(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
|
const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
|
||||||
if (!proposal) {
|
if (!proposal) {
|
||||||
@ -498,7 +561,10 @@ export async function processDownloadProposal(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
ws.notifier.notify();
|
ws.notify({
|
||||||
|
type: NotificationType.ProposalDownloaded,
|
||||||
|
proposalId: proposal.proposalId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -536,6 +602,8 @@ async function startDownloadProposal(
|
|||||||
proposalId: proposalId,
|
proposalId: proposalId,
|
||||||
proposalStatus: ProposalStatus.DOWNLOADING,
|
proposalStatus: ProposalStatus.DOWNLOADING,
|
||||||
repurchaseProposalId: undefined,
|
repurchaseProposalId: undefined,
|
||||||
|
retryInfo: initRetryInfo(),
|
||||||
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
await oneShotPut(ws.db, Stores.proposals, proposalRecord);
|
await oneShotPut(ws.db, Stores.proposals, proposalRecord);
|
||||||
@ -582,6 +650,7 @@ export async function submitPay(
|
|||||||
throw Error("merchant payment signature invalid");
|
throw Error("merchant payment signature invalid");
|
||||||
}
|
}
|
||||||
purchase.finished = true;
|
purchase.finished = true;
|
||||||
|
purchase.retryInfo = initRetryInfo(false);
|
||||||
const modifiedCoins: CoinRecord[] = [];
|
const modifiedCoins: CoinRecord[] = [];
|
||||||
for (const pc of purchase.payReq.coins) {
|
for (const pc of purchase.payReq.coins) {
|
||||||
const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub);
|
const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub);
|
||||||
@ -859,8 +928,6 @@ export async function confirmPay(
|
|||||||
return submitPay(ws, proposalId, sessionId);
|
return submitPay(ws, proposalId, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function getFullRefundFees(
|
export async function getFullRefundFees(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
refundPermissions: MerchantRefundPermission[],
|
refundPermissions: MerchantRefundPermission[],
|
||||||
@ -914,15 +981,13 @@ export async function getFullRefundFees(
|
|||||||
return feeAcc;
|
return feeAcc;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitRefunds(
|
async function submitRefundsToExchange(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
console.error(
|
console.error("not submitting refunds, payment not found:");
|
||||||
"not submitting refunds, payment not found:",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pendingKeys = Object.keys(purchase.refundsPending);
|
const pendingKeys = Object.keys(purchase.refundsPending);
|
||||||
@ -991,14 +1056,18 @@ async function submitRefunds(
|
|||||||
refresh(ws, perm.coin_pub);
|
refresh(ws, perm.coin_pub);
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.badge.showNotification();
|
ws.notify({
|
||||||
ws.notifier.notify();
|
type: NotificationType.RefundsSubmitted,
|
||||||
|
proposalId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acceptRefundResponse(
|
|
||||||
|
async function acceptRefundResponse(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
refundResponse: MerchantRefundResponse,
|
refundResponse: MerchantRefundResponse,
|
||||||
): Promise<string> {
|
): Promise<void> {
|
||||||
const refundPermissions = refundResponse.refund_permissions;
|
const refundPermissions = refundResponse.refund_permissions;
|
||||||
|
|
||||||
if (!refundPermissions.length) {
|
if (!refundPermissions.length) {
|
||||||
@ -1015,7 +1084,8 @@ export async function acceptRefundResponse(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
t.timestamp_refund = getTimestampNow();
|
t.lastRefundTimestamp = getTimestampNow();
|
||||||
|
t.status = PurchaseStatus.ProcessRefund;
|
||||||
|
|
||||||
for (const perm of refundPermissions) {
|
for (const perm of refundPermissions) {
|
||||||
if (
|
if (
|
||||||
@ -1027,18 +1097,48 @@ export async function acceptRefundResponse(
|
|||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hc = refundResponse.h_contract_terms;
|
|
||||||
|
|
||||||
// Add the refund permissions to the purchase within a DB transaction
|
// Add the refund permissions to the purchase within a DB transaction
|
||||||
await oneShotMutate(ws.db, Stores.purchases, hc, f);
|
await oneShotMutate(ws.db, Stores.purchases, proposalId, f);
|
||||||
ws.notifier.notify();
|
await submitRefundsToExchange(ws, proposalId);
|
||||||
|
|
||||||
await submitRefunds(ws, hc);
|
|
||||||
|
|
||||||
return hc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function queryRefund(ws: InternalWalletState, proposalId: string): Promise<void> {
|
||||||
|
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||||
|
if (purchase?.status !== PurchaseStatus.QueryRefund) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refundUrl = new URL("refund", purchase.contractTerms.merchant_base_url).href
|
||||||
|
let resp;
|
||||||
|
try {
|
||||||
|
resp = await ws.http.get(refundUrl);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("error downloading refund permission", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
|
||||||
|
await acceptRefundResponse(ws, proposalId, refundResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startRefundQuery(ws: InternalWalletState, proposalId: string): Promise<void> {
|
||||||
|
const success = await runWithWriteTransaction(ws.db, [Stores.purchases], async (tx) => {
|
||||||
|
const p = await tx.get(Stores.purchases, proposalId);
|
||||||
|
if (p?.status !== PurchaseStatus.Done) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
p.status = PurchaseStatus.QueryRefund;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await queryRefund(ws, proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept a refund, return the contract hash for the contract
|
* Accept a refund, return the contract hash for the contract
|
||||||
* that was involved in the refund.
|
* that was involved in the refund.
|
||||||
@ -1053,17 +1153,56 @@ export async function applyRefund(
|
|||||||
throw Error("invalid refund URI");
|
throw Error("invalid refund URI");
|
||||||
}
|
}
|
||||||
|
|
||||||
const refundUrl = parseResult.refundUrl;
|
const purchase = await oneShotGetIndexed(
|
||||||
|
ws.db,
|
||||||
|
Stores.purchases.orderIdIndex,
|
||||||
|
[parseResult.merchantBaseUrl, parseResult.orderId],
|
||||||
|
);
|
||||||
|
|
||||||
logger.trace("processing refund");
|
if (!purchase) {
|
||||||
let resp;
|
throw Error("no purchase for the taler://refund/ URI was found");
|
||||||
try {
|
|
||||||
resp = await ws.http.get(refundUrl);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("error downloading refund permission", e);
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
|
await startRefundQuery(ws, purchase.proposalId);
|
||||||
return acceptRefundResponse(ws, refundResponse);
|
|
||||||
|
return purchase.contractTermsHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processPurchase(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const onOpErr = (e: OperationError) =>
|
||||||
|
incrementPurchaseRetry(ws, proposalId, e);
|
||||||
|
await guardOperationException(
|
||||||
|
() => processPurchaseImpl(ws, proposalId),
|
||||||
|
onOpErr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processPurchaseImpl(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||||
|
if (!purchase) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (purchase.status) {
|
||||||
|
case PurchaseStatus.Done:
|
||||||
|
return;
|
||||||
|
case PurchaseStatus.Abort:
|
||||||
|
// FIXME
|
||||||
|
break;
|
||||||
|
case PurchaseStatus.SubmitPay:
|
||||||
|
break;
|
||||||
|
case PurchaseStatus.QueryRefund:
|
||||||
|
await queryRefund(ws, proposalId);
|
||||||
|
break;
|
||||||
|
case PurchaseStatus.ProcessRefund:
|
||||||
|
await submitRefundsToExchange(ws, proposalId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw assertUnreachable(purchase.status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ import { Stores, TipRecord, CoinStatus } from "../dbTypes";
|
|||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { PaybackConfirmation } from "../talerTypes";
|
import { PaybackConfirmation } from "../talerTypes";
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { updateExchangeFromUrl } from "./exchanges";
|
||||||
|
import { NotificationType } from "../walletTypes";
|
||||||
|
|
||||||
const logger = new Logger("payback.ts");
|
const logger = new Logger("payback.ts");
|
||||||
|
|
||||||
@ -65,7 +66,9 @@ export async function payback(
|
|||||||
await tx.put(Stores.reserves, reserve);
|
await tx.put(Stores.reserves, reserve);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ws.notifier.notify();
|
ws.notify({
|
||||||
|
type: NotificationType.PaybackStarted,
|
||||||
|
});
|
||||||
|
|
||||||
const paybackRequest = await ws.cryptoApi.createPaybackRequest(coin);
|
const paybackRequest = await ws.cryptoApi.createPaybackRequest(coin);
|
||||||
const reqUrl = new URL("payback", coin.exchangeBaseUrl);
|
const reqUrl = new URL("payback", coin.exchangeBaseUrl);
|
||||||
@ -83,6 +86,8 @@ export async function payback(
|
|||||||
}
|
}
|
||||||
coin.status = CoinStatus.Dormant;
|
coin.status = CoinStatus.Dormant;
|
||||||
await oneShotPut(ws.db, Stores.coins, coin);
|
await oneShotPut(ws.db, Stores.coins, coin);
|
||||||
ws.notifier.notify();
|
ws.notify({
|
||||||
|
type: NotificationType.PaybackFinished,
|
||||||
|
});
|
||||||
await updateExchangeFromUrl(ws, coin.exchangeBaseUrl, true);
|
await updateExchangeFromUrl(ws, coin.exchangeBaseUrl, true);
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,10 @@ import {
|
|||||||
PendingOperationInfo,
|
PendingOperationInfo,
|
||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
|
Timestamp,
|
||||||
|
Duration,
|
||||||
} from "../walletTypes";
|
} from "../walletTypes";
|
||||||
import { runWithReadTransaction } from "../util/query";
|
import { runWithReadTransaction, TransactionHandle } from "../util/query";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import {
|
import {
|
||||||
Stores,
|
Stores,
|
||||||
@ -32,11 +34,355 @@ import {
|
|||||||
ProposalStatus,
|
ProposalStatus,
|
||||||
} from "../dbTypes";
|
} from "../dbTypes";
|
||||||
|
|
||||||
|
function updateRetryDelay(
|
||||||
|
oldDelay: Duration,
|
||||||
|
now: Timestamp,
|
||||||
|
retryTimestamp: Timestamp,
|
||||||
|
): Duration {
|
||||||
|
if (retryTimestamp.t_ms <= now.t_ms) {
|
||||||
|
return { d_ms: 0 };
|
||||||
|
}
|
||||||
|
return { d_ms: Math.min(oldDelay.d_ms, retryTimestamp.t_ms - now.t_ms) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gatherExchangePending(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
now: Timestamp,
|
||||||
|
resp: PendingOperationsResponse,
|
||||||
|
onlyDue: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
if (onlyDue) {
|
||||||
|
// FIXME: exchanges should also be updated regularly
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await tx.iter(Stores.exchanges).forEach(e => {
|
||||||
|
switch (e.updateStatus) {
|
||||||
|
case ExchangeUpdateStatus.FINISHED:
|
||||||
|
if (e.lastError) {
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "bug",
|
||||||
|
givesLifeness: false,
|
||||||
|
message:
|
||||||
|
"Exchange record is in FINISHED state but has lastError set",
|
||||||
|
details: {
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!e.details) {
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "bug",
|
||||||
|
givesLifeness: false,
|
||||||
|
message:
|
||||||
|
"Exchange record does not have details, but no update in progress.",
|
||||||
|
details: {
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!e.wireInfo) {
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "bug",
|
||||||
|
givesLifeness: false,
|
||||||
|
message:
|
||||||
|
"Exchange record does not have wire info, but no update in progress.",
|
||||||
|
details: {
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ExchangeUpdateStatus.FETCH_KEYS:
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "exchange-update",
|
||||||
|
givesLifeness: false,
|
||||||
|
stage: "fetch-keys",
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
lastError: e.lastError,
|
||||||
|
reason: e.updateReason || "unknown",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ExchangeUpdateStatus.FETCH_WIRE:
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "exchange-update",
|
||||||
|
givesLifeness: false,
|
||||||
|
stage: "fetch-wire",
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
lastError: e.lastError,
|
||||||
|
reason: e.updateReason || "unknown",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "bug",
|
||||||
|
givesLifeness: false,
|
||||||
|
message: "Unknown exchangeUpdateStatus",
|
||||||
|
details: {
|
||||||
|
exchangeBaseUrl: e.baseUrl,
|
||||||
|
exchangeUpdateStatus: e.updateStatus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gatherReservePending(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
now: Timestamp,
|
||||||
|
resp: PendingOperationsResponse,
|
||||||
|
onlyDue: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
// FIXME: this should be optimized by using an index for "onlyDue==true".
|
||||||
|
await tx.iter(Stores.reserves).forEach(reserve => {
|
||||||
|
const reserveType = reserve.bankWithdrawStatusUrl ? "taler-bank" : "manual";
|
||||||
|
if (!reserve.retryInfo.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
|
resp.nextRetryDelay,
|
||||||
|
now,
|
||||||
|
reserve.retryInfo.nextRetry,
|
||||||
|
);
|
||||||
|
if (onlyDue && reserve.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (reserve.reserveStatus) {
|
||||||
|
case ReserveRecordStatus.DORMANT:
|
||||||
|
// nothing to report as pending
|
||||||
|
break;
|
||||||
|
case ReserveRecordStatus.WITHDRAWING:
|
||||||
|
case ReserveRecordStatus.UNCONFIRMED:
|
||||||
|
case ReserveRecordStatus.QUERYING_STATUS:
|
||||||
|
case ReserveRecordStatus.REGISTERING_BANK:
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "reserve",
|
||||||
|
givesLifeness: true,
|
||||||
|
stage: reserve.reserveStatus,
|
||||||
|
timestampCreated: reserve.created,
|
||||||
|
reserveType,
|
||||||
|
reservePub: reserve.reservePub,
|
||||||
|
retryInfo: reserve.retryInfo,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "reserve",
|
||||||
|
givesLifeness: true,
|
||||||
|
stage: reserve.reserveStatus,
|
||||||
|
timestampCreated: reserve.created,
|
||||||
|
reserveType,
|
||||||
|
reservePub: reserve.reservePub,
|
||||||
|
bankWithdrawConfirmUrl: reserve.bankWithdrawConfirmUrl,
|
||||||
|
retryInfo: reserve.retryInfo,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "bug",
|
||||||
|
givesLifeness: false,
|
||||||
|
message: "Unknown reserve record status",
|
||||||
|
details: {
|
||||||
|
reservePub: reserve.reservePub,
|
||||||
|
reserveStatus: reserve.reserveStatus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gatherRefreshPending(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
now: Timestamp,
|
||||||
|
resp: PendingOperationsResponse,
|
||||||
|
onlyDue: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
await tx.iter(Stores.refresh).forEach(r => {
|
||||||
|
if (r.finishedTimestamp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
|
resp.nextRetryDelay,
|
||||||
|
now,
|
||||||
|
r.retryInfo.nextRetry,
|
||||||
|
);
|
||||||
|
if (onlyDue && r.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let refreshStatus: string;
|
||||||
|
if (r.norevealIndex === undefined) {
|
||||||
|
refreshStatus = "melt";
|
||||||
|
} else {
|
||||||
|
refreshStatus = "reveal";
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "refresh",
|
||||||
|
givesLifeness: true,
|
||||||
|
oldCoinPub: r.meltCoinPub,
|
||||||
|
refreshStatus,
|
||||||
|
refreshOutputSize: r.newDenoms.length,
|
||||||
|
refreshSessionId: r.refreshSessionId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gatherCoinsPending(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
now: Timestamp,
|
||||||
|
resp: PendingOperationsResponse,
|
||||||
|
onlyDue: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
// Refreshing dirty coins is always due.
|
||||||
|
await tx.iter(Stores.coins).forEach(coin => {
|
||||||
|
if (coin.status == CoinStatus.Dirty) {
|
||||||
|
resp.nextRetryDelay.d_ms = 0;
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
givesLifeness: true,
|
||||||
|
type: "dirty-coin",
|
||||||
|
coinPub: coin.coinPub,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gatherWithdrawalPending(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
now: Timestamp,
|
||||||
|
resp: PendingOperationsResponse,
|
||||||
|
onlyDue: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
await tx.iter(Stores.withdrawalSession).forEach(wsr => {
|
||||||
|
if (wsr.finishTimestamp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
|
resp.nextRetryDelay,
|
||||||
|
now,
|
||||||
|
wsr.retryInfo.nextRetry,
|
||||||
|
);
|
||||||
|
if (onlyDue && wsr.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const numCoinsWithdrawn = wsr.withdrawn.reduce((a, x) => a + (x ? 1 : 0), 0);
|
||||||
|
const numCoinsTotal = wsr.withdrawn.length;
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "withdraw",
|
||||||
|
givesLifeness: true,
|
||||||
|
numCoinsTotal,
|
||||||
|
numCoinsWithdrawn,
|
||||||
|
source: wsr.source,
|
||||||
|
withdrawSessionId: wsr.withdrawSessionId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gatherProposalPending(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
now: Timestamp,
|
||||||
|
resp: PendingOperationsResponse,
|
||||||
|
onlyDue: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
await tx.iter(Stores.proposals).forEach(proposal => {
|
||||||
|
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
|
||||||
|
if (onlyDue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "proposal-choice",
|
||||||
|
givesLifeness: false,
|
||||||
|
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
|
||||||
|
proposalId: proposal.proposalId,
|
||||||
|
proposalTimestamp: proposal.timestamp,
|
||||||
|
});
|
||||||
|
} else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
|
||||||
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
|
resp.nextRetryDelay,
|
||||||
|
now,
|
||||||
|
proposal.retryInfo.nextRetry,
|
||||||
|
);
|
||||||
|
if (onlyDue && proposal.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "proposal-download",
|
||||||
|
givesLifeness: true,
|
||||||
|
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
|
||||||
|
proposalId: proposal.proposalId,
|
||||||
|
proposalTimestamp: proposal.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gatherTipPending(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
now: Timestamp,
|
||||||
|
resp: PendingOperationsResponse,
|
||||||
|
onlyDue: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
await tx.iter(Stores.tips).forEach(tip => {
|
||||||
|
if (tip.pickedUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
|
resp.nextRetryDelay,
|
||||||
|
now,
|
||||||
|
tip.retryInfo.nextRetry,
|
||||||
|
);
|
||||||
|
if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tip.accepted) {
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "tip",
|
||||||
|
givesLifeness: true,
|
||||||
|
merchantBaseUrl: tip.merchantBaseUrl,
|
||||||
|
tipId: tip.tipId,
|
||||||
|
merchantTipId: tip.merchantTipId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gatherPurchasePending(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
now: Timestamp,
|
||||||
|
resp: PendingOperationsResponse,
|
||||||
|
onlyDue: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
await tx.iter(Stores.purchases).forEach((pr) => {
|
||||||
|
if (pr.finished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
|
resp.nextRetryDelay,
|
||||||
|
now,
|
||||||
|
pr.retryInfo.nextRetry,
|
||||||
|
);
|
||||||
|
if (onlyDue && pr.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: "pay",
|
||||||
|
givesLifeness: true,
|
||||||
|
isReplay: false,
|
||||||
|
proposalId: pr.proposalId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export async function getPendingOperations(
|
export async function getPendingOperations(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
|
onlyDue: boolean = false,
|
||||||
): Promise<PendingOperationsResponse> {
|
): Promise<PendingOperationsResponse> {
|
||||||
const pendingOperations: PendingOperationInfo[] = [];
|
const resp: PendingOperationsResponse = {
|
||||||
let minRetryDurationMs = 5000;
|
nextRetryDelay: { d_ms: Number.MAX_SAFE_INTEGER },
|
||||||
|
pendingOperations: [],
|
||||||
|
};
|
||||||
|
const now = getTimestampNow();
|
||||||
await runWithReadTransaction(
|
await runWithReadTransaction(
|
||||||
ws.db,
|
ws.db,
|
||||||
[
|
[
|
||||||
@ -47,207 +393,18 @@ export async function getPendingOperations(
|
|||||||
Stores.withdrawalSession,
|
Stores.withdrawalSession,
|
||||||
Stores.proposals,
|
Stores.proposals,
|
||||||
Stores.tips,
|
Stores.tips,
|
||||||
|
Stores.purchases,
|
||||||
],
|
],
|
||||||
async tx => {
|
async tx => {
|
||||||
await tx.iter(Stores.exchanges).forEach(e => {
|
await gatherExchangePending(tx, now, resp, onlyDue);
|
||||||
switch (e.updateStatus) {
|
await gatherReservePending(tx, now, resp, onlyDue);
|
||||||
case ExchangeUpdateStatus.FINISHED:
|
await gatherRefreshPending(tx, now, resp, onlyDue);
|
||||||
if (e.lastError) {
|
await gatherCoinsPending(tx, now, resp, onlyDue);
|
||||||
pendingOperations.push({
|
await gatherWithdrawalPending(tx, now, resp, onlyDue);
|
||||||
type: "bug",
|
await gatherProposalPending(tx, now, resp, onlyDue);
|
||||||
message:
|
await gatherTipPending(tx, now, resp, onlyDue);
|
||||||
"Exchange record is in FINISHED state but has lastError set",
|
await gatherPurchasePending(tx, now, resp, onlyDue);
|
||||||
details: {
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!e.details) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "bug",
|
|
||||||
message:
|
|
||||||
"Exchange record does not have details, but no update in progress.",
|
|
||||||
details: {
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!e.wireInfo) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "bug",
|
|
||||||
message:
|
|
||||||
"Exchange record does not have wire info, but no update in progress.",
|
|
||||||
details: {
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ExchangeUpdateStatus.FETCH_KEYS:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "exchange-update",
|
|
||||||
stage: "fetch-keys",
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
lastError: e.lastError,
|
|
||||||
reason: e.updateReason || "unknown",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case ExchangeUpdateStatus.FETCH_WIRE:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "exchange-update",
|
|
||||||
stage: "fetch-wire",
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
lastError: e.lastError,
|
|
||||||
reason: e.updateReason || "unknown",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "bug",
|
|
||||||
message: "Unknown exchangeUpdateStatus",
|
|
||||||
details: {
|
|
||||||
exchangeBaseUrl: e.baseUrl,
|
|
||||||
exchangeUpdateStatus: e.updateStatus,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await tx.iter(Stores.reserves).forEach(reserve => {
|
|
||||||
const reserveType = reserve.bankWithdrawStatusUrl
|
|
||||||
? "taler-bank"
|
|
||||||
: "manual";
|
|
||||||
const now = getTimestampNow();
|
|
||||||
switch (reserve.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.DORMANT:
|
|
||||||
// nothing to report as pending
|
|
||||||
break;
|
|
||||||
case ReserveRecordStatus.WITHDRAWING:
|
|
||||||
case ReserveRecordStatus.UNCONFIRMED:
|
|
||||||
case ReserveRecordStatus.QUERYING_STATUS:
|
|
||||||
case ReserveRecordStatus.REGISTERING_BANK:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "reserve",
|
|
||||||
stage: reserve.reserveStatus,
|
|
||||||
timestampCreated: reserve.created,
|
|
||||||
reserveType,
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
});
|
|
||||||
if (reserve.created.t_ms < now.t_ms - 5000) {
|
|
||||||
minRetryDurationMs = 500;
|
|
||||||
} else if (reserve.created.t_ms < now.t_ms - 30000) {
|
|
||||||
minRetryDurationMs = 2000;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "reserve",
|
|
||||||
stage: reserve.reserveStatus,
|
|
||||||
timestampCreated: reserve.created,
|
|
||||||
reserveType,
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
bankWithdrawConfirmUrl: reserve.bankWithdrawConfirmUrl,
|
|
||||||
});
|
|
||||||
if (reserve.created.t_ms < now.t_ms - 5000) {
|
|
||||||
minRetryDurationMs = 500;
|
|
||||||
} else if (reserve.created.t_ms < now.t_ms - 30000) {
|
|
||||||
minRetryDurationMs = 2000;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "bug",
|
|
||||||
message: "Unknown reserve record status",
|
|
||||||
details: {
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
reserveStatus: reserve.reserveStatus,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.iter(Stores.refresh).forEach(r => {
|
|
||||||
if (r.finished) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let refreshStatus: string;
|
|
||||||
if (r.norevealIndex === undefined) {
|
|
||||||
refreshStatus = "melt";
|
|
||||||
} else {
|
|
||||||
refreshStatus = "reveal";
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "refresh",
|
|
||||||
oldCoinPub: r.meltCoinPub,
|
|
||||||
refreshStatus,
|
|
||||||
refreshOutputSize: r.newDenoms.length,
|
|
||||||
refreshSessionId: r.refreshSessionId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.iter(Stores.coins).forEach(coin => {
|
|
||||||
if (coin.status == CoinStatus.Dirty) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "dirty-coin",
|
|
||||||
coinPub: coin.coinPub,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.iter(Stores.withdrawalSession).forEach(ws => {
|
|
||||||
const numCoinsWithdrawn = ws.withdrawn.reduce(
|
|
||||||
(a, x) => a + (x ? 1 : 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const numCoinsTotal = ws.withdrawn.length;
|
|
||||||
if (numCoinsWithdrawn < numCoinsTotal) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "withdraw",
|
|
||||||
numCoinsTotal,
|
|
||||||
numCoinsWithdrawn,
|
|
||||||
source: ws.source,
|
|
||||||
withdrawSessionId: ws.withdrawSessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.iter(Stores.proposals).forEach((proposal) => {
|
|
||||||
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "proposal-choice",
|
|
||||||
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
|
|
||||||
proposalId: proposal.proposalId,
|
|
||||||
proposalTimestamp: proposal.timestamp,
|
|
||||||
});
|
|
||||||
} else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "proposal-download",
|
|
||||||
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
|
|
||||||
proposalId: proposal.proposalId,
|
|
||||||
proposalTimestamp: proposal.timestamp,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.iter(Stores.tips).forEach((tip) => {
|
|
||||||
if (tip.accepted && !tip.pickedUp) {
|
|
||||||
pendingOperations.push({
|
|
||||||
type: "tip",
|
|
||||||
merchantBaseUrl: tip.merchantBaseUrl,
|
|
||||||
tipId: tip.tipId,
|
|
||||||
merchantTipId: tip.merchantTipId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
return resp;
|
||||||
return {
|
|
||||||
pendingOperations,
|
|
||||||
nextRetryDelay: {
|
|
||||||
d_ms: minRetryDurationMs,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ import {
|
|||||||
RefreshPlanchetRecord,
|
RefreshPlanchetRecord,
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
|
initRetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
} from "../dbTypes";
|
} from "../dbTypes";
|
||||||
import { amountToPretty } from "../util/helpers";
|
import { amountToPretty } from "../util/helpers";
|
||||||
import {
|
import {
|
||||||
@ -36,6 +38,8 @@ import { InternalWalletState } from "./state";
|
|||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { getWithdrawDenomList } from "./withdraw";
|
import { getWithdrawDenomList } from "./withdraw";
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { updateExchangeFromUrl } from "./exchanges";
|
||||||
|
import { getTimestampNow, OperationError, NotificationType } from "../walletTypes";
|
||||||
|
import { guardOperationException } from "./errors";
|
||||||
|
|
||||||
const logger = new Logger("refresh.ts");
|
const logger = new Logger("refresh.ts");
|
||||||
|
|
||||||
@ -132,14 +136,16 @@ async function refreshMelt(
|
|||||||
if (rs.norevealIndex !== undefined) {
|
if (rs.norevealIndex !== undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (rs.finished) {
|
if (rs.finishedTimestamp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rs.norevealIndex = norevealIndex;
|
rs.norevealIndex = norevealIndex;
|
||||||
return rs;
|
return rs;
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.notifier.notify();
|
ws.notify({
|
||||||
|
type: NotificationType.RefreshMelted,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshReveal(
|
async function refreshReveal(
|
||||||
@ -225,16 +231,6 @@ async function refreshReveal(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const exchange = oneShotGet(
|
|
||||||
ws.db,
|
|
||||||
Stores.exchanges,
|
|
||||||
refreshSession.exchangeBaseUrl,
|
|
||||||
);
|
|
||||||
if (!exchange) {
|
|
||||||
console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const coins: CoinRecord[] = [];
|
const coins: CoinRecord[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < respJson.ev_sigs.length; i++) {
|
for (let i = 0; i < respJson.ev_sigs.length; i++) {
|
||||||
@ -271,31 +267,71 @@ async function refreshReveal(
|
|||||||
coins.push(coin);
|
coins.push(coin);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshSession.finished = true;
|
|
||||||
|
|
||||||
await runWithWriteTransaction(
|
await runWithWriteTransaction(
|
||||||
ws.db,
|
ws.db,
|
||||||
[Stores.coins, Stores.refresh],
|
[Stores.coins, Stores.refresh],
|
||||||
async tx => {
|
async tx => {
|
||||||
const rs = await tx.get(Stores.refresh, refreshSessionId);
|
const rs = await tx.get(Stores.refresh, refreshSessionId);
|
||||||
if (!rs) {
|
if (!rs) {
|
||||||
|
console.log("no refresh session found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (rs.finished) {
|
if (rs.finishedTimestamp) {
|
||||||
|
console.log("refresh session already finished");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
rs.finishedTimestamp = getTimestampNow();
|
||||||
|
rs.retryInfo = initRetryInfo(false);
|
||||||
for (let coin of coins) {
|
for (let coin of coins) {
|
||||||
await tx.put(Stores.coins, coin);
|
await tx.put(Stores.coins, coin);
|
||||||
}
|
}
|
||||||
await tx.put(Stores.refresh, refreshSession);
|
await tx.put(Stores.refresh, rs);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ws.notifier.notify();
|
console.log("refresh finished (end of reveal)");
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.RefreshRevealed,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function incrementRefreshRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
refreshSessionId: string,
|
||||||
|
err: OperationError | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
await runWithWriteTransaction(ws.db, [Stores.refresh], async tx => {
|
||||||
|
const r = await tx.get(Stores.refresh, refreshSessionId);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.retryInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
r.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(r.retryInfo);
|
||||||
|
r.lastError = err;
|
||||||
|
await tx.put(Stores.refresh, r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function processRefreshSession(
|
export async function processRefreshSession(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
refreshSessionId: string,
|
refreshSessionId: string,
|
||||||
|
) {
|
||||||
|
return ws.memoProcessRefresh.memo(refreshSessionId, async () => {
|
||||||
|
const onOpErr = (e: OperationError) =>
|
||||||
|
incrementRefreshRetry(ws, refreshSessionId, e);
|
||||||
|
return guardOperationException(
|
||||||
|
() => processRefreshSessionImpl(ws, refreshSessionId),
|
||||||
|
onOpErr,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processRefreshSessionImpl(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
refreshSessionId: string,
|
||||||
) {
|
) {
|
||||||
const refreshSession = await oneShotGet(
|
const refreshSession = await oneShotGet(
|
||||||
ws.db,
|
ws.db,
|
||||||
@ -305,7 +341,7 @@ export async function processRefreshSession(
|
|||||||
if (!refreshSession) {
|
if (!refreshSession) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (refreshSession.finished) {
|
if (refreshSession.finishedTimestamp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof refreshSession.norevealIndex !== "number") {
|
if (typeof refreshSession.norevealIndex !== "number") {
|
||||||
@ -376,7 +412,7 @@ export async function refresh(
|
|||||||
x.status = CoinStatus.Dormant;
|
x.status = CoinStatus.Dormant;
|
||||||
return x;
|
return x;
|
||||||
});
|
});
|
||||||
ws.notifier.notify();
|
ws.notify( { type: NotificationType.RefreshRefused });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,29 +424,32 @@ export async function refresh(
|
|||||||
oldDenom.feeRefresh,
|
oldDenom.feeRefresh,
|
||||||
);
|
);
|
||||||
|
|
||||||
function mutateCoin(c: CoinRecord): CoinRecord {
|
|
||||||
const r = Amounts.sub(c.currentAmount, refreshSession.valueWithFee);
|
|
||||||
if (r.saturated) {
|
|
||||||
// Something else must have written the coin value
|
|
||||||
throw TransactionAbort;
|
|
||||||
}
|
|
||||||
c.currentAmount = r.amount;
|
|
||||||
c.status = CoinStatus.Dormant;
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store refresh session and subtract refreshed amount from
|
// Store refresh session and subtract refreshed amount from
|
||||||
// coin in the same transaction.
|
// coin in the same transaction.
|
||||||
await runWithWriteTransaction(
|
await runWithWriteTransaction(
|
||||||
ws.db,
|
ws.db,
|
||||||
[Stores.refresh, Stores.coins],
|
[Stores.refresh, Stores.coins],
|
||||||
async tx => {
|
async tx => {
|
||||||
|
const c = await tx.get(Stores.coins, coin.coinPub);
|
||||||
|
if (!c) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c.status !== CoinStatus.Dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const r = Amounts.sub(c.currentAmount, refreshSession.valueWithFee);
|
||||||
|
if (r.saturated) {
|
||||||
|
console.log("can't refresh coin, no amount left");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c.currentAmount = r.amount;
|
||||||
|
c.status = CoinStatus.Dormant;
|
||||||
await tx.put(Stores.refresh, refreshSession);
|
await tx.put(Stores.refresh, refreshSession);
|
||||||
await tx.mutate(Stores.coins, coin.coinPub, mutateCoin);
|
await tx.put(Stores.coins, c);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
logger.info(`created refresh session ${refreshSession.refreshSessionId}`);
|
logger.info(`created refresh session ${refreshSession.refreshSessionId}`);
|
||||||
ws.notifier.notify();
|
ws.notify( { type: NotificationType.RefreshStarted });
|
||||||
|
|
||||||
await processRefreshSession(ws, refreshSession.refreshSessionId);
|
await processRefreshSession(ws, refreshSession.refreshSessionId);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
ConfirmReserveRequest,
|
ConfirmReserveRequest,
|
||||||
OperationError,
|
OperationError,
|
||||||
|
NotificationType,
|
||||||
} from "../walletTypes";
|
} from "../walletTypes";
|
||||||
import { canonicalizeBaseUrl } from "../util/helpers";
|
import { canonicalizeBaseUrl } from "../util/helpers";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
@ -29,6 +30,8 @@ import {
|
|||||||
CurrencyRecord,
|
CurrencyRecord,
|
||||||
Stores,
|
Stores,
|
||||||
WithdrawalSessionRecord,
|
WithdrawalSessionRecord,
|
||||||
|
initRetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
} from "../dbTypes";
|
} from "../dbTypes";
|
||||||
import {
|
import {
|
||||||
oneShotMutate,
|
oneShotMutate,
|
||||||
@ -42,13 +45,13 @@ import * as Amounts from "../util/amounts";
|
|||||||
import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
|
import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
|
||||||
import { WithdrawOperationStatusResponse, ReserveStatus } from "../talerTypes";
|
import { WithdrawOperationStatusResponse, ReserveStatus } from "../talerTypes";
|
||||||
import { assertUnreachable } from "../util/assertUnreachable";
|
import { assertUnreachable } from "../util/assertUnreachable";
|
||||||
import { OperationFailedAndReportedError } from "../wallet";
|
|
||||||
import { encodeCrock } from "../crypto/talerCrypto";
|
import { encodeCrock } from "../crypto/talerCrypto";
|
||||||
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
||||||
import {
|
import {
|
||||||
getVerifiedWithdrawDenomList,
|
getVerifiedWithdrawDenomList,
|
||||||
processWithdrawSession,
|
processWithdrawSession,
|
||||||
} from "./withdraw";
|
} from "./withdraw";
|
||||||
|
import { guardOperationException, OperationFailedAndReportedError } from "./errors";
|
||||||
|
|
||||||
const logger = new Logger("reserves.ts");
|
const logger = new Logger("reserves.ts");
|
||||||
|
|
||||||
@ -91,7 +94,9 @@ export async function createReserve(
|
|||||||
bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
|
bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
|
||||||
exchangeWire: req.exchangeWire,
|
exchangeWire: req.exchangeWire,
|
||||||
reserveStatus,
|
reserveStatus,
|
||||||
lastStatusQuery: undefined,
|
lastSuccessfulStatusQuery: undefined,
|
||||||
|
retryInfo: initRetryInfo(),
|
||||||
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const senderWire = req.senderWire;
|
const senderWire = req.senderWire;
|
||||||
@ -171,7 +176,7 @@ export async function createReserve(
|
|||||||
|
|
||||||
// Asynchronously process the reserve, but return
|
// Asynchronously process the reserve, but return
|
||||||
// to the caller already.
|
// to the caller already.
|
||||||
processReserve(ws, resp.reservePub).catch(e => {
|
processReserve(ws, resp.reservePub, true).catch(e => {
|
||||||
console.error("Processing reserve failed:", e);
|
console.error("Processing reserve failed:", e);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -188,18 +193,19 @@ export async function createReserve(
|
|||||||
export async function processReserve(
|
export async function processReserve(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
|
forceNow: boolean = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const p = ws.memoProcessReserve.find(reservePub);
|
return ws.memoProcessReserve.memo(reservePub, async () => {
|
||||||
if (p) {
|
const onOpError = (err: OperationError) =>
|
||||||
return p;
|
incrementReserveRetry(ws, reservePub, err);
|
||||||
} else {
|
await guardOperationException(
|
||||||
return ws.memoProcessReserve.put(
|
() => processReserveImpl(ws, reservePub, forceNow),
|
||||||
reservePub,
|
onOpError,
|
||||||
processReserveImpl(ws, reservePub),
|
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function registerReserveWithBank(
|
async function registerReserveWithBank(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
@ -235,6 +241,7 @@ async function registerReserveWithBank(
|
|||||||
}
|
}
|
||||||
r.timestampReserveInfoPosted = getTimestampNow();
|
r.timestampReserveInfoPosted = getTimestampNow();
|
||||||
r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
|
r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
|
||||||
|
r.retryInfo = initRetryInfo();
|
||||||
return r;
|
return r;
|
||||||
});
|
});
|
||||||
return processReserveBankStatus(ws, reservePub);
|
return processReserveBankStatus(ws, reservePub);
|
||||||
@ -243,6 +250,18 @@ async function registerReserveWithBank(
|
|||||||
export async function processReserveBankStatus(
|
export async function processReserveBankStatus(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const onOpError = (err: OperationError) =>
|
||||||
|
incrementReserveRetry(ws, reservePub, err);
|
||||||
|
await guardOperationException(
|
||||||
|
() => processReserveBankStatusImpl(ws, reservePub),
|
||||||
|
onOpError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processReserveBankStatusImpl(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
reservePub: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
|
let reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
|
||||||
switch (reserve?.reserveStatus) {
|
switch (reserve?.reserveStatus) {
|
||||||
@ -287,9 +306,10 @@ export async function processReserveBankStatus(
|
|||||||
const now = getTimestampNow();
|
const now = getTimestampNow();
|
||||||
r.timestampConfirmed = now;
|
r.timestampConfirmed = now;
|
||||||
r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
||||||
|
r.retryInfo = initRetryInfo();
|
||||||
return r;
|
return r;
|
||||||
});
|
});
|
||||||
await processReserveImpl(ws, reservePub);
|
await processReserveImpl(ws, reservePub, true);
|
||||||
} else {
|
} else {
|
||||||
await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
|
await oneShotMutate(ws.db, Stores.reserves, reservePub, r => {
|
||||||
switch (r.reserveStatus) {
|
switch (r.reserveStatus) {
|
||||||
@ -304,16 +324,24 @@ export async function processReserveBankStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setReserveError(
|
async function incrementReserveRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
err: OperationError,
|
err: OperationError | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const mut = (reserve: ReserveRecord) => {
|
await runWithWriteTransaction(ws.db, [Stores.reserves], async tx => {
|
||||||
reserve.lastError = err;
|
const r = await tx.get(Stores.reserves, reservePub);
|
||||||
return reserve;
|
if (!r) {
|
||||||
};
|
return;
|
||||||
await oneShotMutate(ws.db, Stores.reserves, reservePub, mut);
|
}
|
||||||
|
if (!r.retryInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
r.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(r.retryInfo);
|
||||||
|
r.lastError = err;
|
||||||
|
await tx.put(Stores.reserves, r);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -345,15 +373,11 @@ async function updateReserve(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.response?.status === 404) {
|
if (e.response?.status === 404) {
|
||||||
const m = "The exchange does not know about this reserve (yet).";
|
const m = "The exchange does not know about this reserve (yet).";
|
||||||
await setReserveError(ws, reservePub, {
|
await incrementReserveRetry(ws, reservePub, undefined);
|
||||||
type: "waiting",
|
return;
|
||||||
details: {},
|
|
||||||
message: "The exchange does not know about this reserve (yet).",
|
|
||||||
});
|
|
||||||
throw new OperationFailedAndReportedError(m);
|
|
||||||
} else {
|
} else {
|
||||||
const m = e.message;
|
const m = e.message;
|
||||||
await setReserveError(ws, reservePub, {
|
await incrementReserveRetry(ws, reservePub, {
|
||||||
type: "network",
|
type: "network",
|
||||||
details: {},
|
details: {},
|
||||||
message: m,
|
message: m,
|
||||||
@ -369,7 +393,7 @@ async function updateReserve(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: check / compare history!
|
// FIXME: check / compare history!
|
||||||
if (!r.lastStatusQuery) {
|
if (!r.lastSuccessfulStatusQuery) {
|
||||||
// FIXME: check if this matches initial expectations
|
// FIXME: check if this matches initial expectations
|
||||||
r.withdrawRemainingAmount = balance;
|
r.withdrawRemainingAmount = balance;
|
||||||
} else {
|
} else {
|
||||||
@ -392,22 +416,31 @@ async function updateReserve(
|
|||||||
// We're missing some money.
|
// We're missing some money.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.lastStatusQuery = getTimestampNow();
|
r.lastSuccessfulStatusQuery = getTimestampNow();
|
||||||
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
|
r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
|
||||||
|
r.retryInfo = initRetryInfo();
|
||||||
return r;
|
return r;
|
||||||
});
|
});
|
||||||
ws.notifier.notify();
|
ws.notify( { type: NotificationType.ReserveUpdated });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processReserveImpl(
|
async function processReserveImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
|
forceNow: boolean = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
|
const reserve = await oneShotGet(ws.db, Stores.reserves, reservePub);
|
||||||
if (!reserve) {
|
if (!reserve) {
|
||||||
console.log("not processing reserve: reserve does not exist");
|
console.log("not processing reserve: reserve does not exist");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!forceNow) {
|
||||||
|
const now = getTimestampNow();
|
||||||
|
if (reserve.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||||
|
logger.trace("processReserve retry not due yet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
|
`Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
|
||||||
);
|
);
|
||||||
@ -417,10 +450,10 @@ async function processReserveImpl(
|
|||||||
break;
|
break;
|
||||||
case ReserveRecordStatus.REGISTERING_BANK:
|
case ReserveRecordStatus.REGISTERING_BANK:
|
||||||
await processReserveBankStatus(ws, reservePub);
|
await processReserveBankStatus(ws, reservePub);
|
||||||
return processReserveImpl(ws, reservePub);
|
return processReserveImpl(ws, reservePub, true);
|
||||||
case ReserveRecordStatus.QUERYING_STATUS:
|
case ReserveRecordStatus.QUERYING_STATUS:
|
||||||
await updateReserve(ws, reservePub);
|
await updateReserve(ws, reservePub);
|
||||||
return processReserveImpl(ws, reservePub);
|
return processReserveImpl(ws, reservePub, true);
|
||||||
case ReserveRecordStatus.WITHDRAWING:
|
case ReserveRecordStatus.WITHDRAWING:
|
||||||
await depleteReserve(ws, reservePub);
|
await depleteReserve(ws, reservePub);
|
||||||
break;
|
break;
|
||||||
@ -448,12 +481,13 @@ export async function confirmReserve(
|
|||||||
}
|
}
|
||||||
reserve.timestampConfirmed = now;
|
reserve.timestampConfirmed = now;
|
||||||
reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
|
||||||
|
reserve.retryInfo = initRetryInfo();
|
||||||
return reserve;
|
return reserve;
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.notifier.notify();
|
ws.notify({ type: NotificationType.ReserveUpdated });
|
||||||
|
|
||||||
processReserve(ws, req.reservePub).catch(e => {
|
processReserve(ws, req.reservePub, true).catch(e => {
|
||||||
console.log("processing reserve failed:", e);
|
console.log("processing reserve failed:", e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -489,7 +523,7 @@ async function depleteReserve(
|
|||||||
logger.trace(`got denom list`);
|
logger.trace(`got denom list`);
|
||||||
if (denomsForWithdraw.length === 0) {
|
if (denomsForWithdraw.length === 0) {
|
||||||
const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
|
const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
|
||||||
await setReserveError(ws, reserve.reservePub, {
|
await incrementReserveRetry(ws, reserve.reservePub, {
|
||||||
type: "internal",
|
type: "internal",
|
||||||
message: m,
|
message: m,
|
||||||
details: {},
|
details: {},
|
||||||
@ -502,7 +536,8 @@ async function depleteReserve(
|
|||||||
|
|
||||||
const withdrawalSessionId = encodeCrock(randomBytes(32));
|
const withdrawalSessionId = encodeCrock(randomBytes(32));
|
||||||
|
|
||||||
const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value)).amount;
|
const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value))
|
||||||
|
.amount;
|
||||||
|
|
||||||
const withdrawalRecord: WithdrawalSessionRecord = {
|
const withdrawalRecord: WithdrawalSessionRecord = {
|
||||||
withdrawSessionId: withdrawalSessionId,
|
withdrawSessionId: withdrawalSessionId,
|
||||||
@ -517,6 +552,9 @@ async function depleteReserve(
|
|||||||
withdrawn: denomsForWithdraw.map(x => false),
|
withdrawn: denomsForWithdraw.map(x => false),
|
||||||
planchets: denomsForWithdraw.map(x => undefined),
|
planchets: denomsForWithdraw.map(x => undefined),
|
||||||
totalCoinValue,
|
totalCoinValue,
|
||||||
|
retryInfo: initRetryInfo(),
|
||||||
|
lastCoinErrors: denomsForWithdraw.map(x => undefined),
|
||||||
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalCoinWithdrawFee = Amounts.sum(
|
const totalCoinWithdrawFee = Amounts.sum(
|
||||||
@ -545,7 +583,7 @@ async function depleteReserve(
|
|||||||
r.withdrawRemainingAmount = remaining.amount;
|
r.withdrawRemainingAmount = remaining.amount;
|
||||||
r.withdrawAllocatedAmount = allocated.amount;
|
r.withdrawAllocatedAmount = allocated.amount;
|
||||||
r.reserveStatus = ReserveRecordStatus.DORMANT;
|
r.reserveStatus = ReserveRecordStatus.DORMANT;
|
||||||
|
r.retryInfo = initRetryInfo(false);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,8 +204,6 @@ export async function returnCoins(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ws.badge.showNotification();
|
|
||||||
ws.notifier.notify();
|
|
||||||
|
|
||||||
depositReturnedCoins(ws, coinsReturnRecord);
|
depositReturnedCoins(ws, coinsReturnRecord);
|
||||||
}
|
}
|
||||||
@ -269,6 +267,5 @@ async function depositReturnedCoins(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await oneShotPut(ws.db, Stores.coinsReturns, currentCrr);
|
await oneShotPut(ws.db, Stores.coinsReturns, currentCrr);
|
||||||
ws.notifier.notify();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,19 +15,54 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpRequestLibrary } from "../util/http";
|
import { HttpRequestLibrary } from "../util/http";
|
||||||
import { Badge, Notifier, NextUrlResult } from "../walletTypes";
|
import {
|
||||||
|
NextUrlResult,
|
||||||
|
WalletBalance,
|
||||||
|
PendingOperationsResponse,
|
||||||
|
WalletNotification,
|
||||||
|
} from "../walletTypes";
|
||||||
import { SpeculativePayData } from "./pay";
|
import { SpeculativePayData } from "./pay";
|
||||||
import { CryptoApi } from "../crypto/cryptoApi";
|
import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi";
|
||||||
import { AsyncOpMemo } from "../util/asyncMemo";
|
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo";
|
||||||
|
import { Logger } from "../util/logging";
|
||||||
|
|
||||||
export interface InternalWalletState {
|
type NotificationListener = (n: WalletNotification) => void;
|
||||||
db: IDBDatabase;
|
|
||||||
http: HttpRequestLibrary;
|
const logger = new Logger("state.ts");
|
||||||
badge: Badge;
|
|
||||||
notifier: Notifier;
|
export class InternalWalletState {
|
||||||
|
speculativePayData: SpeculativePayData | undefined = undefined;
|
||||||
|
cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
|
||||||
|
memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
|
memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
|
memoGetPending: AsyncOpMemoSingle<
|
||||||
|
PendingOperationsResponse
|
||||||
|
> = new AsyncOpMemoSingle();
|
||||||
|
memoGetBalance: AsyncOpMemoSingle<WalletBalance> = new AsyncOpMemoSingle();
|
||||||
|
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
cryptoApi: CryptoApi;
|
cryptoApi: CryptoApi;
|
||||||
speculativePayData: SpeculativePayData | undefined;
|
|
||||||
cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult };
|
listeners: NotificationListener[] = [];
|
||||||
memoProcessReserve: AsyncOpMemo<void>;
|
|
||||||
memoMakePlanchet: AsyncOpMemo<void>;
|
constructor(
|
||||||
}
|
public db: IDBDatabase,
|
||||||
|
public http: HttpRequestLibrary,
|
||||||
|
cryptoWorkerFactory: CryptoWorkerFactory,
|
||||||
|
) {
|
||||||
|
this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public notify(n: WalletNotification) {
|
||||||
|
logger.trace("Notification", n);
|
||||||
|
for (const l of this.listeners) {
|
||||||
|
const nc = JSON.parse(JSON.stringify(n));
|
||||||
|
setImmediate(() => {
|
||||||
|
l(nc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addNotificationListener(f: (n: WalletNotification) => void): void {
|
||||||
|
this.listeners.push(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -18,14 +18,15 @@
|
|||||||
import { oneShotGet, oneShotPut, oneShotMutate, runWithWriteTransaction } from "../util/query";
|
import { oneShotGet, oneShotPut, oneShotMutate, runWithWriteTransaction } from "../util/query";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { parseTipUri } from "../util/taleruri";
|
import { parseTipUri } from "../util/taleruri";
|
||||||
import { TipStatus, getTimestampNow } from "../walletTypes";
|
import { TipStatus, getTimestampNow, OperationError } from "../walletTypes";
|
||||||
import { TipPickupGetResponse, TipPlanchetDetail, TipResponse } from "../talerTypes";
|
import { TipPickupGetResponse, TipPlanchetDetail, TipResponse } from "../talerTypes";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import { Stores, PlanchetRecord, WithdrawalSessionRecord } from "../dbTypes";
|
import { Stores, PlanchetRecord, WithdrawalSessionRecord, initRetryInfo, updateRetryInfoTimeout } from "../dbTypes";
|
||||||
import { getWithdrawDetailsForAmount, getVerifiedWithdrawDenomList, processWithdrawSession } from "./withdraw";
|
import { getWithdrawDetailsForAmount, getVerifiedWithdrawDenomList, processWithdrawSession } from "./withdraw";
|
||||||
import { getTalerStampSec } from "../util/helpers";
|
import { getTalerStampSec } from "../util/helpers";
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { updateExchangeFromUrl } from "./exchanges";
|
||||||
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
||||||
|
import { guardOperationException } from "./errors";
|
||||||
|
|
||||||
|
|
||||||
export async function getTipStatus(
|
export async function getTipStatus(
|
||||||
@ -74,12 +75,14 @@ export async function getTipStatus(
|
|||||||
pickedUp: false,
|
pickedUp: false,
|
||||||
planchets: undefined,
|
planchets: undefined,
|
||||||
response: undefined,
|
response: undefined,
|
||||||
timestamp: getTimestampNow(),
|
createdTimestamp: getTimestampNow(),
|
||||||
merchantTipId: res.merchantTipId,
|
merchantTipId: res.merchantTipId,
|
||||||
totalFees: Amounts.add(
|
totalFees: Amounts.add(
|
||||||
withdrawDetails.overhead,
|
withdrawDetails.overhead,
|
||||||
withdrawDetails.withdrawFee,
|
withdrawDetails.withdrawFee,
|
||||||
).amount,
|
).amount,
|
||||||
|
retryInfo: initRetryInfo(),
|
||||||
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
await oneShotPut(ws.db, Stores.tips, tipRecord);
|
await oneShotPut(ws.db, Stores.tips, tipRecord);
|
||||||
}
|
}
|
||||||
@ -101,9 +104,37 @@ export async function getTipStatus(
|
|||||||
return tipStatus;
|
return tipStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function incrementTipRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
refreshSessionId: string,
|
||||||
|
err: OperationError | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
await runWithWriteTransaction(ws.db, [Stores.tips], async tx => {
|
||||||
|
const t = await tx.get(Stores.tips, refreshSessionId);
|
||||||
|
if (!t) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!t.retryInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
t.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(t.retryInfo);
|
||||||
|
t.lastError = err;
|
||||||
|
await tx.put(Stores.tips, t);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function processTip(
|
export async function processTip(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tipId: string,
|
tipId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const onOpErr = (e: OperationError) => incrementTipRetry(ws, tipId, e);
|
||||||
|
await guardOperationException(() => processTipImpl(ws, tipId), onOpErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processTipImpl(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
tipId: string,
|
||||||
) {
|
) {
|
||||||
let tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
|
let tipRecord = await oneShotGet(ws.db, Stores.tips, tipId);
|
||||||
if (!tipRecord) {
|
if (!tipRecord) {
|
||||||
@ -205,6 +236,10 @@ export async function processTip(
|
|||||||
rawWithdrawalAmount: tipRecord.amount,
|
rawWithdrawalAmount: tipRecord.amount,
|
||||||
withdrawn: planchets.map((x) => false),
|
withdrawn: planchets.map((x) => false),
|
||||||
totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
|
totalCoinValue: Amounts.sum(planchets.map((p) => p.coinValue)).amount,
|
||||||
|
lastCoinErrors: planchets.map((x) => undefined),
|
||||||
|
retryInfo: initRetryInfo(),
|
||||||
|
finishTimestamp: undefined,
|
||||||
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -217,6 +252,7 @@ export async function processTip(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tr.pickedUp = true;
|
tr.pickedUp = true;
|
||||||
|
tr.retryInfo = initRetryInfo(false);
|
||||||
|
|
||||||
await tx.put(Stores.tips, tr);
|
await tx.put(Stores.tips, tr);
|
||||||
await tx.put(Stores.withdrawalSession, withdrawalSession);
|
await tx.put(Stores.withdrawalSession, withdrawalSession);
|
||||||
@ -224,8 +260,6 @@ export async function processTip(
|
|||||||
|
|
||||||
await processWithdrawSession(ws, withdrawalSessionId);
|
await processWithdrawSession(ws, withdrawalSessionId);
|
||||||
|
|
||||||
ws.notifier.notify();
|
|
||||||
ws.badge.showNotification();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ import {
|
|||||||
CoinStatus,
|
CoinStatus,
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
PlanchetRecord,
|
PlanchetRecord,
|
||||||
|
initRetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
} from "../dbTypes";
|
} from "../dbTypes";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import {
|
import {
|
||||||
@ -30,6 +32,8 @@ import {
|
|||||||
DownloadedWithdrawInfo,
|
DownloadedWithdrawInfo,
|
||||||
ReserveCreationInfo,
|
ReserveCreationInfo,
|
||||||
WithdrawDetails,
|
WithdrawDetails,
|
||||||
|
OperationError,
|
||||||
|
NotificationType,
|
||||||
} from "../walletTypes";
|
} from "../walletTypes";
|
||||||
import { WithdrawOperationStatusResponse } from "../talerTypes";
|
import { WithdrawOperationStatusResponse } from "../talerTypes";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
@ -51,6 +55,7 @@ import { createReserve, processReserveBankStatus } from "./reserves";
|
|||||||
import { WALLET_PROTOCOL_VERSION } from "../wallet";
|
import { WALLET_PROTOCOL_VERSION } from "../wallet";
|
||||||
|
|
||||||
import * as LibtoolVersion from "../util/libtoolVersion";
|
import * as LibtoolVersion from "../util/libtoolVersion";
|
||||||
|
import { guardOperationException } from "./errors";
|
||||||
|
|
||||||
const logger = new Logger("withdraw.ts");
|
const logger = new Logger("withdraw.ts");
|
||||||
|
|
||||||
@ -143,12 +148,9 @@ export async function acceptWithdrawal(
|
|||||||
senderWire: withdrawInfo.senderWire,
|
senderWire: withdrawInfo.senderWire,
|
||||||
exchangeWire: exchangeWire,
|
exchangeWire: exchangeWire,
|
||||||
});
|
});
|
||||||
ws.badge.showNotification();
|
|
||||||
ws.notifier.notify();
|
|
||||||
// We do this here, as the reserve should be registered before we return,
|
// We do this here, as the reserve should be registered before we return,
|
||||||
// so that we can redirect the user to the bank's status page.
|
// so that we can redirect the user to the bank's status page.
|
||||||
await processReserveBankStatus(ws, reserve.reservePub);
|
await processReserveBankStatus(ws, reserve.reservePub);
|
||||||
ws.notifier.notify();
|
|
||||||
console.log("acceptWithdrawal: returning");
|
console.log("acceptWithdrawal: returning");
|
||||||
return {
|
return {
|
||||||
reservePub: reserve.reservePub,
|
reservePub: reserve.reservePub,
|
||||||
@ -234,6 +236,12 @@ async function processPlanchet(
|
|||||||
planchet.denomPub,
|
planchet.denomPub,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const isValid = await ws.cryptoApi.rsaVerify(planchet.coinPub, denomSig, planchet.denomPub);
|
||||||
|
if (!isValid) {
|
||||||
|
throw Error("invalid RSA signature by the exchange");
|
||||||
|
}
|
||||||
|
|
||||||
const coin: CoinRecord = {
|
const coin: CoinRecord = {
|
||||||
blindingKey: planchet.blindingKey,
|
blindingKey: planchet.blindingKey,
|
||||||
coinPriv: planchet.coinPriv,
|
coinPriv: planchet.coinPriv,
|
||||||
@ -249,6 +257,9 @@ async function processPlanchet(
|
|||||||
withdrawSessionId: withdrawalSessionId,
|
withdrawSessionId: withdrawalSessionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let withdrawSessionFinished = false;
|
||||||
|
let reserveDepleted = false;
|
||||||
|
|
||||||
await runWithWriteTransaction(
|
await runWithWriteTransaction(
|
||||||
ws.db,
|
ws.db,
|
||||||
[Stores.coins, Stores.withdrawalSession, Stores.reserves],
|
[Stores.coins, Stores.withdrawalSession, Stores.reserves],
|
||||||
@ -262,6 +273,18 @@ async function processPlanchet(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ws.withdrawn[coinIdx] = true;
|
ws.withdrawn[coinIdx] = true;
|
||||||
|
ws.lastCoinErrors[coinIdx] = undefined;
|
||||||
|
let numDone = 0;
|
||||||
|
for (let i = 0; i < ws.withdrawn.length; i++) {
|
||||||
|
if (ws.withdrawn[i]) {
|
||||||
|
numDone++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (numDone === ws.denoms.length) {
|
||||||
|
ws.finishTimestamp = getTimestampNow();
|
||||||
|
ws.retryInfo = initRetryInfo(false);
|
||||||
|
withdrawSessionFinished = true;
|
||||||
|
}
|
||||||
await tx.put(Stores.withdrawalSession, ws);
|
await tx.put(Stores.withdrawalSession, ws);
|
||||||
if (!planchet.isFromTip) {
|
if (!planchet.isFromTip) {
|
||||||
const r = await tx.get(Stores.reserves, planchet.reservePub);
|
const r = await tx.get(Stores.reserves, planchet.reservePub);
|
||||||
@ -270,14 +293,29 @@ async function processPlanchet(
|
|||||||
r.withdrawCompletedAmount,
|
r.withdrawCompletedAmount,
|
||||||
Amounts.add(denom.value, denom.feeWithdraw).amount,
|
Amounts.add(denom.value, denom.feeWithdraw).amount,
|
||||||
).amount;
|
).amount;
|
||||||
|
if (Amounts.cmp(r.withdrawCompletedAmount, r.withdrawAllocatedAmount) == 0) {
|
||||||
|
reserveDepleted = true;
|
||||||
|
}
|
||||||
await tx.put(Stores.reserves, r);
|
await tx.put(Stores.reserves, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await tx.add(Stores.coins, coin);
|
await tx.add(Stores.coins, coin);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ws.notifier.notify();
|
|
||||||
logger.trace(`withdraw of one coin ${coin.coinPub} finished`);
|
if (withdrawSessionFinished) {
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.WithdrawSessionFinished,
|
||||||
|
withdrawSessionId: withdrawalSessionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reserveDepleted && withdrawalSession.source.type === "reserve") {
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.ReserveDepleted,
|
||||||
|
reservePub: withdrawalSession.source.reservePub,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -437,27 +475,50 @@ async function processWithdrawCoin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!withdrawalSession.planchets[coinIndex]) {
|
if (!withdrawalSession.planchets[coinIndex]) {
|
||||||
logger.trace("creating planchet for coin", coinIndex);
|
|
||||||
const key = `${withdrawalSessionId}-${coinIndex}`;
|
const key = `${withdrawalSessionId}-${coinIndex}`;
|
||||||
const p = ws.memoMakePlanchet.find(key);
|
await ws.memoMakePlanchet.memo(key, async () => {
|
||||||
if (p) {
|
logger.trace("creating planchet for coin", coinIndex);
|
||||||
await p;
|
return makePlanchet(ws, withdrawalSessionId, coinIndex);
|
||||||
} else {
|
});
|
||||||
ws.memoMakePlanchet.put(
|
|
||||||
key,
|
|
||||||
makePlanchet(ws, withdrawalSessionId, coinIndex),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await makePlanchet(ws, withdrawalSessionId, coinIndex);
|
|
||||||
logger.trace("done creating planchet for coin", coinIndex);
|
|
||||||
}
|
}
|
||||||
await processPlanchet(ws, withdrawalSessionId, coinIndex);
|
await processPlanchet(ws, withdrawalSessionId, coinIndex);
|
||||||
logger.trace("starting withdraw for coin", coinIndex);
|
}
|
||||||
|
|
||||||
|
async function incrementWithdrawalRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
withdrawalSessionId: string,
|
||||||
|
err: OperationError | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
await runWithWriteTransaction(ws.db, [Stores.withdrawalSession], async tx => {
|
||||||
|
const wsr = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
|
||||||
|
if (!wsr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!wsr.retryInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wsr.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(wsr.retryInfo);
|
||||||
|
wsr.lastError = err;
|
||||||
|
await tx.put(Stores.withdrawalSession, wsr);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processWithdrawSession(
|
export async function processWithdrawSession(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalSessionId: string,
|
withdrawalSessionId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const onOpErr = (e: OperationError) =>
|
||||||
|
incrementWithdrawalRetry(ws, withdrawalSessionId, e);
|
||||||
|
await guardOperationException(
|
||||||
|
() => processWithdrawSessionImpl(ws, withdrawalSessionId),
|
||||||
|
onOpErr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processWithdrawSessionImpl(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
withdrawalSessionId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.trace("processing withdraw session", withdrawalSessionId);
|
logger.trace("processing withdraw session", withdrawalSessionId);
|
||||||
const withdrawalSession = await oneShotGet(
|
const withdrawalSession = await oneShotGet(
|
||||||
@ -474,7 +535,6 @@ export async function processWithdrawSession(
|
|||||||
processWithdrawCoin(ws, withdrawalSessionId, i),
|
processWithdrawCoin(ws, withdrawalSessionId, i),
|
||||||
);
|
);
|
||||||
await Promise.all(ps);
|
await Promise.all(ps);
|
||||||
ws.badge.showNotification();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
202
src/wallet.ts
202
src/wallet.ts
@ -22,7 +22,7 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { CryptoApi, CryptoWorkerFactory } from "./crypto/cryptoApi";
|
import { CryptoApi, CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
|
||||||
import { HttpRequestLibrary } from "./util/http";
|
import { HttpRequestLibrary } from "./util/http";
|
||||||
import {
|
import {
|
||||||
oneShotPut,
|
oneShotPut,
|
||||||
@ -49,6 +49,7 @@ import {
|
|||||||
processDownloadProposal,
|
processDownloadProposal,
|
||||||
applyRefund,
|
applyRefund,
|
||||||
getFullRefundFees,
|
getFullRefundFees,
|
||||||
|
processPurchaseImpl,
|
||||||
} from "./wallet-impl/pay";
|
} from "./wallet-impl/pay";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -65,14 +66,12 @@ import {
|
|||||||
} from "./dbTypes";
|
} from "./dbTypes";
|
||||||
import { MerchantRefundPermission } from "./talerTypes";
|
import { MerchantRefundPermission } from "./talerTypes";
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
BenchmarkResult,
|
BenchmarkResult,
|
||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
ConfirmReserveRequest,
|
ConfirmReserveRequest,
|
||||||
CreateReserveRequest,
|
CreateReserveRequest,
|
||||||
CreateReserveResponse,
|
CreateReserveResponse,
|
||||||
HistoryEvent,
|
HistoryEvent,
|
||||||
Notifier,
|
|
||||||
ReturnCoinsRequest,
|
ReturnCoinsRequest,
|
||||||
SenderWireInfos,
|
SenderWireInfos,
|
||||||
TipStatus,
|
TipStatus,
|
||||||
@ -85,6 +84,8 @@ import {
|
|||||||
PendingOperationInfo,
|
PendingOperationInfo,
|
||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
HistoryQuery,
|
HistoryQuery,
|
||||||
|
WalletNotification,
|
||||||
|
NotificationType,
|
||||||
} from "./walletTypes";
|
} from "./walletTypes";
|
||||||
import { Logger } from "./util/logging";
|
import { Logger } from "./util/logging";
|
||||||
|
|
||||||
@ -97,8 +98,6 @@ import {
|
|||||||
} from "./wallet-impl/exchanges";
|
} from "./wallet-impl/exchanges";
|
||||||
import { processReserve } from "./wallet-impl/reserves";
|
import { processReserve } from "./wallet-impl/reserves";
|
||||||
|
|
||||||
import { AsyncOpMemo } from "./util/asyncMemo";
|
|
||||||
|
|
||||||
import { InternalWalletState } from "./wallet-impl/state";
|
import { InternalWalletState } from "./wallet-impl/state";
|
||||||
import { createReserve, confirmReserve } from "./wallet-impl/reserves";
|
import { createReserve, confirmReserve } from "./wallet-impl/reserves";
|
||||||
import { processRefreshSession, refresh } from "./wallet-impl/refresh";
|
import { processRefreshSession, refresh } from "./wallet-impl/refresh";
|
||||||
@ -111,6 +110,7 @@ import { returnCoins } from "./wallet-impl/return";
|
|||||||
import { payback } from "./wallet-impl/payback";
|
import { payback } from "./wallet-impl/payback";
|
||||||
import { TimerGroup } from "./util/timer";
|
import { TimerGroup } from "./util/timer";
|
||||||
import { AsyncCondition } from "./util/promiseUtils";
|
import { AsyncCondition } from "./util/promiseUtils";
|
||||||
|
import { AsyncOpMemoSingle } from "./util/asyncMemo";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wallet protocol version spoken with the exchange
|
* Wallet protocol version spoken with the exchange
|
||||||
@ -137,18 +137,6 @@ const builtinCurrencies: CurrencyRecord[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* This error is thrown when an
|
|
||||||
*/
|
|
||||||
export class OperationFailedAndReportedError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
|
|
||||||
// Set the prototype explicitly.
|
|
||||||
Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger = new Logger("wallet.ts");
|
const logger = new Logger("wallet.ts");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,41 +147,18 @@ export class Wallet {
|
|||||||
private timerGroup: TimerGroup = new TimerGroup();
|
private timerGroup: TimerGroup = new TimerGroup();
|
||||||
private latch = new AsyncCondition();
|
private latch = new AsyncCondition();
|
||||||
private stopped: boolean = false;
|
private stopped: boolean = false;
|
||||||
|
private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
|
||||||
|
|
||||||
get db(): IDBDatabase {
|
get db(): IDBDatabase {
|
||||||
return this.ws.db;
|
return this.ws.db;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get badge(): Badge {
|
|
||||||
return this.ws.badge;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get cryptoApi(): CryptoApi {
|
|
||||||
return this.ws.cryptoApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get notifier(): Notifier {
|
|
||||||
return this.ws.notifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
db: IDBDatabase,
|
db: IDBDatabase,
|
||||||
http: HttpRequestLibrary,
|
http: HttpRequestLibrary,
|
||||||
badge: Badge,
|
|
||||||
notifier: Notifier,
|
|
||||||
cryptoWorkerFactory: CryptoWorkerFactory,
|
cryptoWorkerFactory: CryptoWorkerFactory,
|
||||||
) {
|
) {
|
||||||
this.ws = {
|
this.ws = new InternalWalletState(db, http, cryptoWorkerFactory);
|
||||||
badge,
|
|
||||||
cachedNextUrl: {},
|
|
||||||
cryptoApi: new CryptoApi(cryptoWorkerFactory),
|
|
||||||
db,
|
|
||||||
http,
|
|
||||||
notifier,
|
|
||||||
speculativePayData: undefined,
|
|
||||||
memoProcessReserve: new AsyncOpMemo<void>(),
|
|
||||||
memoMakePlanchet: new AsyncOpMemo<void>(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getExchangePaytoUri(exchangeBaseUrl: string, supportedTargetTypes: string[]) {
|
getExchangePaytoUri(exchangeBaseUrl: string, supportedTargetTypes: string[]) {
|
||||||
@ -204,6 +169,10 @@ export class Wallet {
|
|||||||
return getWithdrawDetailsForAmount(this.ws, baseUrl, amount);
|
return getWithdrawDetailsForAmount(this.ws, baseUrl, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addNotificationListener(f: (n: WalletNotification) => void): void {
|
||||||
|
this.ws.addNotificationListener(f);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute one operation based on the pending operation info record.
|
* Execute one operation based on the pending operation info record.
|
||||||
*/
|
*/
|
||||||
@ -213,6 +182,7 @@ export class Wallet {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
switch (pending.type) {
|
switch (pending.type) {
|
||||||
case "bug":
|
case "bug":
|
||||||
|
// Nothing to do, will just be displayed to the user
|
||||||
return;
|
return;
|
||||||
case "dirty-coin":
|
case "dirty-coin":
|
||||||
await refresh(this.ws, pending.coinPub);
|
await refresh(this.ws, pending.coinPub);
|
||||||
@ -224,7 +194,7 @@ export class Wallet {
|
|||||||
await processRefreshSession(this.ws, pending.refreshSessionId);
|
await processRefreshSession(this.ws, pending.refreshSessionId);
|
||||||
break;
|
break;
|
||||||
case "reserve":
|
case "reserve":
|
||||||
await processReserve(this.ws, pending.reservePub);
|
await processReserve(this.ws, pending.reservePub, forceNow);
|
||||||
break;
|
break;
|
||||||
case "withdraw":
|
case "withdraw":
|
||||||
await processWithdrawSession(this.ws, pending.withdrawSessionId);
|
await processWithdrawSession(this.ws, pending.withdrawSessionId);
|
||||||
@ -239,6 +209,7 @@ export class Wallet {
|
|||||||
await processTip(this.ws, pending.tipId);
|
await processTip(this.ws, pending.tipId);
|
||||||
break;
|
break;
|
||||||
case "pay":
|
case "pay":
|
||||||
|
await processPurchaseImpl(this.ws, pending.proposalId);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
assertUnreachable(pending);
|
assertUnreachable(pending);
|
||||||
@ -249,7 +220,8 @@ export class Wallet {
|
|||||||
* Process pending operations.
|
* Process pending operations.
|
||||||
*/
|
*/
|
||||||
public async runPending(forceNow: boolean = false): Promise<void> {
|
public async runPending(forceNow: boolean = false): Promise<void> {
|
||||||
const pendingOpsResponse = await this.getPendingOperations();
|
const onlyDue = !forceNow;
|
||||||
|
const pendingOpsResponse = await this.getPendingOperations(onlyDue);
|
||||||
for (const p of pendingOpsResponse.pendingOperations) {
|
for (const p of pendingOpsResponse.pendingOperations) {
|
||||||
try {
|
try {
|
||||||
await this.processOnePendingOperation(p, forceNow);
|
await this.processOnePendingOperation(p, forceNow);
|
||||||
@ -260,54 +232,96 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process pending operations and wait for scheduled operations in
|
* Run the wallet until there are no more pending operations that give
|
||||||
* a loop until the wallet is stopped explicitly.
|
* liveness left. The wallet will be in a stopped state when this function
|
||||||
|
* returns without resolving to an exception.
|
||||||
*/
|
*/
|
||||||
public async runLoopScheduledRetries(): Promise<void> {
|
public async runUntilDone(): Promise<void> {
|
||||||
while (!this.stopped) {
|
const p = new Promise((resolve, reject) => {
|
||||||
console.log("running wallet retry loop iteration");
|
// Run this asynchronously
|
||||||
let pending = await this.getPendingOperations();
|
this.addNotificationListener(n => {
|
||||||
console.log("waiting for", pending.nextRetryDelay);
|
if (
|
||||||
const timeout = this.timerGroup.resolveAfter(pending.nextRetryDelay.d_ms);
|
n.type === NotificationType.WaitingForRetry &&
|
||||||
await Promise.race([timeout, this.latch.wait()]);
|
n.numGivingLiveness == 0
|
||||||
pending = await this.getPendingOperations();
|
) {
|
||||||
for (const p of pending.pendingOperations) {
|
logger.trace("no liveness-giving operations left, stopping");
|
||||||
try {
|
this.stop();
|
||||||
this.processOnePendingOperation(p);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
this.runRetryLoop().catch(e => {
|
||||||
|
console.log("exception in wallet retry loop");
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await p;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run until all coins have been withdrawn from the given reserve,
|
* Process pending operations and wait for scheduled operations in
|
||||||
* or an error has occured.
|
* a loop until the wallet is stopped explicitly.
|
||||||
*/
|
*/
|
||||||
public async runUntilReserveDepleted(reservePub: string) {
|
public async runRetryLoop(): Promise<void> {
|
||||||
while (true) {
|
// Make sure we only run one main loop at a time.
|
||||||
const r = await this.getPendingOperations();
|
return this.memoRunRetryLoop.memo(async () => {
|
||||||
const allPending = r.pendingOperations;
|
try {
|
||||||
const relevantPending = allPending.filter(x => {
|
await this.runRetryLoopImpl();
|
||||||
switch (x.type) {
|
} catch (e) {
|
||||||
case "reserve":
|
console.error("error during retry loop execution", e);
|
||||||
return x.reservePub === reservePub;
|
throw e;
|
||||||
case "withdraw":
|
|
||||||
return (
|
|
||||||
x.source.type === "reserve" && x.source.reservePub === reservePub
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (relevantPending.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
for (const p of relevantPending) {
|
});
|
||||||
await this.processOnePendingOperation(p);
|
}
|
||||||
|
|
||||||
|
private async runRetryLoopImpl(): Promise<void> {
|
||||||
|
while (!this.stopped) {
|
||||||
|
console.log("running wallet retry loop iteration");
|
||||||
|
let pending = await this.getPendingOperations(true);
|
||||||
|
if (pending.pendingOperations.length === 0) {
|
||||||
|
const allPending = await this.getPendingOperations(false);
|
||||||
|
let numPending = 0;
|
||||||
|
let numGivingLiveness = 0;
|
||||||
|
for (const p of allPending.pendingOperations) {
|
||||||
|
numPending++;
|
||||||
|
if (p.givesLifeness) {
|
||||||
|
numGivingLiveness++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let timeout;
|
||||||
|
if (
|
||||||
|
allPending.pendingOperations.length === 0 ||
|
||||||
|
allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER
|
||||||
|
) {
|
||||||
|
// Wait forever
|
||||||
|
timeout = new Promise(() => {});
|
||||||
|
console.log("waiting forever");
|
||||||
|
} else {
|
||||||
|
console.log("waiting for timeout", pending.nextRetryDelay);
|
||||||
|
timeout = this.timerGroup.resolveAfter(
|
||||||
|
allPending.nextRetryDelay.d_ms,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.ws.notify({
|
||||||
|
type: NotificationType.WaitingForRetry,
|
||||||
|
numGivingLiveness,
|
||||||
|
numPending,
|
||||||
|
});
|
||||||
|
await Promise.race([timeout, this.latch.wait()]);
|
||||||
|
console.log("timeout done");
|
||||||
|
} else {
|
||||||
|
logger.trace("running pending operations that are due");
|
||||||
|
// FIXME: maybe be a bit smarter about executing these
|
||||||
|
// opeations in parallel?
|
||||||
|
for (const p of pending.pendingOperations) {
|
||||||
|
try {
|
||||||
|
console.log("running", p);
|
||||||
|
await this.processOnePendingOperation(p);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.trace("exiting wallet retry loop");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -429,7 +443,6 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if and how an exchange is trusted and/or audited.
|
* Check if and how an exchange is trusted and/or audited.
|
||||||
*/
|
*/
|
||||||
@ -466,7 +479,7 @@ export class Wallet {
|
|||||||
* Get detailed balance information, sliced by exchange and by currency.
|
* Get detailed balance information, sliced by exchange and by currency.
|
||||||
*/
|
*/
|
||||||
async getBalances(): Promise<WalletBalance> {
|
async getBalances(): Promise<WalletBalance> {
|
||||||
return getBalances(this.ws);
|
return this.ws.memoGetBalance.memo(() => getBalances(this.ws));
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh(oldCoinPub: string, force: boolean = false): Promise<void> {
|
async refresh(oldCoinPub: string, force: boolean = false): Promise<void> {
|
||||||
@ -488,8 +501,12 @@ export class Wallet {
|
|||||||
return getHistory(this.ws, historyQuery);
|
return getHistory(this.ws, historyQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPendingOperations(): Promise<PendingOperationsResponse> {
|
async getPendingOperations(
|
||||||
return getPendingOperations(this.ws);
|
onlyDue: boolean = false,
|
||||||
|
): Promise<PendingOperationsResponse> {
|
||||||
|
return this.ws.memoGetPending.memo(() =>
|
||||||
|
getPendingOperations(this.ws, onlyDue),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
|
async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
|
||||||
@ -517,7 +534,6 @@ export class Wallet {
|
|||||||
async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
|
async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
|
||||||
logger.trace("updating currency to", currencyRecord);
|
logger.trace("updating currency to", currencyRecord);
|
||||||
await oneShotPut(this.db, Stores.currencies, currencyRecord);
|
await oneShotPut(this.db, Stores.currencies, currencyRecord);
|
||||||
this.notifier.notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
|
async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
|
||||||
@ -552,7 +568,7 @@ export class Wallet {
|
|||||||
stop() {
|
stop() {
|
||||||
this.stopped = true;
|
this.stopped = true;
|
||||||
this.timerGroup.stopCurrentAndFutureTimers();
|
this.timerGroup.stopCurrentAndFutureTimers();
|
||||||
this.cryptoApi.stop();
|
this.ws.cryptoApi.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSenderWireInfos(): Promise<SenderWireInfos> {
|
async getSenderWireInfos(): Promise<SenderWireInfos> {
|
||||||
@ -693,17 +709,13 @@ export class Wallet {
|
|||||||
const totalFees = totalRefundFees;
|
const totalFees = totalRefundFees;
|
||||||
return {
|
return {
|
||||||
contractTerms: purchase.contractTerms,
|
contractTerms: purchase.contractTerms,
|
||||||
hasRefund: purchase.timestamp_refund !== undefined,
|
hasRefund: purchase.lastRefundTimestamp !== undefined,
|
||||||
totalRefundAmount: totalRefundAmount,
|
totalRefundAmount: totalRefundAmount,
|
||||||
totalRefundAndRefreshFees: totalFees,
|
totalRefundAndRefreshFees: totalFees,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
clearNotification(): void {
|
|
||||||
this.badge.clearNotification();
|
|
||||||
}
|
|
||||||
|
|
||||||
benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
|
benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
|
||||||
return this.cryptoApi.benchmark(repetitions);
|
return this.ws.cryptoApi.benchmark(repetitions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
ExchangeWireInfo,
|
ExchangeWireInfo,
|
||||||
WithdrawalSource,
|
WithdrawalSource,
|
||||||
|
RetryInfo,
|
||||||
} from "./dbTypes";
|
} from "./dbTypes";
|
||||||
import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes";
|
import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes";
|
||||||
|
|
||||||
@ -203,16 +204,6 @@ export interface PayCoinInfo {
|
|||||||
sigs: CoinPaySig[];
|
sigs: CoinPaySig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for notifications from the wallet.
|
|
||||||
*/
|
|
||||||
export interface Notifier {
|
|
||||||
/**
|
|
||||||
* Called when a new notification arrives.
|
|
||||||
*/
|
|
||||||
notify(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For terseness.
|
* For terseness.
|
||||||
*/
|
*/
|
||||||
@ -421,31 +412,6 @@ export interface TipStatus {
|
|||||||
totalFees: AmountJson;
|
totalFees: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Badge that shows activity for the wallet.
|
|
||||||
*/
|
|
||||||
export interface Badge {
|
|
||||||
/**
|
|
||||||
* Start indicating background activity.
|
|
||||||
*/
|
|
||||||
startBusy(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop indicating background activity.
|
|
||||||
*/
|
|
||||||
stopBusy(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the notification in the badge.
|
|
||||||
*/
|
|
||||||
showNotification(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop showing the notification.
|
|
||||||
*/
|
|
||||||
clearNotification(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BenchmarkResult {
|
export interface BenchmarkResult {
|
||||||
time: { [s: string]: number };
|
time: { [s: string]: number };
|
||||||
repetitions: number;
|
repetitions: number;
|
||||||
@ -525,7 +491,7 @@ export interface WalletDiagnostics {
|
|||||||
|
|
||||||
export interface PendingWithdrawOperation {
|
export interface PendingWithdrawOperation {
|
||||||
type: "withdraw";
|
type: "withdraw";
|
||||||
source: WithdrawalSource,
|
source: WithdrawalSource;
|
||||||
withdrawSessionId: string;
|
withdrawSessionId: string;
|
||||||
numCoinsWithdrawn: number;
|
numCoinsWithdrawn: number;
|
||||||
numCoinsTotal: number;
|
numCoinsTotal: number;
|
||||||
@ -539,6 +505,102 @@ export interface PendingPayOperation {
|
|||||||
type: "pay";
|
type: "pay";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum NotificationType {
|
||||||
|
ProposalAccepted = "proposal-accepted",
|
||||||
|
ProposalDownloaded = "proposal-downloaded",
|
||||||
|
RefundsSubmitted = "refunds-submitted",
|
||||||
|
PaybackStarted = "payback-started",
|
||||||
|
PaybackFinished = "payback-finished",
|
||||||
|
RefreshRevealed = "refresh-revealed",
|
||||||
|
RefreshMelted = "refresh-melted",
|
||||||
|
RefreshStarted = "refresh-started",
|
||||||
|
RefreshRefused = "refresh-refused",
|
||||||
|
ReserveUpdated = "reserve-updated",
|
||||||
|
ReserveConfirmed = "reserve-confirmed",
|
||||||
|
ReserveDepleted = "reserve-depleted",
|
||||||
|
WithdrawSessionFinished = "withdraw-session-finished",
|
||||||
|
WaitingForRetry = "waiting-for-retry",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProposalAcceptedNotification {
|
||||||
|
type: NotificationType.ProposalAccepted;
|
||||||
|
proposalId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProposalDownloadedNotification {
|
||||||
|
type: NotificationType.ProposalDownloaded;
|
||||||
|
proposalId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefundsSubmittedNotification {
|
||||||
|
type: NotificationType.RefundsSubmitted;
|
||||||
|
proposalId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaybackStartedNotification {
|
||||||
|
type: NotificationType.PaybackStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaybackFinishedNotification {
|
||||||
|
type: NotificationType.PaybackFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshMeltedNotification {
|
||||||
|
type: NotificationType.RefreshMelted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshRevealedNotification {
|
||||||
|
type: NotificationType.RefreshRevealed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshStartedNotification {
|
||||||
|
type: NotificationType.RefreshStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshRefusedNotification {
|
||||||
|
type: NotificationType.RefreshRefused;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReserveUpdatedNotification {
|
||||||
|
type: NotificationType.ReserveUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReserveConfirmedNotification {
|
||||||
|
type: NotificationType.ReserveConfirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawSessionFinishedNotification {
|
||||||
|
type: NotificationType.WithdrawSessionFinished;
|
||||||
|
withdrawSessionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReserveDepletedNotification {
|
||||||
|
type: NotificationType.ReserveDepleted;
|
||||||
|
reservePub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WaitingForRetryNotification {
|
||||||
|
type: NotificationType.WaitingForRetry;
|
||||||
|
numPending: number;
|
||||||
|
numGivingLiveness: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WalletNotification =
|
||||||
|
| ProposalAcceptedNotification
|
||||||
|
| ProposalDownloadedNotification
|
||||||
|
| RefundsSubmittedNotification
|
||||||
|
| PaybackStartedNotification
|
||||||
|
| PaybackFinishedNotification
|
||||||
|
| RefreshMeltedNotification
|
||||||
|
| RefreshRevealedNotification
|
||||||
|
| RefreshStartedNotification
|
||||||
|
| RefreshRefusedNotification
|
||||||
|
| ReserveUpdatedNotification
|
||||||
|
| ReserveConfirmedNotification
|
||||||
|
| WithdrawSessionFinishedNotification
|
||||||
|
| ReserveDepletedNotification
|
||||||
|
| WaitingForRetryNotification;
|
||||||
|
|
||||||
export interface OperationError {
|
export interface OperationError {
|
||||||
type: string;
|
type: string;
|
||||||
message: string;
|
message: string;
|
||||||
@ -561,7 +623,7 @@ export interface PendingBugOperation {
|
|||||||
|
|
||||||
export interface PendingReserveOperation {
|
export interface PendingReserveOperation {
|
||||||
type: "reserve";
|
type: "reserve";
|
||||||
lastError?: OperationError;
|
retryInfo: RetryInfo | undefined;
|
||||||
stage: string;
|
stage: string;
|
||||||
timestampCreated: Timestamp;
|
timestampCreated: Timestamp;
|
||||||
reserveType: string;
|
reserveType: string;
|
||||||
@ -578,7 +640,6 @@ export interface PendingRefreshOperation {
|
|||||||
refreshOutputSize: number;
|
refreshOutputSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface PendingDirtyCoinOperation {
|
export interface PendingDirtyCoinOperation {
|
||||||
type: "dirty-coin";
|
type: "dirty-coin";
|
||||||
coinPub: string;
|
coinPub: string;
|
||||||
@ -615,17 +676,24 @@ export interface PendingPayOperation {
|
|||||||
isReplay: boolean;
|
isReplay: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PendingOperationInfo =
|
export interface PendingOperationInfoCommon {
|
||||||
| PendingWithdrawOperation
|
type: string;
|
||||||
| PendingReserveOperation
|
givesLifeness: boolean;
|
||||||
| PendingBugOperation
|
}
|
||||||
| PendingDirtyCoinOperation
|
|
||||||
| PendingExchangeUpdateOperation
|
export type PendingOperationInfo = PendingOperationInfoCommon &
|
||||||
| PendingRefreshOperation
|
(
|
||||||
| PendingTipOperation
|
| PendingWithdrawOperation
|
||||||
| PendingProposalDownloadOperation
|
| PendingReserveOperation
|
||||||
| PendingProposalChoiceOperation
|
| PendingBugOperation
|
||||||
| PendingPayOperation;
|
| PendingDirtyCoinOperation
|
||||||
|
| PendingExchangeUpdateOperation
|
||||||
|
| PendingRefreshOperation
|
||||||
|
| PendingTipOperation
|
||||||
|
| PendingProposalDownloadOperation
|
||||||
|
| PendingProposalChoiceOperation
|
||||||
|
| PendingPayOperation
|
||||||
|
);
|
||||||
|
|
||||||
export interface PendingOperationsResponse {
|
export interface PendingOperationsResponse {
|
||||||
pendingOperations: PendingOperationInfo[];
|
pendingOperations: PendingOperationInfo[];
|
||||||
@ -683,4 +751,4 @@ export interface PlanchetCreationRequest {
|
|||||||
denomPub: string;
|
denomPub: string;
|
||||||
reservePub: string;
|
reservePub: string;
|
||||||
reservePriv: string;
|
reservePriv: string;
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,6 @@
|
|||||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
} from "../walletTypes";
|
|
||||||
|
|
||||||
import { isFirefox } from "./compat";
|
import { isFirefox } from "./compat";
|
||||||
|
|
||||||
@ -36,7 +33,7 @@ function rAF(cb: (ts: number) => void) {
|
|||||||
* Badge for Chrome that renders a Taler logo with a rotating ring if some
|
* Badge for Chrome that renders a Taler logo with a rotating ring if some
|
||||||
* background activity is happening.
|
* background activity is happening.
|
||||||
*/
|
*/
|
||||||
export class ChromeBadge implements Badge {
|
export class ChromeBadge {
|
||||||
private canvas: HTMLCanvasElement;
|
private canvas: HTMLCanvasElement;
|
||||||
private ctx: CanvasRenderingContext2D;
|
private ctx: CanvasRenderingContext2D;
|
||||||
/**
|
/**
|
||||||
|
@ -145,10 +145,6 @@ export interface MessageMap {
|
|||||||
request: { talerTipUri: string };
|
request: { talerTipUri: string };
|
||||||
response: walletTypes.TipStatus;
|
response: walletTypes.TipStatus;
|
||||||
};
|
};
|
||||||
"clear-notification": {
|
|
||||||
request: {};
|
|
||||||
response: void;
|
|
||||||
};
|
|
||||||
"accept-refund": {
|
"accept-refund": {
|
||||||
request: { refundUrl: string };
|
request: { refundUrl: string };
|
||||||
response: string;
|
response: string;
|
||||||
|
@ -280,13 +280,6 @@ export function acceptTip(talerTipUri: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear notifications that the wallet shows to the user.
|
|
||||||
*/
|
|
||||||
export function clearNotification(): Promise<void> {
|
|
||||||
return callBackend("clear-notification", { });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a refund and accept it.
|
* Download a refund and accept it.
|
||||||
*/
|
*/
|
||||||
|
@ -28,7 +28,6 @@ import { AmountJson } from "../util/amounts";
|
|||||||
import {
|
import {
|
||||||
ConfirmReserveRequest,
|
ConfirmReserveRequest,
|
||||||
CreateReserveRequest,
|
CreateReserveRequest,
|
||||||
Notifier,
|
|
||||||
ReturnCoinsRequest,
|
ReturnCoinsRequest,
|
||||||
WalletDiagnostics,
|
WalletDiagnostics,
|
||||||
} from "../walletTypes";
|
} from "../walletTypes";
|
||||||
@ -41,7 +40,7 @@ import { MessageType } from "./messages";
|
|||||||
import * as wxApi from "./wxApi";
|
import * as wxApi from "./wxApi";
|
||||||
import Port = chrome.runtime.Port;
|
import Port = chrome.runtime.Port;
|
||||||
import MessageSender = chrome.runtime.MessageSender;
|
import MessageSender = chrome.runtime.MessageSender;
|
||||||
import { BrowserCryptoWorkerFactory } from "../crypto/cryptoApi";
|
import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi";
|
||||||
import { OpenedPromise, openPromise } from "../util/promiseUtils";
|
import { OpenedPromise, openPromise } from "../util/promiseUtils";
|
||||||
|
|
||||||
const NeedsWallet = Symbol("NeedsWallet");
|
const NeedsWallet = Symbol("NeedsWallet");
|
||||||
@ -225,9 +224,6 @@ async function handleMessage(
|
|||||||
case "accept-tip": {
|
case "accept-tip": {
|
||||||
return needsWallet().acceptTip(detail.talerTipUri);
|
return needsWallet().acceptTip(detail.talerTipUri);
|
||||||
}
|
}
|
||||||
case "clear-notification": {
|
|
||||||
return needsWallet().clearNotification();
|
|
||||||
}
|
|
||||||
case "abort-failed-payment": {
|
case "abort-failed-payment": {
|
||||||
if (!detail.contractTermsHash) {
|
if (!detail.contractTermsHash) {
|
||||||
throw Error("contracTermsHash not given");
|
throw Error("contracTermsHash not given");
|
||||||
@ -331,31 +327,6 @@ async function dispatch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChromeNotifier implements Notifier {
|
|
||||||
private ports: Port[] = [];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
chrome.runtime.onConnect.addListener(port => {
|
|
||||||
console.log("got connect!");
|
|
||||||
this.ports.push(port);
|
|
||||||
port.onDisconnect.addListener(() => {
|
|
||||||
const i = this.ports.indexOf(port);
|
|
||||||
if (i >= 0) {
|
|
||||||
this.ports.splice(i, 1);
|
|
||||||
} else {
|
|
||||||
console.error("port already removed");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
notify() {
|
|
||||||
for (const p of this.ports) {
|
|
||||||
p.postMessage({ notify: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTab(tabId: number): Promise<chrome.tabs.Tab> {
|
function getTab(tabId: number): Promise<chrome.tabs.Tab> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
chrome.tabs.get(tabId, (tab: chrome.tabs.Tab) => resolve(tab));
|
chrome.tabs.get(tabId, (tab: chrome.tabs.Tab) => resolve(tab));
|
||||||
@ -458,16 +429,13 @@ async function reinitWallet() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const http = new BrowserHttpLib();
|
const http = new BrowserHttpLib();
|
||||||
const notifier = new ChromeNotifier();
|
|
||||||
console.log("setting wallet");
|
console.log("setting wallet");
|
||||||
const wallet = new Wallet(
|
const wallet = new Wallet(
|
||||||
currentDatabase,
|
currentDatabase,
|
||||||
http,
|
http,
|
||||||
badge,
|
|
||||||
notifier,
|
|
||||||
new BrowserCryptoWorkerFactory(),
|
new BrowserCryptoWorkerFactory(),
|
||||||
);
|
);
|
||||||
wallet.runLoopScheduledRetries().catch((e) => {
|
wallet.runRetryLoop().catch((e) => {
|
||||||
console.log("error during wallet retry loop", e);
|
console.log("error during wallet retry loop", e);
|
||||||
});
|
});
|
||||||
// Useful for debugging in the background page.
|
// Useful for debugging in the background page.
|
||||||
@ -621,21 +589,6 @@ export async function wxMain() {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear notifications both when the popop opens,
|
|
||||||
// as well when it closes.
|
|
||||||
chrome.runtime.onConnect.addListener(port => {
|
|
||||||
if (port.name === "popup") {
|
|
||||||
if (currentWallet) {
|
|
||||||
currentWallet.clearNotification();
|
|
||||||
}
|
|
||||||
port.onDisconnect.addListener(() => {
|
|
||||||
if (currentWallet) {
|
|
||||||
currentWallet.clearNotification();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handlers for catching HTTP requests
|
// Handlers for catching HTTP requests
|
||||||
chrome.webRequest.onHeadersReceived.addListener(
|
chrome.webRequest.onHeadersReceived.addListener(
|
||||||
details => {
|
details => {
|
||||||
|
@ -24,18 +24,17 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src/android/index.ts",
|
"src/android/index.ts",
|
||||||
"src/crypto/browserWorkerEntry.ts",
|
|
||||||
"src/crypto/cryptoApi.ts",
|
|
||||||
"src/crypto/cryptoImplementation.ts",
|
|
||||||
"src/crypto/cryptoWorker.ts",
|
|
||||||
"src/crypto/nodeProcessWorker.ts",
|
|
||||||
"src/crypto/nodeWorkerEntry.ts",
|
|
||||||
"src/crypto/primitives/kdf.ts",
|
"src/crypto/primitives/kdf.ts",
|
||||||
"src/crypto/primitives/nacl-fast.ts",
|
"src/crypto/primitives/nacl-fast.ts",
|
||||||
"src/crypto/primitives/sha256.ts",
|
"src/crypto/primitives/sha256.ts",
|
||||||
"src/crypto/synchronousWorker.ts",
|
|
||||||
"src/crypto/talerCrypto-test.ts",
|
"src/crypto/talerCrypto-test.ts",
|
||||||
"src/crypto/talerCrypto.ts",
|
"src/crypto/talerCrypto.ts",
|
||||||
|
"src/crypto/workers/browserWorkerEntry.ts",
|
||||||
|
"src/crypto/workers/cryptoApi.ts",
|
||||||
|
"src/crypto/workers/cryptoImplementation.ts",
|
||||||
|
"src/crypto/workers/cryptoWorker.ts",
|
||||||
|
"src/crypto/workers/nodeThreadWorker.ts",
|
||||||
|
"src/crypto/workers/synchronousWorker.ts",
|
||||||
"src/db.ts",
|
"src/db.ts",
|
||||||
"src/dbTypes.ts",
|
"src/dbTypes.ts",
|
||||||
"src/headless/bank.ts",
|
"src/headless/bank.ts",
|
||||||
@ -68,6 +67,7 @@
|
|||||||
"src/util/timer.ts",
|
"src/util/timer.ts",
|
||||||
"src/util/wire.ts",
|
"src/util/wire.ts",
|
||||||
"src/wallet-impl/balance.ts",
|
"src/wallet-impl/balance.ts",
|
||||||
|
"src/wallet-impl/errors.ts",
|
||||||
"src/wallet-impl/exchanges.ts",
|
"src/wallet-impl/exchanges.ts",
|
||||||
"src/wallet-impl/history.ts",
|
"src/wallet-impl/history.ts",
|
||||||
"src/wallet-impl/pay.ts",
|
"src/wallet-impl/pay.ts",
|
||||||
|
Loading…
Reference in New Issue
Block a user