aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-08-20 17:58:01 +0200
committerFlorian Dold <florian.dold@gmail.com>2019-08-20 17:58:01 +0200
commit106bc6ad9a78c199e6b0e89bef854174d9014c28 (patch)
treedbb1579c29fca6cd133f2c2bd67ff36150a5cd7b
parent0f2fbf20ed3a74c3a73619f59ae8fe17bd2a883f (diff)
implement pay-url and disable broken wallet GC
-rw-r--r--src/headless/taler-wallet-cli.ts104
-rw-r--r--src/wallet.ts131
-rw-r--r--src/walletTypes.ts7
3 files changed, 162 insertions, 80 deletions
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 0a7529e4b..4a1f5d91e 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -21,13 +21,27 @@ import { MerchantBackendConnection } from "./merchant";
import { runIntegrationTest } from "./integrationtest";
import { Wallet } from "../wallet";
import querystring = require("querystring");
-import qrcodeGenerator = require("qrcode-generator")
+import qrcodeGenerator = require("qrcode-generator");
+import readline = require("readline");
const program = new commander.Command();
program.version("0.0.1").option("--verbose", "enable verbose output", false);
const walletDbPath = os.homedir + "/" + ".talerwalletdb.json";
+function prompt(question: string): Promise<string> {
+ const stdinReadline = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+ return new Promise<string>((resolve, reject) => {
+ stdinReadline.question(question, res => {
+ resolve(res);
+ stdinReadline.close();
+ });
+ });
+}
+
function applyVerbose(verbose: boolean) {
if (verbose) {
console.log("enabled verbose logging");
@@ -64,8 +78,8 @@ program
});
async function asyncSleep(milliSeconds: number): Promise<void> {
- return new Promise<void>((resolve, reject) =>{
- setTimeout(() => resolve(), milliSeconds)
+ return new Promise<void>((resolve, reject) => {
+ setTimeout(() => resolve(), milliSeconds);
});
}
@@ -76,8 +90,16 @@ program
.action(async cmdObj => {
applyVerbose(program.verbose);
console.log("creating order");
- const merchantBackend = new MerchantBackendConnection("https://backend.test.taler.net", "default", "sandbox");
- const orderResp = await merchantBackend.createOrder(cmdObj.amount, cmdObj.summary, "");
+ const merchantBackend = new MerchantBackendConnection(
+ "https://backend.test.taler.net",
+ "default",
+ "sandbox",
+ );
+ const orderResp = await merchantBackend.createOrder(
+ cmdObj.amount,
+ cmdObj.summary,
+ "",
+ );
console.log("created new order with order ID", orderResp.orderId);
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
const qrcode = qrcodeGenerator(0, "M");
@@ -87,21 +109,85 @@ program
process.exit(1);
return;
}
- qrcode.addData("talerpay:" + querystring.escape(contractUrl));
+ const url = "talerpay:" + querystring.escape(contractUrl);
+ console.log("contract url:", url);
+ qrcode.addData(url);
qrcode.make();
console.log(qrcode.createASCII());
- console.log("waiting for payment ...")
+ console.log("waiting for payment ...");
while (1) {
await asyncSleep(500);
- const checkPayResp2 = await merchantBackend.checkPayment(orderResp.orderId);
+ const checkPayResp2 = await merchantBackend.checkPayment(
+ orderResp.orderId,
+ );
if (checkPayResp2.paid) {
- console.log("payment successfully received!")
+ console.log("payment successfully received!");
break;
}
}
});
program
+ .command("pay-url <pay-url>")
+ .option("-y, --yes", "automatically answer yes to prompts")
+ .action(async (payUrl, cmdObj) => {
+ applyVerbose(program.verbose);
+ console.log("paying for", payUrl);
+ const wallet = await getDefaultNodeWallet({
+ persistentStoragePath: walletDbPath,
+ });
+ const result = await wallet.preparePay(payUrl);
+ if (result.status === "error") {
+ console.error("Could not pay:", result.error);
+ process.exit(1);
+ return;
+ }
+ if (result.status === "insufficient-balance") {
+ console.log("contract", result.contractTerms!);
+ console.error("insufficient balance");
+ process.exit(1);
+ return;
+ }
+ if (result.status === "paid") {
+ console.log("already paid!");
+ process.exit(0);
+ return;
+ }
+ if (result.status === "payment-possible") {
+ console.log("paying ...");
+ } else {
+ throw Error("not reached");
+ }
+ console.log("contract", result.contractTerms!);
+ let pay;
+ if (cmdObj.yes) {
+ pay = true;
+ } else {
+ while (true) {
+ const yesNoResp = (await prompt("Pay? [Y/n]")).toLowerCase();
+ if (yesNoResp === "" || yesNoResp === "y" || yesNoResp === "yes") {
+ pay = true;
+ break;
+ } else if (yesNoResp === "n" || yesNoResp === "no") {
+ pay = false;
+ break;
+ } else {
+ console.log("please answer y/n");
+ }
+ }
+ }
+
+ if (pay) {
+ const payRes = await wallet.confirmPay(result.proposalId!, undefined);
+ console.log("paid!");
+ } else {
+ console.log("not paying");
+ }
+
+ wallet.stop();
+ });
+
+program
.command("integrationtest")
.option(
"-e, --exchange <exchange-url>",
diff --git a/src/wallet.ts b/src/wallet.ts
index f7df6658f..eda7bfeaf 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -104,6 +104,7 @@ import {
TipStatus,
WalletBalance,
WalletBalanceEntry,
+ PreparePayResult,
} from "./walletTypes";
import { openPromise } from "./promiseUtils";
@@ -382,7 +383,6 @@ export class Wallet {
private async fillDefaults() {
const onTrue = (r: QueryRoot) => {
- console.log("defaults already applied");
};
const onFalse = (r: QueryRoot) => {
Wallet.enableTracing && console.log("applying defaults");
@@ -593,6 +593,8 @@ export class Wallet {
.iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl)
.toArray();
+ console.log("considering coins", coins);
+
const denoms = await this.q()
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
.toArray();
@@ -718,6 +720,53 @@ export class Wallet {
return t;
}
+ async preparePay(url: string): Promise<PreparePayResult> {
+ const talerpayPrefix = "talerpay:"
+ if (url.startsWith(talerpayPrefix)) {
+ url = decodeURIComponent(url.substring(talerpayPrefix.length));
+ }
+ let proposalId: number;
+ let checkResult: CheckPayResult;
+ try {
+ console.log("downloading proposal");
+ proposalId = await this.downloadProposal(url);
+ console.log("calling checkPay");
+ checkResult = await this.checkPay(proposalId);
+ console.log("checkPay result", checkResult);
+ } catch (e) {
+ return {
+ status: "error",
+ error: e.toString(),
+ }
+ }
+ const proposal = await this.getProposal(proposalId);
+ if (!proposal) {
+ throw Error("could not get proposal");
+ }
+ if (checkResult.status === "paid") {
+ return {
+ status: "paid",
+ contractTerms: proposal.contractTerms,
+ proposalId: proposal.id!,
+ };
+ }
+ if (checkResult.status === "insufficient-balance") {
+ return {
+ status: "insufficient-balance",
+ contractTerms: proposal.contractTerms,
+ proposalId: proposal.id!,
+ };
+ }
+ if (checkResult.status === "payment-possible") {
+ return {
+ status: "payment-possible",
+ contractTerms: proposal.contractTerms,
+ proposalId: proposal.id!,
+ };
+ }
+ throw Error("not reached");
+ }
+
/**
* Download a proposal and store it in the database.
* Returns an id for it to retrieve it later.
@@ -1001,6 +1050,8 @@ export class Wallet {
const paymentAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
+ Wallet.enableTracing && console.log(`checking if payment of ${JSON.stringify(paymentAmount)} is possible`);
+
let wireFeeLimit;
if (proposal.contractTerms.max_wire_fee) {
wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
@@ -1146,7 +1197,10 @@ export class Wallet {
this.processPreCoinThrottle[preCoin.exchangeBaseUrl]
) {
const timeout = Math.min(retryDelayMs * 2, 5 * 60 * 1000);
- Wallet.enableTracing && console.log(`throttling processPreCoin of ${preCoinPub} for ${timeout}ms`);
+ Wallet.enableTracing &&
+ console.log(
+ `throttling processPreCoin of ${preCoinPub} for ${timeout}ms`,
+ );
this.timerGroup.after(retryDelayMs, () => processPreCoinInternal());
return op.promise;
}
@@ -3370,76 +3424,11 @@ export class Wallet {
* based on the current system time.
*/
async collectGarbage() {
- const nowMilli = new Date().getTime();
- const nowSec = Math.floor(nowMilli / 1000);
-
- const gcReserve = (r: ReserveRecord, n: number) => {
- // This rule to purge reserves is a bit over-eager, since we still might
- // receive an emergency payback from the exchange. In this case we need
- // to wait for the exchange to wire the money back or change this rule to
- // wait until all coins from the reserve were spent.
- if (r.timestamp_depleted) {
- return true;
- }
- return false;
- };
- await this.q()
- .deleteIf(Stores.reserves, gcReserve)
- .finish();
-
- const gcProposal = (d: ProposalDownloadRecord, n: number) => {
- // Delete proposal after 60 minutes or 5 minutes before pay deadline,
- // whatever comes first.
- const deadlinePayMilli =
- getTalerStampSec(d.contractTerms.pay_deadline)! * 1000;
- const deadlineExpireMilli = nowMilli + 1000 * 60 * 60;
- return d.timestamp < Math.min(deadlinePayMilli, deadlineExpireMilli);
- };
- await this.q()
- .deleteIf(Stores.proposals, gcProposal)
- .finish();
-
- const activeExchanges: string[] = [];
- const gcExchange = (d: ExchangeRecord, n: number) => {
- // Delete if if unused and last update more than 20 minutes ago
- if (!d.lastUsedTime && nowMilli > d.lastUpdateTime + 1000 * 60 * 20) {
- return true;
- }
- activeExchanges.push(d.baseUrl);
- return false;
- };
-
- await this.q()
- .deleteIf(Stores.exchanges, gcExchange)
- .finish();
-
- // FIXME: check if this is correct!
- const gcDenominations = (d: DenominationRecord, n: number) => {
- if (nowSec > getTalerStampSec(d.stampExpireDeposit)!) {
- console.log("garbage-collecting denomination due to expiration");
- return true;
- }
- if (activeExchanges.indexOf(d.exchangeBaseUrl) < 0) {
- console.log("garbage-collecting denomination due to missing exchange");
- return true;
- }
- return false;
- };
- await this.q()
- .deleteIf(Stores.denominations, gcDenominations)
- .finish();
-
- const gcWireFees = (r: ExchangeWireFeesRecord, n: number) => {
- if (activeExchanges.indexOf(r.exchangeBaseUrl) < 0) {
- return true;
- }
- return false;
- };
- await this.q()
- .deleteIf(Stores.exchangeWireFees, gcWireFees)
- .finish();
+ // FIXME(#5845)
- // FIXME(#5210) also GC coins
+ // We currently do not garbage-collect the wallet database. This might change
+ // after the feature has been properly re-designed, and we have come up with a
+ // strategy to test it.
}
clearNotification(): void {
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index f9d753f28..e6564b91b 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -472,3 +472,10 @@ export interface NextUrlResult {
nextUrl: string;
lastSessionId: string | undefined;
}
+
+export interface PreparePayResult {
+ status: "paid" | "insufficient-balance" | "payment-possible" | "error";
+ contractTerms?: ContractTerms;
+ error?: string;
+ proposalId?: number;
+} \ No newline at end of file