implement pay-url and disable broken wallet GC
This commit is contained in:
parent
0f2fbf20ed
commit
106bc6ad9a
@ -21,13 +21,27 @@ import { MerchantBackendConnection } from "./merchant";
|
|||||||
import { runIntegrationTest } from "./integrationtest";
|
import { runIntegrationTest } from "./integrationtest";
|
||||||
import { Wallet } from "../wallet";
|
import { Wallet } from "../wallet";
|
||||||
import querystring = require("querystring");
|
import querystring = require("querystring");
|
||||||
import qrcodeGenerator = require("qrcode-generator")
|
import qrcodeGenerator = require("qrcode-generator");
|
||||||
|
import readline = require("readline");
|
||||||
|
|
||||||
const program = new commander.Command();
|
const program = new commander.Command();
|
||||||
program.version("0.0.1").option("--verbose", "enable verbose output", false);
|
program.version("0.0.1").option("--verbose", "enable verbose output", false);
|
||||||
|
|
||||||
const walletDbPath = os.homedir + "/" + ".talerwalletdb.json";
|
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) {
|
function applyVerbose(verbose: boolean) {
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
console.log("enabled verbose logging");
|
console.log("enabled verbose logging");
|
||||||
@ -64,8 +78,8 @@ program
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function asyncSleep(milliSeconds: number): Promise<void> {
|
async function asyncSleep(milliSeconds: number): Promise<void> {
|
||||||
return new Promise<void>((resolve, reject) =>{
|
return new Promise<void>((resolve, reject) => {
|
||||||
setTimeout(() => resolve(), milliSeconds)
|
setTimeout(() => resolve(), milliSeconds);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,8 +90,16 @@ program
|
|||||||
.action(async cmdObj => {
|
.action(async cmdObj => {
|
||||||
applyVerbose(program.verbose);
|
applyVerbose(program.verbose);
|
||||||
console.log("creating order");
|
console.log("creating order");
|
||||||
const merchantBackend = new MerchantBackendConnection("https://backend.test.taler.net", "default", "sandbox");
|
const merchantBackend = new MerchantBackendConnection(
|
||||||
const orderResp = await merchantBackend.createOrder(cmdObj.amount, cmdObj.summary, "");
|
"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);
|
console.log("created new order with order ID", orderResp.orderId);
|
||||||
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
|
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
|
||||||
const qrcode = qrcodeGenerator(0, "M");
|
const qrcode = qrcodeGenerator(0, "M");
|
||||||
@ -87,20 +109,84 @@ program
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
qrcode.addData("talerpay:" + querystring.escape(contractUrl));
|
const url = "talerpay:" + querystring.escape(contractUrl);
|
||||||
|
console.log("contract url:", url);
|
||||||
|
qrcode.addData(url);
|
||||||
qrcode.make();
|
qrcode.make();
|
||||||
console.log(qrcode.createASCII());
|
console.log(qrcode.createASCII());
|
||||||
console.log("waiting for payment ...")
|
console.log("waiting for payment ...");
|
||||||
while (1) {
|
while (1) {
|
||||||
await asyncSleep(500);
|
await asyncSleep(500);
|
||||||
const checkPayResp2 = await merchantBackend.checkPayment(orderResp.orderId);
|
const checkPayResp2 = await merchantBackend.checkPayment(
|
||||||
|
orderResp.orderId,
|
||||||
|
);
|
||||||
if (checkPayResp2.paid) {
|
if (checkPayResp2.paid) {
|
||||||
console.log("payment successfully received!")
|
console.log("payment successfully received!");
|
||||||
break;
|
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
|
program
|
||||||
.command("integrationtest")
|
.command("integrationtest")
|
||||||
.option(
|
.option(
|
||||||
|
131
src/wallet.ts
131
src/wallet.ts
@ -104,6 +104,7 @@ import {
|
|||||||
TipStatus,
|
TipStatus,
|
||||||
WalletBalance,
|
WalletBalance,
|
||||||
WalletBalanceEntry,
|
WalletBalanceEntry,
|
||||||
|
PreparePayResult,
|
||||||
} from "./walletTypes";
|
} from "./walletTypes";
|
||||||
import { openPromise } from "./promiseUtils";
|
import { openPromise } from "./promiseUtils";
|
||||||
|
|
||||||
@ -382,7 +383,6 @@ export class Wallet {
|
|||||||
|
|
||||||
private async fillDefaults() {
|
private async fillDefaults() {
|
||||||
const onTrue = (r: QueryRoot) => {
|
const onTrue = (r: QueryRoot) => {
|
||||||
console.log("defaults already applied");
|
|
||||||
};
|
};
|
||||||
const onFalse = (r: QueryRoot) => {
|
const onFalse = (r: QueryRoot) => {
|
||||||
Wallet.enableTracing && console.log("applying defaults");
|
Wallet.enableTracing && console.log("applying defaults");
|
||||||
@ -593,6 +593,8 @@ export class Wallet {
|
|||||||
.iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl)
|
.iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl)
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
|
console.log("considering coins", coins);
|
||||||
|
|
||||||
const denoms = await this.q()
|
const denoms = await this.q()
|
||||||
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
|
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
|
||||||
.toArray();
|
.toArray();
|
||||||
@ -718,6 +720,53 @@ export class Wallet {
|
|||||||
return t;
|
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.
|
* Download a proposal and store it in the database.
|
||||||
* Returns an id for it to retrieve it later.
|
* Returns an id for it to retrieve it later.
|
||||||
@ -1001,6 +1050,8 @@ export class Wallet {
|
|||||||
|
|
||||||
const paymentAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
|
const paymentAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
|
||||||
|
|
||||||
|
Wallet.enableTracing && console.log(`checking if payment of ${JSON.stringify(paymentAmount)} is possible`);
|
||||||
|
|
||||||
let wireFeeLimit;
|
let wireFeeLimit;
|
||||||
if (proposal.contractTerms.max_wire_fee) {
|
if (proposal.contractTerms.max_wire_fee) {
|
||||||
wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
|
wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
|
||||||
@ -1146,7 +1197,10 @@ export class Wallet {
|
|||||||
this.processPreCoinThrottle[preCoin.exchangeBaseUrl]
|
this.processPreCoinThrottle[preCoin.exchangeBaseUrl]
|
||||||
) {
|
) {
|
||||||
const timeout = Math.min(retryDelayMs * 2, 5 * 60 * 1000);
|
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());
|
this.timerGroup.after(retryDelayMs, () => processPreCoinInternal());
|
||||||
return op.promise;
|
return op.promise;
|
||||||
}
|
}
|
||||||
@ -3370,76 +3424,11 @@ export class Wallet {
|
|||||||
* based on the current system time.
|
* based on the current system time.
|
||||||
*/
|
*/
|
||||||
async collectGarbage() {
|
async collectGarbage() {
|
||||||
const nowMilli = new Date().getTime();
|
// FIXME(#5845)
|
||||||
const nowSec = Math.floor(nowMilli / 1000);
|
|
||||||
|
|
||||||
const gcReserve = (r: ReserveRecord, n: number) => {
|
// We currently do not garbage-collect the wallet database. This might change
|
||||||
// This rule to purge reserves is a bit over-eager, since we still might
|
// after the feature has been properly re-designed, and we have come up with a
|
||||||
// receive an emergency payback from the exchange. In this case we need
|
// strategy to test it.
|
||||||
// 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(#5210) also GC coins
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearNotification(): void {
|
clearNotification(): void {
|
||||||
|
@ -472,3 +472,10 @@ export interface NextUrlResult {
|
|||||||
nextUrl: string;
|
nextUrl: string;
|
||||||
lastSessionId: string | undefined;
|
lastSessionId: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PreparePayResult {
|
||||||
|
status: "paid" | "insufficient-balance" | "payment-possible" | "error";
|
||||||
|
contractTerms?: ContractTerms;
|
||||||
|
error?: string;
|
||||||
|
proposalId?: number;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user