WIP: simplification and error handling

This commit is contained in:
Florian Dold 2019-11-21 23:09:43 +01:00
parent e8f362ccfe
commit c623309430
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
20 changed files with 1142 additions and 943 deletions

View File

@ -63,7 +63,7 @@
"@types/chrome": "^0.0.91",
"@types/urijs": "^1.19.3",
"axios": "^0.19.0",
"idb-bridge": "^0.0.11",
"idb-bridge": "^0.0.14",
"qrcode-generator": "^1.4.3",
"source-map-support": "^0.5.12",
"urijs": "^1.18.10"

View File

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

View File

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

View File

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

View File

@ -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 {
@ -374,13 +402,18 @@ 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();

View File

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

View File

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

View File

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

View File

@ -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,6 +131,161 @@ async function withWallet<T>(
}
walletCli
.subcommand("", "balance", { help: "Show wallet balance." })
.action(async args => {
console.log("balance command called");
await withWallet(args, async wallet => {
const balance = await wallet.getBalances();
console.log(JSON.stringify(balance, undefined, 2));
});
});
walletCli
.subcommand("", "history", { help: "Show wallet event history." })
.maybeOption("from", ["--from"], clk.STRING)
.maybeOption("to", ["--to"], clk.STRING)
.maybeOption("limit", ["--limit"], clk.STRING)
.maybeOption("contEvt", ["--continue-with"], clk.STRING)
.action(async args => {
await withWallet(args, async wallet => {
const history = await wallet.getHistory();
console.log(JSON.stringify(history, undefined, 2));
});
});
walletCli
.subcommand("", "pending", { help: "Show pending operations." })
.action(async args => {
await withWallet(args, async wallet => {
const pending = await wallet.getPendingOperations();
console.log(JSON.stringify(pending, undefined, 2));
});
});
async function asyncSleep(milliSeconds: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
setTimeout(() => resolve(), milliSeconds);
});
}
walletCli
.subcommand("runPendingOpt", "run-pending", {
help: "Run pending operations.",
})
.action(async args => {
await withWallet(args, async wallet => {
await wallet.runPending();
});
});
walletCli
.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, {
@ -146,63 +312,60 @@ walletCli
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) => {
const balance = await wallet.getBalances();
console.log(JSON.stringify(balance, undefined, 2));
await withWallet(args, async (wallet) => {
await doPay(wallet, talerPayUri, { alwaysYes: true });
});
});
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)
.action(async args => {
withWallet(args, async (wallet) => {
const history = await wallet.getHistory();
console.log(JSON.stringify(history, undefined, 2));
});
});
walletCli
.subcommand("", "pending", { help: "Show pending operations." })
.action(async args => {
withWallet(args, async (wallet) => {
const pending = await wallet.getPendingOperations();
console.log(JSON.stringify(pending, undefined, 2));
});
});
async function asyncSleep(milliSeconds: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
setTimeout(() => resolve(), milliSeconds);
});
}
walletCli
.subcommand("runPendingOpt", "run-pending", {
help: "Run pending operations."
testCli
.subcommand("integrationtestCmd", "integrationtest", {
help: "Run integration test with bank, exchange and merchant.",
})
.action(async (args) => {
withWallet(args, async (wallet) => {
await wallet.processPending();
});
.requiredOption("exchange", ["-e", "--exchange"], clk.STRING, {
default: "https://exchange.test.taler.net/",
})
.requiredOption("merchant", ["-m", "--merchant"], clk.STRING, {
default: "https://backend.test.taler.net/",
})
.requiredOption("merchantApiKey", ["-k", "--merchant-api-key"], clk.STRING, {
default: "sandbox",
})
.requiredOption("bank", ["-b", "--bank"], clk.STRING, {
default: "https://bank.test.taler.net/",
})
.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;
try {
await runIntegrationTest({
amountToSpend: cmdObj.spendAmount,
amountToWithdraw: cmdObj.withdrawAmount,
bankBaseUrl: cmdObj.bank,
exchangeBaseUrl: cmdObj.exchange,
merchantApiKey: cmdObj.merchantApiKey,
merchantBaseUrl: cmdObj.merchant,
}).catch(err => {
console.error("Failed with exception:");
console.error(err);
});
process.exit(0);
} catch (e) {
console.error(e);
process.exit(1);
}
});
walletCli
testCli
.subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:1",
@ -249,174 +412,14 @@ walletCli
}
});
walletCli
.subcommand("integrationtestCmd", "integrationtest", {
help: "Run integration test with bank, exchange and merchant.",
})
.requiredOption("exchange", ["-e", "--exchange"], clk.STRING, {
default: "https://exchange.test.taler.net/",
})
.requiredOption("merchant", ["-m", "--merchant"], clk.STRING, {
default: "https://backend.test.taler.net/",
})
.requiredOption("merchantApiKey", ["-k", "--merchant-api-key"], clk.STRING, {
default: "sandbox",
})
.requiredOption("bank", ["-b", "--bank"], clk.STRING, {
default: "https://bank.test.taler.net/",
})
.requiredOption("withdrawAmount", ["-b", "--bank"], clk.STRING, {
default: "TESTKUDOS:10",
})
.requiredOption("spendAmount", ["-s", "--spend-amount"], clk.STRING, {
default: "TESTKUDOS:4",
})
.action(async args => {
applyVerbose(args.wallet.verbose);
let cmdObj = args.integrationtestCmd;
try {
await runIntegrationTest({
amountToSpend: cmdObj.spendAmount,
amountToWithdraw: cmdObj.withdrawAmount,
bankBaseUrl: cmdObj.bank,
exchangeBaseUrl: cmdObj.exchange,
merchantApiKey: cmdObj.merchantApiKey,
merchantBaseUrl: cmdObj.merchant,
}).catch(err => {
console.error("Failed with exception:");
console.error(err);
});
process.exit(0);
} catch (e) {
console.error(e);
process.exit(1);
}
});
walletCli
.subcommand("withdrawUriCmd", "withdraw-uri")
.requiredArgument("withdrawUri", clk.STRING)
.action(async args => {
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!");
process.exit(1);
return;
}
const { reservePub, confirmTransferUrl } = await wallet.acceptWithdrawal(
withdrawUrl,
selectedExchange,
);
if (confirmTransferUrl) {
console.log("please confirm the transfer at", confirmTransferUrl);
}
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();

View File

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

View File

@ -1,5 +1,3 @@
import { openPromise } from "./promiseUtils";
/*
This file is part of TALER
(C) 2016 GNUnet e.V.
@ -22,6 +20,12 @@ import { openPromise } from "./promiseUtils";
* @author Florian Dold
*/
/**
* Imports.
*/
import { openPromise } from "./promiseUtils";
/**
* Result of an inner join.
*/
@ -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");

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -79,7 +79,7 @@ export interface MessageMap {
};
"get-history": {
request: {};
response: walletTypes.HistoryRecord[];
response: walletTypes.HistoryEvent[];
};
"get-coins": {
request: { exchangeBaseUrl: string };

View File

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

View File

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

View File

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

View File

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

View File

@ -33,6 +33,8 @@
"src/crypto/cryptoWorker.ts",
"src/crypto/emscInterface-test.ts",
"src/crypto/emscInterface.ts",
"src/crypto/nativeCrypto-test.ts",
"src/crypto/nativeCrypto.ts",
"src/crypto/nodeEmscriptenLoader.ts",
"src/crypto/nodeProcessWorker.ts",
"src/crypto/nodeWorkerEntry.ts",
@ -53,6 +55,7 @@
"src/index.ts",
"src/libtoolVersion-test.ts",
"src/libtoolVersion.ts",
"src/logging.ts",
"src/promiseUtils.ts",
"src/query.ts",
"src/talerTypes.ts",

View File

@ -3412,10 +3412,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
dependencies:
safer-buffer ">= 2.1.2 < 3"
idb-bridge@^0.0.11:
version "0.0.11"
resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.11.tgz#ba2fbd24b7e6f7f4de8333ed12b0912e64dda308"
integrity sha512-fLlHce/WwT6eD3sc54gsfvM5fZqrhAPwBNH4uU/y6D0C1+0higH7OgC5/wploMhkmNYkQID3BMNZvSUBr0leSQ==
idb-bridge@^0.0.14:
version "0.0.14"
resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.14.tgz#5fd50cd68b574df0eb6b1a960cef0cb984a21ded"
integrity sha512-jc9ZYGhhIrW6nh/pWyycGWzCmsLTFQ0iMY61lN+y9YcIOCxREpAkZxdfmhwNL7H0RvsYp7iJv0GH7ujs7HPC+g==
ieee754@^1.1.4:
version "1.1.13"