aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-11-21 23:09:43 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-11-21 23:09:43 +0100
commitc6233094306cd264f8faa2041388dff01ff8cf01 (patch)
treef3ee839a4254c528058887c6517a76bec8919b15 /src
parente8f362ccfea683fe16ce68b956f068ffa0b001b1 (diff)
WIP: simplification and error handling
Diffstat (limited to 'src')
-rw-r--r--src/crypto/cryptoApi-test.ts23
-rw-r--r--src/crypto/cryptoImplementation.ts16
-rw-r--r--src/db.ts1
-rw-r--r--src/dbTypes.ts171
-rw-r--r--src/headless/clk.ts2
-rw-r--r--src/headless/helpers.ts16
-rw-r--r--src/headless/integrationtest.ts1
-rw-r--r--src/headless/taler-wallet-cli.ts380
-rw-r--r--src/http.ts7
-rw-r--r--src/query.ts78
-rw-r--r--src/wallet.ts1243
-rw-r--r--src/walletTypes.ts34
-rw-r--r--src/webex/messages.ts2
-rw-r--r--src/webex/pages/payback.tsx6
-rw-r--r--src/webex/pages/popup.tsx8
-rw-r--r--src/webex/renderHtml.tsx2
-rw-r--r--src/webex/wxBackend.ts2
17 files changed, 1094 insertions, 898 deletions
diff --git a/src/crypto/cryptoApi-test.ts b/src/crypto/cryptoApi-test.ts
index 39f46c5c3..d9d42081c 100644
--- a/src/crypto/cryptoApi-test.ts
+++ b/src/crypto/cryptoApi-test.ts
@@ -22,6 +22,7 @@ import {
DenominationRecord,
DenominationStatus,
ReserveRecord,
+ ReserveRecordStatus,
} from "../dbTypes";
import { CryptoApi } from "./cryptoApi";
@@ -86,18 +87,18 @@ test("precoin creation", async t => {
const crypto = new CryptoApi(new NodeCryptoWorkerFactory());
const { priv, pub } = await crypto.createEddsaKeypair();
const r: ReserveRecord = {
- created: 0,
- current_amount: null,
- exchange_base_url: "https://example.com/exchange",
+ created: { t_ms: 0 },
+ currentAmount: null,
+ exchangeBaseUrl: "https://example.com/exchange",
hasPayback: false,
- precoin_amount: { currency: "PUDOS", value: 0, fraction: 0 },
- requested_amount: { currency: "PUDOS", value: 0, fraction: 0 },
- reserve_priv: priv,
- reserve_pub: pub,
- timestamp_confirmed: 0,
- timestamp_depleted: 0,
- timestamp_reserve_info_posted: 0,
- exchangeWire: "payto://foo"
+ precoinAmount: { currency: "PUDOS", value: 0, fraction: 0 },
+ requestedAmount: { currency: "PUDOS", value: 0, fraction: 0 },
+ reservePriv: priv,
+ reservePub: pub,
+ timestampConfirmed: undefined,
+ timestampReserveInfoPosted: undefined,
+ exchangeWire: "payto://foo",
+ reserveStatus: ReserveRecordStatus.UNCONFIRMED,
};
const precoin = await crypto.createPreCoin(denomValid1, r);
diff --git a/src/crypto/cryptoImplementation.ts b/src/crypto/cryptoImplementation.ts
index d50d40027..7dd019c18 100644
--- a/src/crypto/cryptoImplementation.ts
+++ b/src/crypto/cryptoImplementation.ts
@@ -45,6 +45,7 @@ import * as native from "./emscInterface";
import { AmountJson } from "../amounts";
import * as Amounts from "../amounts";
import * as timer from "../timer";
+import { getRandomBytes, encodeCrock } from "./nativeCrypto";
export class CryptoImplementation {
static enableTracing: boolean = false;
@@ -60,9 +61,9 @@ export class CryptoImplementation {
reserve: ReserveRecord,
): PreCoinRecord {
const reservePriv = new native.EddsaPrivateKey(this.emsc);
- reservePriv.loadCrock(reserve.reserve_priv);
+ reservePriv.loadCrock(reserve.reservePriv);
const reservePub = new native.EddsaPublicKey(this.emsc);
- reservePub.loadCrock(reserve.reserve_pub);
+ reservePub.loadCrock(reserve.reservePub);
const denomPub = native.RsaPublicKey.fromCrock(this.emsc, denom.denomPub);
const coinPriv = native.EddsaPrivateKey.create(this.emsc);
const coinPub = coinPriv.getPublicKey();
@@ -103,7 +104,7 @@ export class CryptoImplementation {
coinValue: denom.value,
denomPub: denomPub.toCrock(),
denomPubHash: denomPubHash.toCrock(),
- exchangeBaseUrl: reserve.exchange_base_url,
+ exchangeBaseUrl: reserve.exchangeBaseUrl,
isFromTip: false,
reservePub: reservePub.toCrock(),
withdrawSig: sig.toCrock(),
@@ -199,14 +200,14 @@ export class CryptoImplementation {
isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean {
const p = new native.MasterWireFeePS(this.emsc, {
closing_fee: new native.Amount(this.emsc, wf.closingFee).toNbo(),
- end_date: native.AbsoluteTimeNbo.fromStampSeconds(this.emsc, wf.endStamp),
+ end_date: native.AbsoluteTimeNbo.fromStampSeconds(this.emsc, (wf.endStamp.t_ms / 1000)),
h_wire_method: native.ByteArray.fromStringWithNull(
this.emsc,
type,
).hash(),
start_date: native.AbsoluteTimeNbo.fromStampSeconds(
this.emsc,
- wf.startStamp,
+ Math.floor(wf.startStamp.t_ms / 1000),
),
wire_fee: new native.Amount(this.emsc, wf.wireFee).toNbo(),
});
@@ -354,7 +355,7 @@ export class CryptoImplementation {
const newAmount = new native.Amount(this.emsc, cd.coin.currentAmount);
newAmount.sub(coinSpend);
cd.coin.currentAmount = newAmount.toJson();
- cd.coin.status = CoinStatus.PurchasePending;
+ cd.coin.status = CoinStatus.Dirty;
const d = new native.DepositRequestPS(this.emsc, {
amount_with_fee: coinSpend.toNbo(),
@@ -505,7 +506,10 @@ export class CryptoImplementation {
valueOutput = Amounts.add(valueOutput, denom.value).amount;
}
+ const refreshSessionId = encodeCrock(getRandomBytes(32));
+
const refreshSession: RefreshSessionRecord = {
+ refreshSessionId,
confirmSig,
exchangeBaseUrl,
finished: false,
diff --git a/src/db.ts b/src/db.ts
index 00eac4320..e317b0aaf 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -12,7 +12,6 @@ export function openTalerDb(
onVersionChange: () => void,
onUpgradeUnsupported: (oldVersion: number, newVersion: number) => void,
): Promise<IDBDatabase> {
- console.log("in openTalerDb");
return new Promise<IDBDatabase>((resolve, reject) => {
const req = idbFactory.open(DB_NAME, WALLET_DB_VERSION);
req.onerror = e => {
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 0d54069ec..22d98ffac 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -46,6 +46,36 @@ import { Timestamp, OperationError } from "./walletTypes";
*/
export const WALLET_DB_VERSION = 27;
+export enum ReserveRecordStatus {
+ /**
+ * Waiting for manual confirmation.
+ */
+ UNCONFIRMED = "unconfirmed",
+
+ /**
+ * Reserve must be registered with the bank.
+ */
+ REGISTERING_BANK = "registering-bank",
+
+ /**
+ * Querying reserve status with the exchange.
+ */
+ QUERYING_STATUS = "querying-status",
+
+ /**
+ * Status is queried, the wallet must now select coins
+ * and start withdrawing.
+ */
+ WITHDRAWING = "withdrawing",
+
+ /**
+ * The corresponding withdraw record has been created.
+ * No further processing is done, unless explicitly requested
+ * by the user.
+ */
+ DORMANT = "dormant",
+}
+
/**
* A reserve record as stored in the wallet's database.
*/
@@ -53,28 +83,22 @@ export interface ReserveRecord {
/**
* The reserve public key.
*/
- reserve_pub: string;
+ reservePub: string;
/**
* The reserve private key.
*/
- reserve_priv: string;
+ reservePriv: string;
/**
* The exchange base URL.
*/
- exchange_base_url: string;
+ exchangeBaseUrl: string;
/**
* Time when the reserve was created.
*/
- created: number;
-
- /**
- * Time when the reserve was depleted.
- * Set to 0 if not depleted yet.
- */
- timestamp_depleted: number;
+ created: Timestamp;
/**
* Time when the information about this reserve was posted to the bank.
@@ -83,32 +107,32 @@ export interface ReserveRecord {
*
* Set to 0 if that hasn't happened yet.
*/
- timestamp_reserve_info_posted: number;
+ timestampReserveInfoPosted: Timestamp | undefined;
/**
* Time when the reserve was confirmed.
*
* Set to 0 if not confirmed yet.
*/
- timestamp_confirmed: number;
+ timestampConfirmed: Timestamp | undefined;
/**
* Current amount left in the reserve
*/
- current_amount: AmountJson | null;
+ currentAmount: AmountJson | null;
/**
* Amount requested when the reserve was created.
* When a reserve is re-used (rare!) the current_amount can
* be higher than the requested_amount
*/
- requested_amount: AmountJson;
+ requestedAmount: AmountJson;
/**
* What's the current amount that sits
* in precoins?
*/
- precoin_amount: AmountJson;
+ precoinAmount: AmountJson;
/**
* We got some payback to this reserve. We'll cease to automatically
@@ -129,6 +153,10 @@ export interface ReserveRecord {
exchangeWire: string;
bankWithdrawStatusUrl?: string;
+
+ reserveStatus: ReserveRecordStatus;
+
+ lastError?: OperationError;
}
/**
@@ -341,9 +369,9 @@ export interface ExchangeDetails {
}
export enum ExchangeUpdateStatus {
- NONE = "none",
FETCH_KEYS = "fetch_keys",
FETCH_WIRE = "fetch_wire",
+ FINISHED = "finished",
}
export interface ExchangeBankAccount {
@@ -375,12 +403,17 @@ export interface ExchangeRecord {
wireInfo: ExchangeWireInfo | undefined;
/**
+ * When was the exchange added to the wallet?
+ */
+ timestampAdded: Timestamp;
+
+ /**
* Time when the update to the exchange has been started or
* undefined if no update is in progress.
*/
updateStarted: Timestamp | undefined;
-
updateStatus: ExchangeUpdateStatus;
+ updateReason?: "initial" | "forced";
lastError?: OperationError;
}
@@ -436,31 +469,15 @@ export enum CoinStatus {
/**
* Withdrawn and never shown to anybody.
*/
- Fresh,
- /**
- * Currently planned to be sent to a merchant for a purchase.
- */
- PurchasePending,
+ Fresh = "fresh",
/**
* Used for a completed transaction and now dirty.
*/
- Dirty,
+ Dirty = "dirty",
/**
- * A coin that was refreshed.
+ * A coin that has been spent and refreshed.
*/
- Refreshed,
- /**
- * Coin marked to be paid back, but payback not finished.
- */
- PaybackPending,
- /**
- * Coin fully paid back.
- */
- PaybackDone,
- /**
- * Coin was dirty but can't be refreshed.
- */
- Useless,
+ Dormant = "dormant",
}
/**
@@ -569,7 +586,7 @@ export class ProposalDownloadRecord {
* was created.
*/
@Checkable.Number()
- timestamp: number;
+ timestamp: Timestamp;
/**
* Private key for the nonce.
@@ -658,7 +675,7 @@ export interface TipRecord {
*/
nextUrl?: string;
- timestamp: number;
+ timestamp: Timestamp;
pickupUrl: string;
}
@@ -735,9 +752,9 @@ export interface RefreshSessionRecord {
finished: boolean;
/**
- * Record ID when retrieved from the DB.
+ * A 32-byte base32-crockford encoded random identifier.
*/
- id?: number;
+ refreshSessionId: string;
}
/**
@@ -771,12 +788,12 @@ export interface WireFee {
/**
* Start date of the fee.
*/
- startStamp: number;
+ startStamp: Timestamp;
/**
* End date of the fee.
*/
- endStamp: number;
+ endStamp: Timestamp;
/**
* Signature made by the exchange master key.
@@ -830,14 +847,13 @@ export interface PurchaseRecord {
* When was the purchase made?
* Refers to the time that the user accepted.
*/
- timestamp: number;
+ timestamp: Timestamp;
/**
* When was the last refund made?
* Set to 0 if no refund was made on the purchase.
*/
- timestamp_refund: number;
-
+ timestamp_refund: Timestamp | undefined;
/**
* Last session signature that we submitted to /pay (if any).
@@ -917,7 +933,6 @@ export interface CoinsReturnRecord {
wire: any;
}
-
export interface WithdrawalRecord {
/**
* Reserve that we're withdrawing from.
@@ -928,18 +943,22 @@ export interface WithdrawalRecord {
* When was the withdrawal operation started started?
* Timestamp in milliseconds.
*/
- startTimestamp: number;
+ startTimestamp: Timestamp;
/**
* When was the withdrawal operation completed?
*/
- finishTimestamp?: number;
+ finishTimestamp?: Timestamp;
/**
* Amount that is being withdrawn with this operation.
* This does not include fees.
*/
withdrawalAmount: string;
+
+ numCoinsTotal: number;
+
+ numCoinsWithdrawn: number;
}
/* tslint:disable:completed-docs */
@@ -983,11 +1002,6 @@ export namespace Stores {
"urlIndex",
"url",
);
- timestampIndex = new Index<string, ProposalDownloadRecord>(
- this,
- "timestampIndex",
- "timestamp",
- );
}
class PurchasesStore extends Store<PurchaseRecord> {
@@ -1005,11 +1019,6 @@ export namespace Stores {
"orderIdIndex",
"contractTerms.order_id",
);
- timestampIndex = new Index<string, PurchaseRecord>(
- this,
- "timestampIndex",
- "timestamp",
- );
}
class DenominationsStore extends Store<DenominationRecord> {
@@ -1051,23 +1060,8 @@ export namespace Stores {
class ReservesStore extends Store<ReserveRecord> {
constructor() {
- super("reserves", { keyPath: "reserve_pub" });
+ super("reserves", { keyPath: "reservePub" });
}
- timestampCreatedIndex = new Index<string, ReserveRecord>(
- this,
- "timestampCreatedIndex",
- "created",
- );
- timestampConfirmedIndex = new Index<string, ReserveRecord>(
- this,
- "timestampConfirmedIndex",
- "timestamp_confirmed",
- );
- timestampDepletedIndex = new Index<string, ReserveRecord>(
- this,
- "timestampDepletedIndex",
- "timestamp_depleted",
- );
}
class TipsStore extends Store<TipRecord> {
@@ -1092,8 +1086,26 @@ export namespace Stores {
class WithdrawalsStore extends Store<WithdrawalRecord> {
constructor() {
- super("withdrawals", { keyPath: "id", autoIncrement: true })
+ super("withdrawals", { keyPath: "id", autoIncrement: true });
}
+ byReservePub = new Index<string, WithdrawalRecord>(
+ this,
+ "withdrawalsReservePubIndex",
+ "reservePub",
+ );
+ }
+
+ class PreCoinsStore extends Store<PreCoinRecord> {
+ constructor() {
+ super("precoins", {
+ keyPath: "coinPub",
+ });
+ }
+ byReservePub = new Index<string, PreCoinRecord>(
+ this,
+ "precoinsReservePubIndex",
+ "reservePub",
+ );
}
export const coins = new CoinsStore();
@@ -1104,13 +1116,10 @@ export namespace Stores {
export const currencies = new CurrenciesStore();
export const denominations = new DenominationsStore();
export const exchanges = new ExchangeStore();
- export const precoins = new Store<PreCoinRecord>("precoins", {
- keyPath: "coinPub",
- });
+ export const precoins = new PreCoinsStore();
export const proposals = new ProposalsStore();
export const refresh = new Store<RefreshSessionRecord>("refresh", {
- keyPath: "id",
- autoIncrement: true,
+ keyPath: "refreshSessionId",
});
export const reserves = new ReservesStore();
export const purchases = new PurchasesStore();
diff --git a/src/headless/clk.ts b/src/headless/clk.ts
index f66d609e8..51ee119c9 100644
--- a/src/headless/clk.ts
+++ b/src/headless/clk.ts
@@ -440,7 +440,7 @@ export class CommandGroup<GN extends keyof any, TG> {
if (option.isFlag == false && option.required == true) {
if (!foundOptions[option.name]) {
if (option.args.default !== undefined) {
- parsedArgs[this.argKey] = option.args.default;
+ myArgs[option.name] = option.args.default;
} else {
const name = option.flagspec.join(",")
console.error(`error: missing option '${name}'`);
diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts
index 49881d469..5e06a2f25 100644
--- a/src/headless/helpers.ts
+++ b/src/headless/helpers.ts
@@ -21,7 +21,7 @@
/**
* Imports.
*/
-import { Wallet } from "../wallet";
+import { Wallet, OperationFailedAndReportedError } from "../wallet";
import { Notifier, Badge } from "../walletTypes";
import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge";
import { SynchronousCryptoWorkerFactory } from "../crypto/synchronousWorker";
@@ -139,18 +139,16 @@ export async function getDefaultNodeWallet(
const storagePath = args.persistentStoragePath;
if (storagePath) {
- console.log(`using storage path ${storagePath}`);
-
try {
const dbContentStr: string = fs.readFileSync(storagePath, { encoding: "utf-8" });
const dbContent = JSON.parse(dbContentStr);
myBackend.importDump(dbContent);
- console.log("imported wallet");
} catch (e) {
- console.log("could not read wallet file");
+ console.error("could not read wallet file");
}
myBackend.afterCommitCallback = async () => {
+ console.log("DATABASE COMMITTED");
// Allow caller to stop persisting the wallet.
if (args.persistentStoragePath === undefined) {
return;
@@ -190,8 +188,6 @@ export async function getDefaultNodeWallet(
myUnsupportedUpgrade,
);
- console.log("opened db");
-
return new Wallet(
myDb,
myHttpLib,
@@ -214,6 +210,8 @@ export async function withdrawTestBalance(
exchangeWire: "payto://unknown",
});
+ const reservePub = reserveResponse.reservePub;
+
const bank = new Bank(bankBaseUrl);
const bankUser = await bank.registerRandomUser();
@@ -228,11 +226,11 @@ export async function withdrawTestBalance(
await bank.createReserve(
bankUser,
amount,
- reserveResponse.reservePub,
+ reservePub,
exchangePaytoUri,
);
await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub });
- await myWallet.processReserve(reserveResponse.reservePub);
+ await myWallet.runUntilReserveDepleted(reservePub);
}
diff --git a/src/headless/integrationtest.ts b/src/headless/integrationtest.ts
index 6b3286904..6f2139c9d 100644
--- a/src/headless/integrationtest.ts
+++ b/src/headless/integrationtest.ts
@@ -31,6 +31,7 @@ export async function runIntegrationTest(args: {
amountToWithdraw: string;
amountToSpend: string;
}) {
+ console.log("running test with", args);
const myWallet = await getDefaultNodeWallet();
await withdrawTestBalance(myWallet, args.amountToWithdraw, args.bankBaseUrl, args.exchangeBaseUrl);
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 06235d0b4..0a6780808 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -18,9 +18,14 @@ import os = require("os");
import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers";
import { MerchantBackendConnection } from "./merchant";
import { runIntegrationTest } from "./integrationtest";
-import { Wallet } from "../wallet";
+import { Wallet, OperationFailedAndReportedError } from "../wallet";
import qrcodeGenerator = require("qrcode-generator");
import * as clk from "./clk";
+import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
+import { Logger } from "../logging";
+import * as Amounts from "../amounts";
+
+const logger = new Logger("taler-wallet-cli.ts");
const walletDbPath = os.homedir + "/" + ".talerwalletdb.json";
@@ -82,6 +87,7 @@ function applyVerbose(verbose: boolean) {
if (verbose) {
console.log("enabled verbose logging");
Wallet.enableTracing = true;
+ BridgeIDBFactory.enableTracing = true;
}
}
@@ -103,16 +109,21 @@ async function withWallet<T>(
walletCliArgs: WalletCliArgsType,
f: (w: Wallet) => Promise<T>,
): Promise<T> {
- applyVerbose(walletCliArgs.wallet.verbose);
const wallet = await getDefaultNodeWallet({
persistentStoragePath: walletDbPath,
});
+ applyVerbose(walletCliArgs.wallet.verbose);
try {
await wallet.fillDefaults();
const ret = await f(wallet);
return ret;
} catch (e) {
- console.error("caught exception:", e);
+ if (e instanceof OperationFailedAndReportedError) {
+ console.error("Operation failed: " + e.message);
+ console.log("Hint: check pending operations for details.");
+ } else {
+ console.error("caught exception:", e);
+ }
process.exit(1);
} finally {
wallet.stop();
@@ -120,45 +131,10 @@ async function withWallet<T>(
}
walletCli
- .subcommand("testPayCmd", "test-pay", { help: "create contract and pay" })
- .requiredOption("amount", ["-a", "--amount"], clk.STRING)
- .requiredOption("summary", ["-s", "--summary"], clk.STRING, {
- default: "Test Payment",
- })
- .action(async args => {
- const cmdArgs = args.testPayCmd;
- console.log("creating order");
- const merchantBackend = new MerchantBackendConnection(
- "https://backend.test.taler.net/",
- "sandbox",
- );
- const orderResp = await merchantBackend.createOrder(
- cmdArgs.amount,
- cmdArgs.summary,
- "",
- );
- console.log("created new order with order ID", orderResp.orderId);
- const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
- const talerPayUri = checkPayResp.taler_pay_uri;
- if (!talerPayUri) {
- console.error("fatal: no taler pay URI received from backend");
- process.exit(1);
- return;
- }
- console.log("taler pay URI:", talerPayUri);
-
- const wallet = await getDefaultNodeWallet({
- persistentStoragePath: walletDbPath,
- });
-
- await doPay(wallet, talerPayUri, { alwaysYes: true });
- });
-
-walletCli
.subcommand("", "balance", { help: "Show wallet balance." })
.action(async args => {
console.log("balance command called");
- withWallet(args, async (wallet) => {
+ await withWallet(args, async wallet => {
const balance = await wallet.getBalances();
console.log(JSON.stringify(balance, undefined, 2));
});
@@ -166,12 +142,12 @@ walletCli
walletCli
.subcommand("", "history", { help: "Show wallet event history." })
- .requiredOption("from", ["--from"], clk.STRING)
- .requiredOption("to", ["--to"], clk.STRING)
- .requiredOption("limit", ["--limit"], clk.STRING)
- .requiredOption("contEvt", ["--continue-with"], clk.STRING)
+ .maybeOption("from", ["--from"], clk.STRING)
+ .maybeOption("to", ["--to"], clk.STRING)
+ .maybeOption("limit", ["--limit"], clk.STRING)
+ .maybeOption("contEvt", ["--continue-with"], clk.STRING)
.action(async args => {
- withWallet(args, async (wallet) => {
+ await withWallet(args, async wallet => {
const history = await wallet.getHistory();
console.log(JSON.stringify(history, undefined, 2));
});
@@ -180,7 +156,7 @@ walletCli
walletCli
.subcommand("", "pending", { help: "Show pending operations." })
.action(async args => {
- withWallet(args, async (wallet) => {
+ await withWallet(args, async wallet => {
const pending = await wallet.getPendingOperations();
console.log(JSON.stringify(pending, undefined, 2));
});
@@ -194,25 +170,129 @@ async function asyncSleep(milliSeconds: number): Promise<void> {
walletCli
.subcommand("runPendingOpt", "run-pending", {
- help: "Run pending operations."
+ help: "Run pending operations.",
})
- .action(async (args) => {
- withWallet(args, async (wallet) => {
- await wallet.processPending();
+ .action(async args => {
+ await withWallet(args, async wallet => {
+ await wallet.runPending();
});
});
walletCli
- .subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
- .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
- default: "TESTKUDOS:1",
+ .subcommand("handleUri", "handle-uri", {
+ help: "Handle a taler:// URI.",
})
+ .requiredArgument("uri", clk.STRING)
+ .flag("autoYes", ["-y", "--yes"])
+ .action(async args => {
+ await withWallet(args, async wallet => {
+ const uri: string = args.handleUri.uri;
+ if (uri.startsWith("taler://pay/")) {
+ await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes });
+ } else if (uri.startsWith("taler://tip/")) {
+ const res = await wallet.getTipStatus(uri);
+ console.log("tip status", res);
+ await wallet.acceptTip(uri);
+ } else if (uri.startsWith("taler://refund/")) {
+ await wallet.applyRefund(uri);
+ } else if (uri.startsWith("taler://withdraw/")) {
+ const withdrawInfo = await wallet.getWithdrawalInfo(uri);
+ const selectedExchange = withdrawInfo.suggestedExchange;
+ if (!selectedExchange) {
+ console.error("no suggested exchange!");
+ process.exit(1);
+ return;
+ }
+ const { confirmTransferUrl } = await wallet.acceptWithdrawal(
+ uri,
+ selectedExchange,
+ );
+ if (confirmTransferUrl) {
+ console.log("please confirm the transfer at", confirmTransferUrl);
+ }
+ } else {
+ console.error("unrecognized URI");
+ }
+ });
+ });
+
+const exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", {
+ help: "Manage exchanges.",
+});
+
+exchangesCli
+ .subcommand("exchangesListCmd", "list", {
+ help: "List known exchanges.",
+ })
+ .action(async args => {
+ console.log("Listing exchanges ...");
+ await withWallet(args, async wallet => {
+ const exchanges = await wallet.getExchanges();
+ console.log("exchanges", exchanges);
+ });
+ });
+
+exchangesCli
+ .subcommand("exchangesUpdateCmd", "update", {
+ help: "Update or add an exchange by base URL.",
+ })
+ .requiredArgument("url", clk.STRING, {
+ help: "Base URL of the exchange.",
+ })
+ .flag("force", ["-f", "--force"])
+ .action(async args => {
+ await withWallet(args, async wallet => {
+ const res = await wallet.updateExchangeFromUrl(
+ args.exchangesUpdateCmd.url,
+ args.exchangesUpdateCmd.force,
+ );
+ });
+ });
+
+const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
+ help:
+ "Subcommands for advanced operations (only use if you know what you're doing!).",
+});
+
+advancedCli
+ .subcommand("refresh", "force-refresh", {
+ help: "Force a refresh on a coin.",
+ })
+ .requiredArgument("coinPub", clk.STRING)
+ .action(async args => {
+ await withWallet(args, async wallet => {
+ await wallet.refresh(args.refresh.coinPub, true);
+ });
+ });
+
+advancedCli
+ .subcommand("coins", "list-coins", {
+ help: "List coins.",
+ })
+ .action(async args => {
+ await withWallet(args, async wallet => {
+ const coins = await wallet.getCoins();
+ for (const coin of coins) {
+ console.log(`coin ${coin.coinPub}`);
+ console.log(` status ${coin.status}`);
+ console.log(` exchange ${coin.exchangeBaseUrl}`);
+ console.log(` remaining amount ${Amounts.toString(coin.currentAmount)}`);
+ }
+ });
+ });
+
+const testCli = walletCli.subcommand("testingArgs", "testing", {
+ help: "Subcommands for testing GNU Taler deployments.",
+});
+
+testCli
+ .subcommand("testPayCmd", "test-pay", { help: "create contract and pay" })
+ .requiredOption("amount", ["-a", "--amount"], clk.STRING)
.requiredOption("summary", ["-s", "--summary"], clk.STRING, {
default: "Test Payment",
})
.action(async args => {
- const cmdArgs = args.testMerchantQrcodeCmd;
- applyVerbose(args.wallet.verbose);
+ const cmdArgs = args.testPayCmd;
console.log("creating order");
const merchantBackend = new MerchantBackendConnection(
"https://backend.test.taler.net/",
@@ -225,7 +305,6 @@ walletCli
);
console.log("created new order with order ID", orderResp.orderId);
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
- const qrcode = qrcodeGenerator(0, "M");
const talerPayUri = checkPayResp.taler_pay_uri;
if (!talerPayUri) {
console.error("fatal: no taler pay URI received from backend");
@@ -233,23 +312,13 @@ walletCli
return;
}
console.log("taler pay URI:", talerPayUri);
- qrcode.addData(talerPayUri);
- qrcode.make();
- console.log(qrcode.createASCII());
- console.log("waiting for payment ...");
- while (1) {
- await asyncSleep(500);
- const checkPayResp2 = await merchantBackend.checkPayment(
- orderResp.orderId,
- );
- if (checkPayResp2.paid) {
- console.log("payment successfully received!");
- break;
- }
- }
+ await withWallet(args, async (wallet) => {
+ await doPay(wallet, talerPayUri, { alwaysYes: true });
+ });
});
-walletCli
+
+testCli
.subcommand("integrationtestCmd", "integrationtest", {
help: "Run integration test with bank, exchange and merchant.",
})
@@ -265,13 +334,14 @@ walletCli
.requiredOption("bank", ["-b", "--bank"], clk.STRING, {
default: "https://bank.test.taler.net/",
})
- .requiredOption("withdrawAmount", ["-b", "--bank"], clk.STRING, {
+ .requiredOption("withdrawAmount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:10",
})
.requiredOption("spendAmount", ["-s", "--spend-amount"], clk.STRING, {
default: "TESTKUDOS:4",
})
.action(async args => {
+ console.log("parsed args", args);
applyVerbose(args.wallet.verbose);
let cmdObj = args.integrationtestCmd;
@@ -295,128 +365,61 @@ walletCli
}
});
-walletCli
- .subcommand("withdrawUriCmd", "withdraw-uri")
- .requiredArgument("withdrawUri", clk.STRING)
+testCli
+ .subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
+ .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
+ default: "TESTKUDOS:1",
+ })
+ .requiredOption("summary", ["-s", "--summary"], clk.STRING, {
+ default: "Test Payment",
+ })
.action(async args => {
+ const cmdArgs = args.testMerchantQrcodeCmd;
applyVerbose(args.wallet.verbose);
- const cmdArgs = args.withdrawUriCmd;
- const withdrawUrl = cmdArgs.withdrawUri;
- console.log("withdrawing", withdrawUrl);
- const wallet = await getDefaultNodeWallet({
- persistentStoragePath: walletDbPath,
- });
-
- const withdrawInfo = await wallet.getWithdrawalInfo(withdrawUrl);
-
- console.log("withdraw info", withdrawInfo);
-
- const selectedExchange = withdrawInfo.suggestedExchange;
- if (!selectedExchange) {
- console.error("no suggested exchange!");
+ console.log("creating order");
+ const merchantBackend = new MerchantBackendConnection(
+ "https://backend.test.taler.net/",
+ "sandbox",
+ );
+ const orderResp = await merchantBackend.createOrder(
+ cmdArgs.amount,
+ cmdArgs.summary,
+ "",
+ );
+ console.log("created new order with order ID", orderResp.orderId);
+ const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
+ const qrcode = qrcodeGenerator(0, "M");
+ const talerPayUri = checkPayResp.taler_pay_uri;
+ if (!talerPayUri) {
+ console.error("fatal: no taler pay URI received from backend");
process.exit(1);
return;
}
-
- const { reservePub, confirmTransferUrl } = await wallet.acceptWithdrawal(
- withdrawUrl,
- selectedExchange,
- );
-
- if (confirmTransferUrl) {
- console.log("please confirm the transfer at", confirmTransferUrl);
+ console.log("taler pay URI:", talerPayUri);
+ qrcode.addData(talerPayUri);
+ qrcode.make();
+ console.log(qrcode.createASCII());
+ console.log("waiting for payment ...");
+ while (1) {
+ await asyncSleep(500);
+ const checkPayResp2 = await merchantBackend.checkPayment(
+ orderResp.orderId,
+ );
+ if (checkPayResp2.paid) {
+ console.log("payment successfully received!");
+ break;
+ }
}
-
- await wallet.processReserve(reservePub);
-
- console.log("finished withdrawing");
-
- wallet.stop();
- });
-
-walletCli
- .subcommand("tipUriCmd", "tip-uri")
- .requiredArgument("uri", clk.STRING)
- .action(async args => {
- applyVerbose(args.wallet.verbose);
- const tipUri = args.tipUriCmd.uri;
- console.log("getting tip", tipUri);
- const wallet = await getDefaultNodeWallet({
- persistentStoragePath: walletDbPath,
- });
- const res = await wallet.getTipStatus(tipUri);
- console.log("tip status", res);
- await wallet.acceptTip(tipUri);
- wallet.stop();
});
-walletCli
- .subcommand("refundUriCmd", "refund-uri")
- .requiredArgument("uri", clk.STRING)
- .action(async args => {
- applyVerbose(args.wallet.verbose);
- const refundUri = args.refundUriCmd.uri;
- console.log("getting refund", refundUri);
- const wallet = await getDefaultNodeWallet({
- persistentStoragePath: walletDbPath,
- });
- await wallet.applyRefund(refundUri);
- wallet.stop();
- });
-
-const exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", {
- help: "Manage exchanges.",
-});
-
-exchangesCli
- .subcommand("exchangesListCmd", "list", {
- help: "List known exchanges.",
- })
- .action(async args => {
- console.log("Listing exchanges ...");
- withWallet(args, async (wallet) => {
- const exchanges = await wallet.getExchanges();
- console.log("exchanges", exchanges);
- });
- });
-
-exchangesCli
- .subcommand("exchangesUpdateCmd", "update", {
- help: "Update or add an exchange by base URL.",
- })
- .requiredArgument("url", clk.STRING, {
- help: "Base URL of the exchange.",
- })
- .action(async args => {
- withWallet(args, async (wallet) => {
- const res = await wallet.updateExchangeFromUrl(args.exchangesUpdateCmd.url);
- });
- });
-
-walletCli
- .subcommand("payUriCmd", "pay-uri")
- .requiredArgument("url", clk.STRING)
- .flag("autoYes", ["-y", "--yes"])
- .action(async args => {
- applyVerbose(args.wallet.verbose);
- const payUrl = args.payUriCmd.url;
- console.log("paying for", payUrl);
- const wallet = await getDefaultNodeWallet({
- persistentStoragePath: walletDbPath,
- });
-
- await doPay(wallet, payUrl, { alwaysYes: args.payUriCmd.autoYes });
- wallet.stop();
- });
-
-const testCli = walletCli.subcommand("testingArgs", "testing", {
- help: "Subcommands for testing GNU Taler deployments.",
-});
-
testCli
.subcommand("withdrawArgs", "withdraw", {
help: "Withdraw from a test bank (must support test registrations).",
})
+ .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
+ default: "TESTKUDOS:10",
+ help: "Amount to withdraw.",
+ })
.requiredOption("exchange", ["-e", "--exchange"], clk.STRING, {
default: "https://exchange.test.taler.net/",
help: "Exchange base URL.",
@@ -426,14 +429,15 @@ testCli
help: "Bank base URL",
})
.action(async args => {
- applyVerbose(args.wallet.verbose);
- console.log("balance command called");
- const wallet = await getDefaultNodeWallet({
- persistentStoragePath: walletDbPath,
+ await withWallet(args, async wallet => {
+ await withdrawTestBalance(
+ wallet,
+ args.withdrawArgs.amount,
+ args.withdrawArgs.bank,
+ args.withdrawArgs.exchange,
+ );
+ logger.info("Withdraw done");
});
- console.log("got wallet");
- const balance = await wallet.getBalances();
- console.log(JSON.stringify(balance, undefined, 2));
});
walletCli.run();
diff --git a/src/http.ts b/src/http.ts
index 8c1f772d8..a2bfab279 100644
--- a/src/http.ts
+++ b/src/http.ts
@@ -107,10 +107,3 @@ export class BrowserHttpLib implements HttpRequestLibrary {
return this.req("post", url, { req: form });
}
}
-
-/**
- * Exception thrown on request errors.
- */
-export class RequestException {
- constructor(public detail: any) {}
-}
diff --git a/src/query.ts b/src/query.ts
index f510da55d..5726bcaa6 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -1,5 +1,3 @@
-import { openPromise } from "./promiseUtils";
-
/*
This file is part of TALER
(C) 2016 GNUnet e.V.
@@ -23,6 +21,12 @@ import { openPromise } from "./promiseUtils";
*/
/**
+ * Imports.
+ */
+import { openPromise } from "./promiseUtils";
+
+
+/**
* Result of an inner join.
*/
export interface JoinResult<L, R> {
@@ -63,27 +67,48 @@ export interface IndexOptions {
}
function requestToPromise(req: IDBRequest): Promise<any> {
+ const stack = Error("Failed request was started here.")
return new Promise((resolve, reject) => {
req.onsuccess = () => {
resolve(req.result);
};
req.onerror = () => {
+ console.log("error in DB request", req.error);
reject(req.error);
+ console.log("Request failed:", stack);
};
});
}
-export function oneShotGet<T>(
+function transactionToPromise(tx: IDBTransaction): Promise<void> {
+ const stack = Error("Failed transaction was started here.");
+ return new Promise((resolve, reject) => {
+ tx.onabort = () => {
+ reject(TransactionAbort);
+ };
+ tx.oncomplete = () => {
+ resolve();
+ };
+ tx.onerror = () => {
+ console.error("Transaction failed:", stack);
+ reject(tx.error);
+ };
+ });
+}
+
+export async function oneShotGet<T>(
db: IDBDatabase,
store: Store<T>,
key: any,
): Promise<T | undefined> {
const tx = db.transaction([store.name], "readonly");
const req = tx.objectStore(store.name).get(key);
- return requestToPromise(req);
+ const v = await requestToPromise(req)
+ await transactionToPromise(tx);
+ return v;
}
-export function oneShotGetIndexed<S extends IDBValidKey, T>(
+export async function oneShotGetIndexed<S extends IDBValidKey, T>(
db: IDBDatabase,
index: Index<S, T>,
key: any,
@@ -93,10 +118,12 @@ export function oneShotGetIndexed<S extends IDBValidKey, T>(
.objectStore(index.storeName)
.index(index.indexName)
.get(key);
- return requestToPromise(req);
+ const v = await requestToPromise(req);
+ await transactionToPromise(tx);
+ return v;
}
-export function oneShotPut<T>(
+export async function oneShotPut<T>(
db: IDBDatabase,
store: Store<T>,
value: T,
@@ -104,7 +131,9 @@ export function oneShotPut<T>(
): Promise<any> {
const tx = db.transaction([store.name], "readwrite");
const req = tx.objectStore(store.name).put(value, key);
- return requestToPromise(req);
+ const v = await requestToPromise(req);
+ await transactionToPromise(tx);
+ return v;
}
function applyMutation<T>(
@@ -115,7 +144,7 @@ function applyMutation<T>(
req.onsuccess = () => {
const cursor = req.result;
if (cursor) {
- const val = cursor.value();
+ const val = cursor.value;
const modVal = f(val);
if (modVal !== undefined && modVal !== null) {
const req2: IDBRequest = cursor.update(modVal);
@@ -138,7 +167,7 @@ function applyMutation<T>(
});
}
-export function oneShotMutate<T>(
+export async function oneShotMutate<T>(
db: IDBDatabase,
store: Store<T>,
key: any,
@@ -146,7 +175,8 @@ export function oneShotMutate<T>(
): Promise<void> {
const tx = db.transaction([store.name], "readwrite");
const req = tx.objectStore(store.name).openCursor(key);
- return applyMutation(req, f);
+ await applyMutation(req, f);
+ await transactionToPromise(tx);
}
type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
@@ -326,15 +356,12 @@ export function runWithWriteTransaction<T>(
stores: Store<any>[],
f: (t: TransactionHandle) => Promise<T>,
): Promise<T> {
+ const stack = Error("Failed transaction was started here.");
return new Promise((resolve, reject) => {
const storeName = stores.map(x => x.name);
const tx = db.transaction(storeName, "readwrite");
let funResult: any = undefined;
let gotFunResult: boolean = false;
- tx.onerror = () => {
- console.error("error in transaction:", tx.error);
- reject(tx.error);
- };
tx.oncomplete = () => {
// This is a fatal error: The transaction completed *before*
// the transaction function returned. Likely, the transaction
@@ -350,15 +377,30 @@ export function runWithWriteTransaction<T>(
}
resolve(funResult);
};
+ tx.onerror = () => {
+ console.error("error in transaction");
+ };
tx.onabort = () => {
- console.error("aborted transaction");
- reject(AbortTransaction);
+ if (tx.error) {
+ console.error("Transaction aborted with error:", tx.error);
+ } else {
+ console.log("Trasaction aborted (no error)");
+ }
+ reject(TransactionAbort);
};
const th = new TransactionHandle(tx);
const resP = f(th);
resP.then(result => {
gotFunResult = true;
funResult = result;
+ }).catch((e) => {
+ if (e == TransactionAbort) {
+ console.info("aborting transaction");
+ } else {
+ tx.abort();
+ console.error("Transaction failed:", e);
+ console.error(stack);
+ }
});
});
}
@@ -401,4 +443,4 @@ export class Index<S extends IDBValidKey, T> {
/**
* Exception that should be thrown by client code to abort a transaction.
*/
-export const AbortTransaction = Symbol("abort_transaction");
+export const TransactionAbort = Symbol("transaction_abort");
diff --git a/src/wallet.ts b/src/wallet.ts
index 71e058fd9..58bb6b8c3 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -31,10 +31,10 @@ import {
strcmp,
extractTalerStamp,
} from "./helpers";
-import { HttpRequestLibrary, RequestException } from "./http";
+import { HttpRequestLibrary } from "./http";
import * as LibtoolVersion from "./libtoolVersion";
import {
- AbortTransaction,
+ TransactionAbort,
oneShotPut,
oneShotGet,
runWithWriteTransaction,
@@ -43,7 +43,6 @@ import {
oneShotGetIndexed,
oneShotMutate,
} from "./query";
-import { TimerGroup } from "./timer";
import { AmountJson } from "./amounts";
import * as Amounts from "./amounts";
@@ -70,6 +69,7 @@ import {
WithdrawalRecord,
ExchangeDetails,
ExchangeUpdateStatus,
+ ReserveRecordStatus,
} from "./dbTypes";
import {
Auditor,
@@ -99,7 +99,7 @@ import {
ConfirmReserveRequest,
CreateReserveRequest,
CreateReserveResponse,
- HistoryRecord,
+ HistoryEvent,
NextUrlResult,
Notifier,
PayCoinInfo,
@@ -119,8 +119,8 @@ import {
HistoryQuery,
getTimestampNow,
OperationError,
+ Timestamp,
} from "./walletTypes";
-import { openPromise } from "./promiseUtils";
import {
parsePayUri,
parseWithdrawUri,
@@ -128,6 +128,7 @@ import {
parseRefundUri,
} from "./taleruri";
import { isFirefox } from "./webex/compat";
+import { Logger } from "./logging";
interface SpeculativePayData {
payCoinInfo: PayCoinInfo;
@@ -343,22 +344,24 @@ interface CoinsForPaymentArgs {
paymentAmount: AmountJson;
wireFeeAmortization: number;
wireFeeLimit: AmountJson;
- wireFeeTime: number;
+ wireFeeTime: Timestamp;
wireMethod: string;
}
/**
* This error is thrown when an
*/
-class OperationFailedAndReportedError extends Error {
- constructor(public reason: Error) {
- super("Reported failed operation: " + reason.message);
+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");
+
/**
* The platform-independent wallet implementation.
*/
@@ -372,26 +375,8 @@ export class Wallet {
private badge: Badge;
private notifier: Notifier;
private cryptoApi: CryptoApi;
- private processPreCoinConcurrent = 0;
- private processPreCoinThrottle: { [url: string]: number } = {};
- private timerGroup: TimerGroup;
private speculativePayData: SpeculativePayData | undefined;
private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
- private activeTipOperations: { [s: string]: Promise<void> } = {};
- private activeProcessReserveOperations: {
- [reservePub: string]: Promise<void>;
- } = {};
- private activeProcessPreCoinOperations: {
- [preCoinPub: string]: Promise<void>;
- } = {};
- private activeRefreshOperations: {
- [coinPub: string]: Promise<void>;
- } = {};
-
- /**
- * Set of identifiers for running operations.
- */
- private runningOperations: Set<string> = new Set();
constructor(
db: IDBDatabase,
@@ -405,10 +390,13 @@ export class Wallet {
this.badge = badge;
this.notifier = notifier;
this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
- this.timerGroup = new TimerGroup();
}
- public async processPending(): Promise<void> {
+ /**
+ * Process pending operations.
+ */
+ public async runPending(): Promise<void> {
+ // FIXME: maybe prioritize pending operations by their urgency?
const exchangeBaseUrlList = await oneShotIter(
this.db,
Stores.exchanges,
@@ -417,19 +405,61 @@ export class Wallet {
for (let exchangeBaseUrl of exchangeBaseUrlList) {
await this.updateExchangeFromUrl(exchangeBaseUrl);
}
+
+ const reservesPubList = await oneShotIter(this.db, Stores.reserves).map(
+ x => x.reservePub,
+ );
+
+ for (let reservePub of reservesPubList) {
+ await this.processReserve(reservePub);
+ }
+
+ const refreshSessionList = await oneShotIter(this.db, Stores.refresh).map(
+ x => x.refreshSessionId,
+ );
+ for (let rs of refreshSessionList) {
+ await this.processRefreshSession(rs);
+ }
}
/**
- * Start processing pending operations asynchronously.
+ * Process pending operations and wait for scheduled operations in
+ * a loop until the wallet is stopped explicitly.
*/
- public start() {
- const work = async () => {
- await this.collectGarbage().catch(e => console.log(e));
- this.updateExchanges();
- this.resumePendingFromDb();
- this.timerGroup.every(1000 * 60 * 15, () => this.updateExchanges());
- };
- work();
+ public async runUntilStopped(): Promise<void> {
+ throw Error("not implemented");
+ }
+
+ /**
+ * Run until all coins have been withdrawn from the given reserve,
+ * or an error has occured.
+ */
+ public async runUntilReserveDepleted(reservePub: string) {
+ while (true) {
+ let reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
+ if (!reserve) {
+ throw Error("Reserve does not exist.");
+ }
+ if (reserve.lastError !== undefined) {
+ throw Error("Reserve error: " + reserve.lastError.message);
+ }
+ if (reserve.reserveStatus === ReserveRecordStatus.UNCONFIRMED) {
+ throw Error("Reserve is not confirmed.");
+ }
+ if (reserve.reserveStatus === ReserveRecordStatus.DORMANT) {
+ // Check if all withdraws are done!
+ const precoins = await oneShotIterIndex(
+ this.db,
+ Stores.precoins.byReservePub,
+ reservePub,
+ ).toArray();
+ for (const pc of precoins) {
+ await this.processPreCoin(pc.coinPub);
+ }
+ break;
+ }
+ await this.processReserve(reservePub);
+ }
}
/**
@@ -457,18 +487,6 @@ export class Wallet {
);
}
- private startOperation(operationId: string) {
- this.runningOperations.add(operationId);
- this.badge.startBusy();
- }
-
- private stopOperation(operationId: string) {
- this.runningOperations.delete(operationId);
- if (this.runningOperations.size === 0) {
- this.badge.stopBusy();
- }
- }
-
async updateExchanges(): Promise<void> {
const exchangeUrls = await oneShotIter(this.db, Stores.exchanges).map(
e => e.baseUrl,
@@ -481,35 +499,6 @@ export class Wallet {
}
}
- /**
- * Resume various pending operations that are pending
- * by looking at the database.
- */
- private resumePendingFromDb(): void {
- Wallet.enableTracing && console.log("resuming pending operations from db");
-
- oneShotIter(this.db, Stores.reserves).forEach(reserve => {
- Wallet.enableTracing &&
- console.log("resuming reserve", reserve.reserve_pub);
- this.processReserve(reserve.reserve_pub);
- });
-
- oneShotIter(this.db, Stores.precoins).forEach(preCoin => {
- Wallet.enableTracing && console.log("resuming precoin");
- this.processPreCoin(preCoin.coinPub);
- });
-
- oneShotIter(this.db, Stores.refresh).forEach((r: RefreshSessionRecord) => {
- this.continueRefreshSession(r);
- });
-
- oneShotIter(this.db, Stores.coinsReturns).forEach(
- (r: CoinsReturnRecord) => {
- this.depositReturnedCoins(r);
- },
- );
- }
-
private async getCoinsForReturn(
exchangeBaseUrl: string,
amount: AmountJson,
@@ -752,8 +741,8 @@ export class Wallet {
payReq,
refundsDone: {},
refundsPending: {},
- timestamp: new Date().getTime(),
- timestamp_refund: 0,
+ timestamp: getTimestampNow(),
+ timestamp_refund: undefined,
};
await runWithWriteTransaction(
@@ -819,7 +808,13 @@ export class Wallet {
proposal.contractTerms.fulfillment_url,
);
- if (differentPurchase) {
+ let fulfillmentUrl = proposal.contractTerms.fulfillment_url;
+ let doublePurchaseDetection = false;
+ if (fulfillmentUrl.startsWith("http")) {
+ doublePurchaseDetection = true;
+ }
+
+ if (differentPurchase && doublePurchaseDetection) {
// We do this check to prevent merchant B to find out if we bought a
// digital product with merchant A by abusing the existing payment
// redirect feature.
@@ -870,7 +865,10 @@ export class Wallet {
paymentAmount,
wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
wireFeeLimit,
- wireFeeTime: getTalerStampSec(proposal.contractTerms.timestamp) || 0,
+ // FIXME: parse this properly
+ wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
+ t_ms: 0,
+ },
wireMethod: proposal.contractTerms.wire_method,
});
@@ -962,7 +960,7 @@ export class Wallet {
contractTermsHash,
merchantSig: proposal.sig,
noncePriv: priv,
- timestamp: new Date().getTime(),
+ timestamp: getTimestampNow(),
url,
downloadSessionId: sessionId,
};
@@ -1143,7 +1141,10 @@ export class Wallet {
paymentAmount: Amounts.parseOrThrow(proposal.contractTerms.amount),
wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
wireFeeLimit,
- wireFeeTime: getTalerStampSec(proposal.contractTerms.timestamp) || 0,
+ // FIXME: parse this properly
+ wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
+ t_ms: 0,
+ },
wireMethod: proposal.contractTerms.wire_method,
});
@@ -1218,7 +1219,7 @@ export class Wallet {
}
/**
- * Send reserve details
+ * Send reserve details to the bank.
*/
private async sendReserveInfoToBank(reservePub: string) {
const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
@@ -1226,12 +1227,16 @@ export class Wallet {
throw Error("reserve not in db");
}
+ if (reserve.reserveStatus != ReserveRecordStatus.REGISTERING_BANK) {
+ return;
+ }
+
const bankStatusUrl = reserve.bankWithdrawStatusUrl;
if (!bankStatusUrl) {
- throw Error("reserve not confirmed yet, and no status URL available.");
+ throw Error("no bank withdraw status URL available.");
}
- const now = new Date().getTime();
+ const now = getTimestampNow();
let status;
try {
const statusResp = await this.http.get(bankStatusUrl);
@@ -1243,10 +1248,10 @@ export class Wallet {
if (status.transfer_done) {
await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
- r.timestamp_confirmed = now;
+ r.timestampConfirmed = now;
return r;
});
- } else if (reserve.timestamp_reserve_info_posted === 0) {
+ } else if (reserve.timestampReserveInfoPosted === undefined) {
try {
if (!status.selection_done) {
const bankResp = await this.http.postJson(bankStatusUrl, {
@@ -1259,7 +1264,7 @@ export class Wallet {
throw e;
}
await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
- r.timestamp_reserve_info_posted = now;
+ r.timestampReserveInfoPosted = now;
return r;
});
}
@@ -1268,73 +1273,38 @@ export class Wallet {
/**
* First fetch information requred to withdraw from the reserve,
* then deplete the reserve, withdrawing coins until it is empty.
+ *
+ * The returned promise resolves once the reserve is set to the
+ * state DORMANT.
*/
async processReserve(reservePub: string): Promise<void> {
- const activeOperation = this.activeProcessReserveOperations[reservePub];
-
- if (activeOperation) {
- return activeOperation;
+ const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
+ if (!reserve) {
+ console.log("not processing reserve: reserve does not exist");
+ return;
}
-
- const opId = "reserve-" + reservePub;
- this.startOperation(opId);
-
- // This opened promise gets resolved only once the
- // reserve withdraw operation succeeds, even after retries.
- const op = openPromise<void>();
-
- const processReserveInternal = async (retryDelayMs: number = 250) => {
- let isHardError = false;
- // By default, do random, exponential backoff truncated at 3 minutes.
- // Sometimes though, we want to try again faster.
- let maxTimeout = 3000 * 60;
- try {
- const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
- if (!reserve) {
- isHardError = true;
- throw Error("reserve not in db");
- }
-
- if (reserve.timestamp_confirmed === 0) {
- const bankStatusUrl = reserve.bankWithdrawStatusUrl;
- if (!bankStatusUrl) {
- isHardError = true;
- throw Error(
- "reserve not confirmed yet, and no status URL available.",
- );
- }
- maxTimeout = 2000;
- /* This path is only taken if the wallet crashed after a withdraw was accepted,
- * and before the information could be sent to the bank. */
- await this.sendReserveInfoToBank(reservePub);
- throw Error("waiting for reserve to be confirmed");
- }
-
- const updatedReserve = await this.updateReserve(reservePub);
- await this.depleteReserve(updatedReserve);
- op.resolve();
- } catch (e) {
- if (isHardError) {
- op.reject(e);
- }
- const nextDelay = Math.min(
- 2 * retryDelayMs + retryDelayMs * Math.random(),
- maxTimeout,
- );
-
- this.timerGroup.after(retryDelayMs, () =>
- processReserveInternal(nextDelay),
- );
- }
- };
-
- try {
- processReserveInternal();
- this.activeProcessReserveOperations[reservePub] = op.promise;
- await op.promise;
- } finally {
- this.stopOperation(opId);
- delete this.activeProcessReserveOperations[reservePub];
+ logger.trace(
+ `Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
+ );
+ switch (reserve.reserveStatus) {
+ case ReserveRecordStatus.UNCONFIRMED:
+ // nothing to do
+ break;
+ case ReserveRecordStatus.REGISTERING_BANK:
+ await this.sendReserveInfoToBank(reservePub);
+ return this.processReserve(reservePub);
+ case ReserveRecordStatus.QUERYING_STATUS:
+ await this.updateReserve(reservePub);
+ return this.processReserve(reservePub);
+ case ReserveRecordStatus.WITHDRAWING:
+ await this.depleteReserve(reservePub);
+ break;
+ case ReserveRecordStatus.DORMANT:
+ // nothing to do
+ break;
+ default:
+ console.warn("unknown reserve record status:", reserve.reserveStatus);
+ break;
}
}
@@ -1342,117 +1312,89 @@ export class Wallet {
* Given a planchet, withdraw a coin from the exchange.
*/
private async processPreCoin(preCoinPub: string): Promise<void> {
- const activeOperation = this.activeProcessPreCoinOperations[preCoinPub];
- if (activeOperation) {
- return activeOperation;
+ console.log("processPreCoin", preCoinPub);
+ const preCoin = await oneShotGet(this.db, Stores.precoins, preCoinPub);
+ if (!preCoin) {
+ console.log("processPreCoin: preCoinPub not found");
+ return;
+ }
+ const exchange = await oneShotGet(
+ this.db,
+ Stores.exchanges,
+ preCoin.exchangeBaseUrl,
+ );
+ if (!exchange) {
+ console.error("db inconsistent: exchange for precoin not found");
+ return;
}
- const op = openPromise<void>();
-
- const processPreCoinInternal = async (retryDelayMs: number = 200) => {
- const preCoin = await oneShotGet(this.db, Stores.precoins, preCoinPub);
- if (!preCoin) {
- console.log("processPreCoin: preCoinPub not found");
- return;
- }
- // Throttle concurrent executions of this function,
- // so we don't withdraw too many coins at once.
- if (
- this.processPreCoinConcurrent >= 4 ||
- this.processPreCoinThrottle[preCoin.exchangeBaseUrl]
- ) {
- const timeout = Math.min(retryDelayMs * 2, 5 * 60 * 1000);
- Wallet.enableTracing &&
- console.log(
- `throttling processPreCoin of ${preCoinPub} for ${timeout}ms`,
- );
- this.timerGroup.after(retryDelayMs, () => processPreCoinInternal());
- return op.promise;
- }
-
- this.processPreCoinConcurrent++;
-
- try {
- const exchange = await oneShotGet(
- this.db,
- Stores.exchanges,
- preCoin.exchangeBaseUrl,
- );
- if (!exchange) {
- console.error("db inconsistent: exchange for precoin not found");
- return;
- }
- const denom = await oneShotGet(this.db, Stores.denominations, [
- preCoin.exchangeBaseUrl,
- preCoin.denomPub,
- ]);
- if (!denom) {
- console.error("db inconsistent: denom for precoin not found");
- return;
- }
+ const denom = await oneShotGet(this.db, Stores.denominations, [
+ preCoin.exchangeBaseUrl,
+ preCoin.denomPub,
+ ]);
- const coin = await this.withdrawExecute(preCoin);
+ if (!denom) {
+ console.error("db inconsistent: denom for precoin not found");
+ return;
+ }
- const mutateReserve = (r: ReserveRecord) => {
- const x = Amounts.sub(
- r.precoin_amount,
- preCoin.coinValue,
- denom.feeWithdraw,
- );
- if (x.saturated) {
- console.error("database inconsistent");
- throw AbortTransaction;
- }
- r.precoin_amount = x.amount;
- return r;
- };
+ const wd: any = {};
+ wd.denom_pub_hash = preCoin.denomPubHash;
+ wd.reserve_pub = preCoin.reservePub;
+ wd.reserve_sig = preCoin.withdrawSig;
+ wd.coin_ev = preCoin.coinEv;
+ const reqUrl = new URI("reserve/withdraw").absoluteTo(exchange.baseUrl);
+ const resp = await this.http.postJson(reqUrl.href(), wd);
- await runWithWriteTransaction(
- this.db,
- [Stores.reserves, Stores.precoins, Stores.coins],
- async tx => {
- await tx.mutate(Stores.reserves, preCoin.reservePub, mutateReserve);
- await tx.delete(Stores.precoins, coin.coinPub);
- await tx.add(Stores.coins, coin);
- },
- );
+ const r = resp.responseJson;
- this.badge.showNotification();
+ const denomSig = await this.cryptoApi.rsaUnblind(
+ r.ev_sig,
+ preCoin.blindingKey,
+ preCoin.denomPub,
+ );
- this.notifier.notify();
- op.resolve();
- } catch (e) {
- console.error(
- "Failed to withdraw coin from precoin, retrying in",
- retryDelayMs,
- "ms",
- e,
- );
- // exponential backoff truncated at one minute
- const nextRetryDelayMs = Math.min(retryDelayMs * 2, 5 * 60 * 1000);
- this.timerGroup.after(retryDelayMs, () =>
- processPreCoinInternal(nextRetryDelayMs),
- );
+ const coin: CoinRecord = {
+ blindingKey: preCoin.blindingKey,
+ coinPriv: preCoin.coinPriv,
+ coinPub: preCoin.coinPub,
+ currentAmount: preCoin.coinValue,
+ denomPub: preCoin.denomPub,
+ denomPubHash: preCoin.denomPubHash,
+ denomSig,
+ exchangeBaseUrl: preCoin.exchangeBaseUrl,
+ reservePub: preCoin.reservePub,
+ status: CoinStatus.Fresh,
+ };
- const currentThrottle =
- this.processPreCoinThrottle[preCoin.exchangeBaseUrl] || 0;
- this.processPreCoinThrottle[preCoin.exchangeBaseUrl] =
- currentThrottle + 1;
- this.timerGroup.after(retryDelayMs, () => {
- this.processPreCoinThrottle[preCoin.exchangeBaseUrl]--;
- });
- } finally {
- this.processPreCoinConcurrent--;
+ const mutateReserve = (r: ReserveRecord) => {
+ const x = Amounts.sub(
+ r.precoinAmount,
+ preCoin.coinValue,
+ denom.feeWithdraw,
+ );
+ if (x.saturated) {
+ console.error("database inconsistent");
+ throw TransactionAbort;
}
+ r.precoinAmount = x.amount;
+ return r;
};
- try {
- this.activeProcessPreCoinOperations[preCoinPub] = op.promise;
- await processPreCoinInternal();
- return op.promise;
- } finally {
- delete this.activeProcessPreCoinOperations[preCoinPub];
- }
+ await runWithWriteTransaction(
+ this.db,
+ [Stores.reserves, Stores.precoins, Stores.coins],
+ async tx => {
+ const currentPc = await tx.get(Stores.precoins, coin.coinPub);
+ if (!currentPc) {
+ return;
+ }
+ await tx.mutate(Stores.reserves, preCoin.reservePub, mutateReserve);
+ await tx.delete(Stores.precoins, coin.coinPub);
+ await tx.add(Stores.coins, coin);
+ },
+ );
+ logger.trace(`withdraw of one coin ${coin.coinPub} finished`);
}
/**
@@ -1465,24 +1407,31 @@ export class Wallet {
req: CreateReserveRequest,
): Promise<CreateReserveResponse> {
const keypair = await this.cryptoApi.createEddsaKeypair();
- const now = new Date().getTime();
+ const now = getTimestampNow();
const canonExchange = canonicalizeBaseUrl(req.exchange);
+ let reserveStatus;
+ if (req.bankWithdrawStatusUrl) {
+ reserveStatus = ReserveRecordStatus.REGISTERING_BANK;
+ } else {
+ reserveStatus = ReserveRecordStatus.UNCONFIRMED;
+ }
+
const reserveRecord: ReserveRecord = {
created: now,
- current_amount: null,
- exchange_base_url: canonExchange,
+ currentAmount: null,
+ exchangeBaseUrl: canonExchange,
hasPayback: false,
- precoin_amount: Amounts.getZero(req.amount.currency),
- requested_amount: req.amount,
- reserve_priv: keypair.priv,
- reserve_pub: keypair.pub,
+ precoinAmount: Amounts.getZero(req.amount.currency),
+ requestedAmount: req.amount,
+ reservePriv: keypair.priv,
+ reservePub: keypair.pub,
senderWire: req.senderWire,
- timestamp_confirmed: 0,
- timestamp_reserve_info_posted: 0,
- timestamp_depleted: 0,
+ timestampConfirmed: undefined,
+ timestampReserveInfoPosted: undefined,
bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
exchangeWire: req.exchangeWire,
+ reserveStatus,
};
const senderWire = req.senderWire;
@@ -1522,7 +1471,7 @@ export class Wallet {
const cr: CurrencyRecord = currencyRecord;
- runWithWriteTransaction(
+ await runWithWriteTransaction(
this.db,
[Stores.currencies, Stores.reserves],
async tx => {
@@ -1531,9 +1480,9 @@ export class Wallet {
},
);
- if (req.bankWithdrawStatusUrl) {
- this.processReserve(keypair.pub);
- }
+ this.processReserve(keypair.pub).catch(e => {
+ console.error("Processing reserve failed:", e);
+ });
const r: CreateReserveResponse = {
exchange: canonExchange,
@@ -1552,53 +1501,21 @@ export class Wallet {
* an unconfirmed reserve should be hidden.
*/
async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
- const now = new Date().getTime();
- const reserve = await oneShotGet(this.db, Stores.reserves, req.reservePub);
- if (!reserve) {
- console.error("Unable to confirm reserve, not found in DB");
- return;
- }
- reserve.timestamp_confirmed = now;
- await oneShotPut(this.db, Stores.reserves, reserve);
- this.notifier.notify();
-
- this.processReserve(reserve.reserve_pub);
- }
+ const now = getTimestampNow();
+ await oneShotMutate(this.db, Stores.reserves, req.reservePub, reserve => {
+ if (reserve.reserveStatus !== ReserveRecordStatus.UNCONFIRMED) {
+ return;
+ }
+ reserve.timestampConfirmed = now;
+ reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+ return reserve;
+ });
- private async withdrawExecute(pc: PreCoinRecord): Promise<CoinRecord> {
- const wd: any = {};
- wd.denom_pub_hash = pc.denomPubHash;
- wd.reserve_pub = pc.reservePub;
- wd.reserve_sig = pc.withdrawSig;
- wd.coin_ev = pc.coinEv;
- const reqUrl = new URI("reserve/withdraw").absoluteTo(pc.exchangeBaseUrl);
- const resp = await this.http.postJson(reqUrl.href(), wd);
+ this.notifier.notify();
- if (resp.status !== 200) {
- throw new RequestException({
- hint: "Withdrawal failed",
- status: resp.status,
- });
- }
- const r = resp.responseJson;
- const denomSig = await this.cryptoApi.rsaUnblind(
- r.ev_sig,
- pc.blindingKey,
- pc.denomPub,
- );
- const coin: CoinRecord = {
- blindingKey: pc.blindingKey,
- coinPriv: pc.coinPriv,
- coinPub: pc.coinPub,
- currentAmount: pc.coinValue,
- denomPub: pc.denomPub,
- denomPubHash: pc.denomPubHash,
- denomSig,
- exchangeBaseUrl: pc.exchangeBaseUrl,
- reservePub: pc.reservePub,
- status: CoinStatus.Fresh,
- };
- return coin;
+ this.processReserve(req.reservePub).catch(e => {
+ console.log("processing reserve failed:", e);
+ });
}
/**
@@ -1607,31 +1524,41 @@ export class Wallet {
* When finished, marks the reserve as depleted by setting
* the depleted timestamp.
*/
- private async depleteReserve(reserve: ReserveRecord): Promise<void> {
- Wallet.enableTracing && console.log("depleting reserve");
- if (!reserve.current_amount) {
- throw Error("can't withdraw when amount is unknown");
+ private async depleteReserve(reservePub: string): Promise<void> {
+ const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
+ if (!reserve) {
+ return;
}
- const withdrawAmount = reserve.current_amount;
+ if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
+ return;
+ }
+ logger.trace(`depleting reserve ${reservePub}`);
+
+ const withdrawAmount = reserve.currentAmount;
if (!withdrawAmount) {
- throw Error("can't withdraw when amount is unknown");
+ throw Error("BUG: reserveStatus=WITHDRAWING, but currentAmount is empty");
}
+
const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(
- reserve.exchange_base_url,
+ reserve.exchangeBaseUrl,
withdrawAmount,
);
- const smallestAmount = await this.getVerifiedSmallestWithdrawAmount(
- reserve.exchange_base_url,
- );
-
- console.log(`withdrawing ${denomsForWithdraw.length} coins`);
-
- const stampMsNow = Math.floor(new Date().getTime());
+ if (denomsForWithdraw.length === 0) {
+ const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
+ await this.setReserveError(reserve.reservePub, {
+ type: "internal",
+ message: m,
+ details: {},
+ });
+ throw new OperationFailedAndReportedError(m);
+ }
const withdrawalRecord: WithdrawalRecord = {
- reservePub: reserve.reserve_pub,
+ reservePub: reserve.reservePub,
withdrawalAmount: Amounts.toString(withdrawAmount),
- startTimestamp: stampMsNow,
+ startTimestamp: getTimestampNow(),
+ numCoinsTotal: denomsForWithdraw.length,
+ numCoinsWithdrawn: 0,
};
const preCoinRecords: PreCoinRecord[] = await Promise.all(
@@ -1651,49 +1578,50 @@ export class Wallet {
).amount;
function mutateReserve(r: ReserveRecord): ReserveRecord {
- const currentAmount = r.current_amount;
+ const currentAmount = r.currentAmount;
if (!currentAmount) {
throw Error("can't withdraw when amount is unknown");
}
- r.precoin_amount = Amounts.add(
- r.precoin_amount,
+ r.precoinAmount = Amounts.add(
+ r.precoinAmount,
totalWithdrawAmount,
).amount;
const result = Amounts.sub(currentAmount, totalWithdrawAmount);
if (result.saturated) {
console.error("can't create precoins, saturated");
- throw AbortTransaction;
- }
- r.current_amount = result.amount;
-
- // Reserve is depleted if the amount left is too small to withdraw
- if (Amounts.cmp(r.current_amount, smallestAmount) < 0) {
- r.timestamp_depleted = new Date().getTime();
+ throw TransactionAbort;
}
+ r.currentAmount = result.amount;
+ r.reserveStatus = ReserveRecordStatus.DORMANT;
return r;
}
- // This will fail and throw an exception if the remaining amount in the
- // reserve is too low to create a pre-coin.
- try {
- await runWithWriteTransaction(
- this.db,
- [Stores.precoins, Stores.withdrawals, Stores.reserves],
- async tx => {
- for (let pcr of preCoinRecords) {
- await tx.put(Stores.precoins, pcr);
- }
- await tx.mutate(Stores.reserves, reserve.reserve_pub, mutateReserve);
- await tx.put(Stores.withdrawals, withdrawalRecord);
- },
- );
- } catch (e) {
- return;
- }
+ const success = await runWithWriteTransaction(
+ this.db,
+ [Stores.precoins, Stores.withdrawals, Stores.reserves],
+ async tx => {
+ const myReserve = await tx.get(Stores.reserves, reservePub);
+ if (!myReserve) {
+ return false;
+ }
+ if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
+ return false;
+ }
+ for (let pcr of preCoinRecords) {
+ await tx.put(Stores.precoins, pcr);
+ }
+ await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve);
+ await tx.put(Stores.withdrawals, withdrawalRecord);
+ return true;
+ },
+ );
- for (let x of preCoinRecords) {
- await this.processPreCoin(x.coinPub);
+ if (success) {
+ logger.trace(`withdrawing ${preCoinRecords.length} coins`);
+ for (let x of preCoinRecords) {
+ await this.processPreCoin(x.coinPub);
+ }
}
}
@@ -1701,34 +1629,52 @@ export class Wallet {
* Update the information about a reserve that is stored in the wallet
* by quering the reserve's exchange.
*/
- private async updateReserve(reservePub: string): Promise<ReserveRecord> {
+ private async updateReserve(reservePub: string): Promise<void> {
const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
if (!reserve) {
throw Error("reserve not in db");
}
- if (reserve.timestamp_confirmed === 0) {
- throw Error("");
+ if (reserve.timestampConfirmed === undefined) {
+ throw Error("reserve not confirmed yet");
+ }
+
+ if (reserve.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
+ return;
}
const reqUrl = new URI("reserve/status").absoluteTo(
- reserve.exchange_base_url,
+ reserve.exchangeBaseUrl,
);
reqUrl.query({ reserve_pub: reservePub });
- const resp = await this.http.get(reqUrl.href());
- if (resp.status !== 200) {
- Wallet.enableTracing &&
- console.warn(`reserve/status returned ${resp.status}`);
- throw Error();
+ let resp;
+ try {
+ resp = await this.http.get(reqUrl.href());
+ } catch (e) {
+ if (e.response?.status === 404) {
+ console.log("Reserve now known to exchange (yet).");
+ return;
+ } else {
+ const m = e.message;
+ this.setReserveError(reservePub, {
+ type: "network",
+ details: {},
+ message: m,
+ });
+ throw new OperationFailedAndReportedError(m);
+ }
}
const reserveInfo = ReserveStatus.checked(resp.responseJson);
- if (!reserveInfo) {
- throw Error();
- }
- reserve.current_amount = Amounts.parseOrThrow(reserveInfo.balance);
+ await oneShotMutate(this.db, Stores.reserves, reserve.reservePub, r => {
+ if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
+ return;
+ }
+ reserve.currentAmount = Amounts.parseOrThrow(reserveInfo.balance);
+ reserve.reserveStatus = ReserveRecordStatus.WITHDRAWING;
+ return r;
+ });
await oneShotPut(this.db, Stores.reserves, reserve);
this.notifier.notify();
- return reserve;
}
async getPossibleDenoms(
@@ -1984,7 +1930,8 @@ export class Wallet {
versionMatch.currentCmp === -1
) {
console.warn(
- `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated (exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
+ `wallet version ${WALLET_PROTOCOL_VERSION} might be outdated ` +
+ `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
);
if (isFirefox()) {
console.log("skipping update check on Firefox");
@@ -2060,19 +2007,25 @@ export class Wallet {
wireInfo: undefined,
updateStatus: ExchangeUpdateStatus.FETCH_KEYS,
updateStarted: now,
+ updateReason: "initial",
+ timestampAdded: getTimestampNow(),
};
await oneShotPut(this.db, Stores.exchanges, newExchangeRecord);
} else {
- runWithWriteTransaction(this.db, [Stores.exchanges], async t => {
+ await runWithWriteTransaction(this.db, [Stores.exchanges], async t => {
const rec = await t.get(Stores.exchanges, baseUrl);
if (!rec) {
return;
}
- if (rec.updateStatus != ExchangeUpdateStatus.NONE && !force) {
+ if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && !force) {
return;
}
+ if (rec.updateStatus != ExchangeUpdateStatus.FETCH_KEYS && force) {
+ rec.updateReason = "forced";
+ }
rec.updateStarted = now;
rec.updateStatus = ExchangeUpdateStatus.FETCH_KEYS;
+ rec.lastError = undefined;
t.put(Stores.exchanges, rec);
});
}
@@ -2104,6 +2057,17 @@ export class Wallet {
await oneShotMutate(this.db, Stores.exchanges, baseUrl, mut);
}
+ private async setReserveError(
+ reservePub: string,
+ err: OperationError,
+ ): Promise<void> {
+ const mut = (reserve: ReserveRecord) => {
+ reserve.lastError = err;
+ return reserve;
+ };
+ await oneShotMutate(this.db, Stores.reserves, reservePub, mut);
+ }
+
/**
* Fetch the exchange's /keys and update our database accordingly.
*
@@ -2129,23 +2093,27 @@ export class Wallet {
try {
keysResp = await this.http.get(keysUrl.href());
} catch (e) {
+ const m = `Fetching keys failed: ${e.message}`;
await this.setExchangeError(baseUrl, {
type: "network",
- details: {},
- message: `Fetching keys failed: ${e.message}`,
+ details: {
+ requestUrl: e.config?.url,
+ },
+ message: m,
});
- throw e;
+ throw new OperationFailedAndReportedError(m);
}
let exchangeKeysJson: KeysJson;
try {
exchangeKeysJson = KeysJson.checked(keysResp.responseJson);
} catch (e) {
+ const m = `Parsing /keys response failed: ${e.message}`;
await this.setExchangeError(baseUrl, {
type: "protocol-violation",
details: {},
- message: `Parsing /keys response failed: ${e.message}`,
+ message: m,
});
- throw e;
+ throw new OperationFailedAndReportedError(m);
}
const lastUpdateTimestamp = extractTalerStamp(
@@ -2158,7 +2126,7 @@ export class Wallet {
details: {},
message: m,
});
- throw Error(m);
+ throw new OperationFailedAndReportedError(m);
}
if (exchangeKeysJson.denoms.length === 0) {
@@ -2168,7 +2136,7 @@ export class Wallet {
details: {},
message: m,
});
- throw Error(m);
+ throw new OperationFailedAndReportedError(m);
}
const protocolVersion = exchangeKeysJson.version;
@@ -2179,32 +2147,69 @@ export class Wallet {
details: {},
message: m,
});
- throw Error(m);
+ throw new OperationFailedAndReportedError(m);
}
const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
.currency;
- const mutExchangeRecord = (r: ExchangeRecord) => {
- if (r.updateStatus != ExchangeUpdateStatus.FETCH_KEYS) {
- console.log("not updating, wrong state (concurrent modification?)");
- return undefined;
- }
- r.details = {
- currency,
- protocolVersion,
- lastUpdateTime: lastUpdateTimestamp,
- masterPublicKey: exchangeKeysJson.master_public_key,
- auditors: exchangeKeysJson.auditors,
- };
- r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
- r.lastError = undefined;
- return r;
- };
+ const newDenominations = await Promise.all(
+ exchangeKeysJson.denoms.map(d =>
+ this.denominationRecordFromKeys(baseUrl, d),
+ ),
+ );
+
+ await runWithWriteTransaction(
+ this.db,
+ [Stores.exchanges, Stores.denominations],
+ async tx => {
+ const r = await tx.get(Stores.exchanges, baseUrl);
+ if (!r) {
+ console.warn(`exchange ${baseUrl} no longer present`);
+ return;
+ }
+ if (r.details) {
+ // FIXME: We need to do some consistency checks!
+ }
+ r.details = {
+ auditors: exchangeKeysJson.auditors,
+ currency: currency,
+ lastUpdateTime: lastUpdateTimestamp,
+ masterPublicKey: exchangeKeysJson.master_public_key,
+ protocolVersion: protocolVersion,
+ };
+ r.updateStatus = ExchangeUpdateStatus.FETCH_WIRE;
+ r.lastError = undefined;
+ await tx.put(Stores.exchanges, r);
+
+ for (const newDenom of newDenominations) {
+ const oldDenom = await tx.get(Stores.denominations, [
+ baseUrl,
+ newDenom.denomPub,
+ ]);
+ if (oldDenom) {
+ // FIXME: Do consistency check
+ } else {
+ await tx.put(Stores.denominations, newDenom);
+ }
+ }
+ },
+ );
}
+ /**
+ * Fetch wire information for an exchange and store it in the database.
+ *
+ * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
+ */
private async updateExchangeWithWireInfo(exchangeBaseUrl: string) {
- exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+ const exchange = await this.findExchange(exchangeBaseUrl);
+ if (!exchange) {
+ return;
+ }
+ if (exchange.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
+ return;
+ }
const reqUrl = new URI("wire")
.absoluteTo(exchangeBaseUrl)
.addQuery("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
@@ -2215,6 +2220,45 @@ export class Wallet {
throw Error("/wire response malformed");
}
const wireInfo = ExchangeWireJson.checked(wiJson);
+ const feesForType: { [wireMethod: string]: WireFee[] } = {};
+ for (const wireMethod of Object.keys(wireInfo.fees)) {
+ const feeList: WireFee[] = [];
+ for (const x of wireInfo.fees[wireMethod]) {
+ const startStamp = extractTalerStamp(x.start_date);
+ if (!startStamp) {
+ throw Error("wrong date format");
+ }
+ const endStamp = extractTalerStamp(x.end_date);
+ if (!endStamp) {
+ throw Error("wrong date format");
+ }
+ feeList.push({
+ closingFee: Amounts.parseOrThrow(x.closing_fee),
+ endStamp,
+ sig: x.sig,
+ startStamp,
+ wireFee: Amounts.parseOrThrow(x.wire_fee),
+ });
+ }
+ feesForType[wireMethod] = feeList;
+ }
+
+ await runWithWriteTransaction(this.db, [Stores.exchanges], async tx => {
+ const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
+ if (!r) {
+ return;
+ }
+ if (r.updateStatus != ExchangeUpdateStatus.FETCH_WIRE) {
+ return;
+ }
+ r.wireInfo = {
+ accounts: wireInfo.accounts,
+ feesForType: feesForType,
+ };
+ r.updateStatus = ExchangeUpdateStatus.FINISHED;
+ r.lastError = undefined;
+ await tx.put(Stores.exchanges, r);
+ });
}
/**
@@ -2312,17 +2356,17 @@ export class Wallet {
});
await tx.iter(Stores.reserves).forEach(r => {
- if (!r.timestamp_confirmed) {
+ if (!r.timestampConfirmed) {
return;
}
- let amount = Amounts.getZero(r.requested_amount.currency);
- amount = Amounts.add(amount, r.precoin_amount).amount;
- addTo(balanceStore, "pendingIncoming", amount, r.exchange_base_url);
+ let amount = Amounts.getZero(r.requestedAmount.currency);
+ amount = Amounts.add(amount, r.precoinAmount).amount;
+ addTo(balanceStore, "pendingIncoming", amount, r.exchangeBaseUrl);
addTo(
balanceStore,
"pendingIncomingWithdraw",
amount,
- r.exchange_base_url,
+ r.exchangeBaseUrl,
);
});
@@ -2333,8 +2377,8 @@ export class Wallet {
addTo(
balanceStore,
"paybackAmount",
- r.current_amount!,
- r.exchange_base_url,
+ r.currentAmount!,
+ r.exchangeBaseUrl,
);
return balanceStore;
});
@@ -2359,23 +2403,27 @@ export class Wallet {
return balanceStore;
}
- async createRefreshSession(
- oldCoinPub: string,
- ): Promise<RefreshSessionRecord | undefined> {
+ async refresh(oldCoinPub: string, force: boolean = false): Promise<void> {
const coin = await oneShotGet(this.db, Stores.coins, oldCoinPub);
-
if (!coin) {
- throw Error("coin not found");
+ console.warn("can't refresh, coin not in database");
+ return;
}
-
- if (coin.currentAmount.value === 0 && coin.currentAmount.fraction === 0) {
- return undefined;
+ switch (coin.status) {
+ case CoinStatus.Dirty:
+ break;
+ case CoinStatus.Dormant:
+ return;
+ case CoinStatus.Fresh:
+ if (!force) {
+ return;
+ }
+ break;
}
const exchange = await this.updateExchangeFromUrl(coin.exchangeBaseUrl);
-
if (!exchange) {
- throw Error("db inconsistent");
+ throw Error("db inconsistent: exchange of coin not found");
}
const oldDenom = await oneShotGet(this.db, Stores.denominations, [
@@ -2384,7 +2432,7 @@ export class Wallet {
]);
if (!oldDenom) {
- throw Error("db inconsistent");
+ throw Error("db inconsistent: denomination for coin not found");
}
const availableDenoms: DenominationRecord[] = await oneShotIterIndex(
@@ -2401,20 +2449,22 @@ export class Wallet {
availableDenoms,
);
- Wallet.enableTracing && console.log("refreshing coin", coin);
- Wallet.enableTracing && console.log("refreshing into", newCoinDenoms);
-
if (newCoinDenoms.length === 0) {
- Wallet.enableTracing &&
- console.log(
- `not refreshing, available amount ${amountToPretty(
- availableAmount,
- )} too small`,
- );
- coin.status = CoinStatus.Useless;
- await oneShotPut(this.db, Stores.coins, coin);
+ logger.trace(
+ `not refreshing, available amount ${amountToPretty(
+ availableAmount,
+ )} too small`,
+ );
+ await oneShotMutate(this.db, Stores.coins, oldCoinPub, x => {
+ if (x.status != coin.status) {
+ // Concurrent modification?
+ return;
+ }
+ x.status = CoinStatus.Dormant;
+ return x;
+ });
this.notifier.notify();
- return undefined;
+ return;
}
const refreshSession: RefreshSessionRecord = await this.cryptoApi.createRefreshSession(
@@ -2429,114 +2479,58 @@ export class Wallet {
const r = Amounts.sub(c.currentAmount, refreshSession.valueWithFee);
if (r.saturated) {
// Something else must have written the coin value
- throw AbortTransaction;
+ throw TransactionAbort;
}
c.currentAmount = r.amount;
- c.status = CoinStatus.Refreshed;
+ c.status = CoinStatus.Dormant;
return c;
}
- let key;
-
// Store refresh session and subtract refreshed amount from
// coin in the same transaction.
await runWithWriteTransaction(
this.db,
[Stores.refresh, Stores.coins],
async tx => {
- key = await tx.put(Stores.refresh, refreshSession);
+ await tx.put(Stores.refresh, refreshSession);
await tx.mutate(Stores.coins, coin.coinPub, mutateCoin);
},
);
+ logger.info(`created refresh session ${refreshSession.refreshSessionId}`);
this.notifier.notify();
- if (!key || typeof key !== "number") {
- throw Error("insert failed");
- }
-
- refreshSession.id = key;
-
- return refreshSession;
+ await this.processRefreshSession(refreshSession.refreshSessionId);
}
- async refresh(oldCoinPub: string): Promise<void> {
- const refreshImpl = async () => {
- const oldRefreshSessions = await oneShotIter(
- this.db,
- Stores.refresh,
- ).toArray();
- for (const session of oldRefreshSessions) {
- if (session.finished) {
- continue;
- }
- Wallet.enableTracing &&
- console.log(
- "waiting for unfinished old refresh session for",
- oldCoinPub,
- session,
- );
- await this.continueRefreshSession(session);
- }
- const coin = await oneShotGet(this.db, Stores.coins, oldCoinPub);
- if (!coin) {
- console.warn("can't refresh, coin not in database");
- return;
- }
- if (
- coin.status === CoinStatus.Useless ||
- coin.status === CoinStatus.Fresh
- ) {
- Wallet.enableTracing &&
- console.log(
- "not refreshing due to coin status",
- CoinStatus[coin.status],
- );
- return;
- }
- const refreshSession = await this.createRefreshSession(oldCoinPub);
- if (!refreshSession) {
- // refreshing not necessary
- Wallet.enableTracing && console.log("not refreshing", oldCoinPub);
- return;
- }
- return this.continueRefreshSession(refreshSession);
- };
-
- const activeRefreshOp = this.activeRefreshOperations[oldCoinPub];
-
- if (activeRefreshOp) {
- return activeRefreshOp;
- }
-
- try {
- const newOp = refreshImpl();
- this.activeRefreshOperations[oldCoinPub] = newOp;
- const res = await newOp;
- return res;
- } finally {
- delete this.activeRefreshOperations[oldCoinPub];
+ async processRefreshSession(refreshSessionId: string) {
+ const refreshSession = await oneShotGet(
+ this.db,
+ Stores.refresh,
+ refreshSessionId,
+ );
+ if (!refreshSession) {
+ return;
}
- }
-
- async continueRefreshSession(refreshSession: RefreshSessionRecord) {
if (refreshSession.finished) {
return;
}
if (typeof refreshSession.norevealIndex !== "number") {
- await this.refreshMelt(refreshSession);
- const r = await oneShotGet(this.db, Stores.refresh, refreshSession.id);
- if (!r) {
- throw Error("refresh session does not exist anymore");
- }
- refreshSession = r;
+ await this.refreshMelt(refreshSession.refreshSessionId);
}
-
- await this.refreshReveal(refreshSession);
+ await this.refreshReveal(refreshSession.refreshSessionId);
+ logger.trace("refresh finished");
}
- async refreshMelt(refreshSession: RefreshSessionRecord): Promise<void> {
+ async refreshMelt(refreshSessionId: string): Promise<void> {
+ const refreshSession = await oneShotGet(
+ this.db,
+ Stores.refresh,
+ refreshSessionId,
+ );
+ if (!refreshSession) {
+ return;
+ }
if (refreshSession.norevealIndex !== undefined) {
- console.error("won't melt again");
return;
}
@@ -2582,12 +2576,29 @@ export class Wallet {
refreshSession.norevealIndex = norevealIndex;
- await oneShotPut(this.db, Stores.refresh, refreshSession);
+ await oneShotMutate(this.db, Stores.refresh, refreshSessionId, rs => {
+ if (rs.norevealIndex !== undefined) {
+ return;
+ }
+ if (rs.finished) {
+ return;
+ }
+ rs.norevealIndex = norevealIndex;
+ return rs;
+ });
this.notifier.notify();
}
- async refreshReveal(refreshSession: RefreshSessionRecord): Promise<void> {
+ private async refreshReveal(refreshSessionId: string): Promise<void> {
+ const refreshSession = await oneShotGet(
+ this.db,
+ Stores.refresh,
+ refreshSessionId,
+ );
+ if (!refreshSession) {
+ return;
+ }
const norevealIndex = refreshSession.norevealIndex;
if (norevealIndex === undefined) {
throw Error("can't reveal without melting first");
@@ -2706,6 +2717,13 @@ export class Wallet {
this.db,
[Stores.coins, Stores.refresh],
async tx => {
+ const rs = await tx.get(Stores.refresh, refreshSessionId);
+ if (!rs) {
+ return;
+ }
+ if (rs.finished) {
+ return;
+ }
for (let coin of coins) {
await tx.put(Stores.coins, coin);
}
@@ -2726,8 +2744,8 @@ export class Wallet {
*/
async getHistory(
historyQuery?: HistoryQuery,
- ): Promise<{ history: HistoryRecord[] }> {
- const history: HistoryRecord[] = [];
+ ): Promise<{ history: HistoryEvent[] }> {
+ const history: HistoryEvent[] = [];
// FIXME: do pagination instead of generating the full history
@@ -2744,6 +2762,7 @@ export class Wallet {
},
timestamp: p.timestamp,
type: "claim-order",
+ explicit: false,
});
}
@@ -2758,6 +2777,7 @@ export class Wallet {
},
timestamp: w.startTimestamp,
type: "withdraw",
+ explicit: false,
});
}
@@ -2772,6 +2792,7 @@ export class Wallet {
},
timestamp: p.timestamp,
type: "pay",
+ explicit: false,
});
if (p.timestamp_refund) {
const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
@@ -2796,6 +2817,7 @@ export class Wallet {
},
timestamp: p.timestamp_refund,
type: "refund",
+ explicit: false,
});
}
}
@@ -2803,24 +2825,31 @@ export class Wallet {
const reserves = await oneShotIter(this.db, Stores.reserves).toArray();
for (const r of reserves) {
+ const reserveType = r.bankWithdrawStatusUrl ? "taler-bank" : "manual";
history.push({
detail: {
- exchangeBaseUrl: r.exchange_base_url,
- requestedAmount: Amounts.toString(r.requested_amount),
- reservePub: r.reserve_pub,
+ exchangeBaseUrl: r.exchangeBaseUrl,
+ requestedAmount: Amounts.toString(r.requestedAmount),
+ reservePub: r.reservePub,
+ reserveType,
+ bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
},
timestamp: r.created,
- type: "create-reserve",
+ type: "reserve-created",
+ explicit: false,
});
- if (r.timestamp_depleted) {
+ if (r.timestampConfirmed) {
history.push({
detail: {
- exchangeBaseUrl: r.exchange_base_url,
- requestedAmount: r.requested_amount,
- reservePub: r.reserve_pub,
+ exchangeBaseUrl: r.exchangeBaseUrl,
+ requestedAmount: Amounts.toString(r.requestedAmount),
+ reservePub: r.reservePub,
+ reserveType,
+ bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
},
- timestamp: r.timestamp_depleted,
- type: "depleted-reserve",
+ timestamp: r.created,
+ type: "reserve-confirmed",
+ explicit: false,
});
}
}
@@ -2835,11 +2864,23 @@ export class Wallet {
tipId: tip.tipId,
},
timestamp: tip.timestamp,
+ explicit: false,
type: "tip",
});
}
- history.sort((h1, h2) => Math.sign(h1.timestamp - h2.timestamp));
+ await oneShotIter(this.db, Stores.exchanges).forEach(exchange => {
+ history.push({
+ type: "exchange-added",
+ explicit: false,
+ timestamp: exchange.timestampAdded,
+ detail: {
+ exchangeBaseUrl: exchange.baseUrl,
+ },
+ });
+ });
+
+ history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));
return { history };
}
@@ -2849,7 +2890,17 @@ export class Wallet {
const exchanges = await this.getExchanges();
for (let e of exchanges) {
switch (e.updateStatus) {
- case ExchangeUpdateStatus.NONE:
+ case ExchangeUpdateStatus.FINISHED:
+ if (e.lastError) {
+ pendingOperations.push({
+ type: "bug",
+ message:
+ "Exchange record is in FINISHED state but has lastError set",
+ details: {
+ exchangeBaseUrl: e.baseUrl,
+ },
+ });
+ }
if (!e.details) {
pendingOperations.push({
type: "bug",
@@ -2860,12 +2911,24 @@ export class Wallet {
},
});
}
+ 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:
@@ -2873,10 +2936,79 @@ export class Wallet {
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 oneShotIter(this.db, Stores.reserves).forEach(reserve => {
+ const reserveType = reserve.bankWithdrawStatusUrl
+ ? "taler-bank"
+ : "manual";
+ switch (reserve.reserveStatus) {
+ case ReserveRecordStatus.DORMANT:
+ // nothing to report as pending
+ break;
+ case ReserveRecordStatus.WITHDRAWING:
+ case ReserveRecordStatus.UNCONFIRMED:
+ case ReserveRecordStatus.QUERYING_STATUS:
+ pendingOperations.push({
+ type: "reserve",
+ stage: reserve.reserveStatus,
+ timestampCreated: reserve.created,
+ reserveType,
+ });
+ break;
+ default:
+ pendingOperations.push({
+ type: "bug",
+ message: "Unknown reserve record status",
+ details: {
+ reservePub: reserve.reservePub,
+ reserveStatus: reserve.reserveStatus,
+ },
+ });
+ break;
+ }
+ });
+
+ await oneShotIter(this.db, 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,
+ });
+ });
+
+ await oneShotIter(this.db, Stores.precoins).forEach(pc => {
+ pendingOperations.push({
+ type: "withdraw",
+ stage: "planchet",
+ reservePub: pc.reservePub,
+ });
+ });
return {
pendingOperations,
};
@@ -2914,16 +3046,20 @@ export class Wallet {
async getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
return await oneShotIter(this.db, Stores.reserves).filter(
- r => r.exchange_base_url === exchangeBaseUrl,
+ r => r.exchangeBaseUrl === exchangeBaseUrl,
);
}
- async getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
+ async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
return await oneShotIter(this.db, Stores.coins).filter(
c => c.exchangeBaseUrl === exchangeBaseUrl,
);
}
+ async getCoins(): Promise<CoinRecord[]> {
+ return await oneShotIter(this.db, Stores.coins).toArray();
+ }
+
async getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> {
return await oneShotIter(this.db, Stores.precoins).filter(
c => c.exchangeBaseUrl === exchangeBaseUrl,
@@ -2948,15 +3084,10 @@ export class Wallet {
throw Error(`Reserve of coin ${coinPub} not found`);
}
switch (coin.status) {
- case CoinStatus.Refreshed:
- throw Error(
- `Can't do payback for coin ${coinPub} since it's refreshed`,
- );
- case CoinStatus.PaybackDone:
- console.log(`Coin ${coinPub} already payed back`);
- return;
+ case CoinStatus.Dormant:
+ throw Error(`Can't do payback for coin ${coinPub} since it's dormant`);
}
- coin.status = CoinStatus.PaybackPending;
+ coin.status = CoinStatus.Dormant;
// Even if we didn't get the payback yet, we suspend withdrawal, since
// technically we might update reserve status before we get the response
// from the reserve for the payback request.
@@ -2985,7 +3116,7 @@ export class Wallet {
if (!coin) {
throw Error(`Coin ${coinPub} not found, can't confirm payback`);
}
- coin.status = CoinStatus.PaybackDone;
+ coin.status = CoinStatus.Dormant;
await oneShotPut(this.db, Stores.coins, coin);
this.notifier.notify();
await this.updateReserve(reservePub!);
@@ -3023,7 +3154,9 @@ export class Wallet {
}
reserve.hasPayback = false;
await oneShotPut(this.db, Stores.reserves, reserve);
- this.depleteReserve(reserve);
+ this.depleteReserve(reserve.reservePub).catch(e => {
+ console.error("Error depleting reserve after payback", e);
+ });
}
async getPaybackReserves(): Promise<ReserveRecord[]> {
@@ -3036,7 +3169,7 @@ export class Wallet {
* Stop ongoing processing.
*/
stop() {
- this.timerGroup.stopCurrentAndFutureTimers();
+ //this.timerGroup.stopCurrentAndFutureTimers();
this.cryptoApi.stop();
}
@@ -3249,7 +3382,7 @@ export class Wallet {
return;
}
- t.timestamp_refund = new Date().getTime();
+ t.timestamp_refund = getTimestampNow();
for (const perm of refundPermissions) {
if (
@@ -3444,25 +3577,10 @@ export class Wallet {
return feeAcc;
}
- async acceptTip(talerTipUri: string): Promise<void> {
- const { tipId, merchantOrigin } = await this.getTipStatus(talerTipUri);
- const key = `${tipId}${merchantOrigin}`;
- if (this.activeTipOperations[key]) {
- return this.activeTipOperations[key];
- }
- const p = this.acceptTipImpl(tipId, merchantOrigin);
- this.activeTipOperations[key] = p;
- try {
- return await p;
- } finally {
- delete this.activeTipOperations[key];
- }
- }
-
- private async acceptTipImpl(
- tipId: string,
- merchantOrigin: string,
+async acceptTip(
+ talerTipUri: string,
): Promise<void> {
+ const { tipId, merchantOrigin } = await this.getTipStatus(talerTipUri);
let tipRecord = await oneShotGet(this.db, Stores.tips, [
tipId,
merchantOrigin,
@@ -3603,7 +3721,7 @@ export class Wallet {
pickedUp: false,
planchets: undefined,
response: undefined,
- timestamp: new Date().getTime(),
+ timestamp: getTimestampNow(),
tipId: res.tipId,
pickupUrl: res.tipPickupUrl,
totalFees: Amounts.add(
@@ -3732,7 +3850,6 @@ export class Wallet {
senderWire: withdrawInfo.senderWire,
exchangeWire: exchangeWire,
});
- await this.sendReserveInfoToBank(reserve.reservePub);
return {
reservePub: reserve.reservePub,
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
@@ -3767,7 +3884,7 @@ export class Wallet {
const totalFees = totalRefundFees;
return {
contractTerms: purchase.contractTerms,
- hasRefund: purchase.timestamp_refund !== 0,
+ hasRefund: purchase.timestamp_refund !== undefined,
totalRefundAmount: totalRefundAmount,
totalRefundAndRefreshFees: totalFees,
};
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index b227ca816..a11da029f 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -233,7 +233,7 @@ export interface ConfirmPayResult {
/**
* Activity history record.
*/
-export interface HistoryRecord {
+export interface HistoryEvent {
/**
* Type of the history event.
*/
@@ -242,7 +242,7 @@ export interface HistoryRecord {
/**
* Time when the activity was recorded.
*/
- timestamp: number;
+ timestamp: Timestamp;
/**
* Subject of the entry. Used to group multiple history records together.
@@ -254,6 +254,13 @@ export interface HistoryRecord {
* Details used when rendering the history record.
*/
detail: any;
+
+ /**
+ * Set to 'true' if the event has been explicitly created,
+ * and set to 'false' if the event has been derived from the
+ * state of the database.
+ */
+ explicit: boolean;
}
/**
@@ -516,6 +523,8 @@ export interface WalletDiagnostics {
export interface PendingWithdrawOperation {
type: "withdraw";
+ stage: string;
+ reservePub: string;
}
export interface PendingRefreshOperation {
@@ -535,6 +544,7 @@ export interface OperationError {
export interface PendingExchangeUpdateOperation {
type: "exchange-update";
stage: string;
+ reason: string;
exchangeBaseUrl: string;
lastError?: OperationError;
}
@@ -545,10 +555,28 @@ export interface PendingBugOperation {
details: any;
}
+export interface PendingReserveOperation {
+ type: "reserve";
+ lastError?: OperationError;
+ stage: string;
+ timestampCreated: Timestamp;
+ reserveType: string;
+}
+
+export interface PendingRefreshOperation {
+ type: "refresh";
+ lastError?: OperationError;
+ oldCoinPub: string;
+ refreshStatus: string;
+ refreshOutputSize: number;
+}
+
export type PendingOperationInfo =
| PendingWithdrawOperation
+ | PendingReserveOperation
| PendingBugOperation
- | PendingExchangeUpdateOperation;
+ | PendingExchangeUpdateOperation
+ | PendingRefreshOperation;
export interface PendingOperationsResponse {
pendingOperations: PendingOperationInfo[];
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 3f6e5cc4a..034bf2849 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -79,7 +79,7 @@ export interface MessageMap {
};
"get-history": {
request: {};
- response: walletTypes.HistoryRecord[];
+ response: walletTypes.HistoryEvent[];
};
"get-coins": {
request: { exchangeBaseUrl: string };
diff --git a/src/webex/pages/payback.tsx b/src/webex/pages/payback.tsx
index 934c28c0a..af14b95d4 100644
--- a/src/webex/pages/payback.tsx
+++ b/src/webex/pages/payback.tsx
@@ -57,11 +57,11 @@ function Payback() {
<div>
{reserves.map(r => (
<div>
- <h2>Reserve for ${renderAmount(r.current_amount!)}</h2>
+ <h2>Reserve for ${renderAmount(r.currentAmount!)}</h2>
<ul>
- <li>Exchange: ${r.exchange_base_url}</li>
+ <li>Exchange: ${r.exchangeBaseUrl}</li>
</ul>
- <button onClick={() => withdrawPaybackReserve(r.reserve_pub)}>
+ <button onClick={() => withdrawPaybackReserve(r.reservePub)}>
Withdraw again
</button>
</div>
diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx
index 205945471..78b7374b3 100644
--- a/src/webex/pages/popup.tsx
+++ b/src/webex/pages/popup.tsx
@@ -30,7 +30,7 @@ import { AmountJson } from "../../amounts";
import * as Amounts from "../../amounts";
import {
- HistoryRecord,
+ HistoryEvent,
WalletBalance,
WalletBalanceEntry,
} from "../../walletTypes";
@@ -327,7 +327,7 @@ class WalletBalanceView extends React.Component<any, any> {
}
}
-function formatHistoryItem(historyItem: HistoryRecord) {
+function formatHistoryItem(historyItem: HistoryEvent) {
const d = historyItem.detail;
console.log("hist item", historyItem);
switch (historyItem.type) {
@@ -459,7 +459,7 @@ class WalletHistory extends React.Component<any, any> {
render(): JSX.Element {
console.log("rendering history");
- const history: HistoryRecord[] = this.myHistory;
+ const history: HistoryEvent[] = this.myHistory;
if (this.gotError) {
return i18n.str`Error: could not retrieve event history`;
}
@@ -474,7 +474,7 @@ class WalletHistory extends React.Component<any, any> {
const item = (
<div className="historyItem">
<div className="historyDate">
- {new Date(record.timestamp).toString()}
+ {new Date(record.timestamp.t_ms).toString()}
</div>
{formatHistoryItem(record)}
</div>
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index f2cccfba6..c2fdb1f14 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -215,7 +215,7 @@ function FeeDetailsView(props: {
<tbody>
{rci!.wireFees.feesForType[s].map(f => (
<tr>
- <td>{moment.unix(f.endStamp).format("llll")}</td>
+ <td>{moment.unix(Math.floor(f.endStamp.t_ms / 1000)).format("llll")}</td>
<td>{renderAmount(f.wireFee)}</td>
<td>{renderAmount(f.closingFee)}</td>
</tr>
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index f4decbc60..57c10d94a 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -176,7 +176,7 @@ async function handleMessage(
if (typeof detail.exchangeBaseUrl !== "string") {
return Promise.reject(Error("exchangBaseUrl missing"));
}
- return needsWallet().getCoins(detail.exchangeBaseUrl);
+ return needsWallet().getCoinsForExchange(detail.exchangeBaseUrl);
}
case "get-precoins": {
if (typeof detail.exchangeBaseUrl !== "string") {