diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-08-20 17:58:01 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-08-20 17:58:01 +0200 |
commit | 106bc6ad9a78c199e6b0e89bef854174d9014c28 (patch) | |
tree | dbb1579c29fca6cd133f2c2bd67ff36150a5cd7b | |
parent | 0f2fbf20ed3a74c3a73619f59ae8fe17bd2a883f (diff) |
implement pay-url and disable broken wallet GC
-rw-r--r-- | src/headless/taler-wallet-cli.ts | 104 | ||||
-rw-r--r-- | src/wallet.ts | 131 | ||||
-rw-r--r-- | src/walletTypes.ts | 7 |
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 |