rudimentary taler://withdraw support
This commit is contained in:
parent
70c0a557f9
commit
1390175a9a
@ -38,7 +38,7 @@
|
|||||||
"structured-clone": "^0.2.2",
|
"structured-clone": "^0.2.2",
|
||||||
"terser-webpack-plugin": "^1.2.3",
|
"terser-webpack-plugin": "^1.2.3",
|
||||||
"through2": "3.0.1",
|
"through2": "3.0.1",
|
||||||
"tslint": "^5.14.0",
|
"tslint": "^5.19.0",
|
||||||
"typedoc": "^0.15.0",
|
"typedoc": "^0.15.0",
|
||||||
"typescript": "^3.3.4000",
|
"typescript": "^3.3.4000",
|
||||||
"uglify-js": "^3.0.27",
|
"uglify-js": "^3.0.27",
|
||||||
@ -54,7 +54,7 @@
|
|||||||
"@types/urijs": "^1.19.3",
|
"@types/urijs": "^1.19.3",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"commander": "^3.0.0",
|
"commander": "^3.0.0",
|
||||||
"idb-bridge": "^0.0.7",
|
"idb-bridge": "^0.0.9",
|
||||||
"qrcode-generator": "^1.4.3",
|
"qrcode-generator": "^1.4.3",
|
||||||
"source-map-support": "^0.5.12",
|
"source-map-support": "^0.5.12",
|
||||||
"urijs": "^1.18.10"
|
"urijs": "^1.18.10"
|
||||||
|
@ -96,6 +96,8 @@ test("precoin creation", async t => {
|
|||||||
reserve_pub: pub,
|
reserve_pub: pub,
|
||||||
timestamp_confirmed: 0,
|
timestamp_confirmed: 0,
|
||||||
timestamp_depleted: 0,
|
timestamp_depleted: 0,
|
||||||
|
timestamp_reserve_info_posted: 0,
|
||||||
|
exchangeWire: "payto://foo"
|
||||||
};
|
};
|
||||||
|
|
||||||
const precoin = await crypto.createPreCoin(denomValid1, r);
|
const precoin = await crypto.createPreCoin(denomValid1, r);
|
||||||
|
@ -93,13 +93,19 @@ export class SynchronousCryptoWorker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let result: any;
|
||||||
try {
|
try {
|
||||||
const result = (impl as any)[operation](...args);
|
result = (impl as any)[operation](...args);
|
||||||
this.dispatchMessage({ result, id });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("error during operation", e);
|
console.log("error during operation", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setImmediate(() => this.dispatchMessage({ result, id }));
|
||||||
|
} catch (e) {
|
||||||
|
console.log("got error during dispatch", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,6 +81,16 @@ export interface ReserveRecord {
|
|||||||
*/
|
*/
|
||||||
timestamp_depleted: number;
|
timestamp_depleted: number;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time when the information about this reserve was posted to the bank.
|
||||||
|
*
|
||||||
|
* Only applies if bankWithdrawStatusUrl is defined.
|
||||||
|
*
|
||||||
|
* Set to 0 if that hasn't happened yet.
|
||||||
|
*/
|
||||||
|
timestamp_reserve_info_posted: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time when the reserve was confirmed.
|
* Time when the reserve was confirmed.
|
||||||
*
|
*
|
||||||
@ -117,6 +127,14 @@ export interface ReserveRecord {
|
|||||||
* transfered funds for this reserve.
|
* transfered funds for this reserve.
|
||||||
*/
|
*/
|
||||||
senderWire?: string;
|
senderWire?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire information (as payto URI) for the exchange, specifically
|
||||||
|
* the account that was transferred to when creating the reserve.
|
||||||
|
*/
|
||||||
|
exchangeWire: string;
|
||||||
|
|
||||||
|
bankWithdrawStatusUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ export class Bank {
|
|||||||
reservePub: string,
|
reservePub: string,
|
||||||
exchangePaytoUri: string,
|
exchangePaytoUri: string,
|
||||||
) {
|
) {
|
||||||
const reqUrl = new URI("taler/withdraw")
|
const reqUrl = new URI("api/withdraw-headless")
|
||||||
.absoluteTo(this.bankBaseUrl)
|
.absoluteTo(this.bankBaseUrl)
|
||||||
.href();
|
.href();
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ export class Bank {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async registerRandomUser(): Promise<BankUser> {
|
async registerRandomUser(): Promise<BankUser> {
|
||||||
const reqUrl = new URI("register").absoluteTo(this.bankBaseUrl).href();
|
const reqUrl = new URI("api/register").absoluteTo(this.bankBaseUrl).href();
|
||||||
const randId = makeId(8);
|
const randId = makeId(8);
|
||||||
const bankUser: BankUser = {
|
const bankUser: BankUser = {
|
||||||
username: `testuser-${randId}`,
|
username: `testuser-${randId}`,
|
||||||
|
@ -54,6 +54,7 @@ class ConsoleBadge implements Badge {
|
|||||||
export class NodeHttpLib implements HttpRequestLibrary {
|
export class NodeHttpLib implements HttpRequestLibrary {
|
||||||
async get(url: string): Promise<import("../http").HttpResponse> {
|
async get(url: string): Promise<import("../http").HttpResponse> {
|
||||||
enableTracing && console.log("making GET request to", url);
|
enableTracing && console.log("making GET request to", url);
|
||||||
|
try {
|
||||||
const resp = await Axios({
|
const resp = await Axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: url,
|
url: url,
|
||||||
@ -65,6 +66,9 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
responseJson: resp.data,
|
responseJson: resp.data,
|
||||||
status: resp.status,
|
status: resp.status,
|
||||||
};
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async postJson(
|
async postJson(
|
||||||
@ -72,6 +76,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
body: any,
|
body: any,
|
||||||
): Promise<import("../http").HttpResponse> {
|
): Promise<import("../http").HttpResponse> {
|
||||||
enableTracing && console.log("making POST request to", url);
|
enableTracing && console.log("making POST request to", url);
|
||||||
|
try {
|
||||||
const resp = await Axios({
|
const resp = await Axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: url,
|
url: url,
|
||||||
@ -84,25 +89,9 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
responseJson: resp.data,
|
responseJson: resp.data,
|
||||||
status: resp.status,
|
status: resp.status,
|
||||||
};
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
async postForm(
|
|
||||||
url: string,
|
|
||||||
form: any,
|
|
||||||
): Promise<import("../http").HttpResponse> {
|
|
||||||
enableTracing && console.log("making POST request to", url);
|
|
||||||
const resp = await Axios({
|
|
||||||
method: "post",
|
|
||||||
url: url,
|
|
||||||
data: querystring.stringify(form),
|
|
||||||
responseType: "json",
|
|
||||||
});
|
|
||||||
enableTracing && console.log("got response", resp.data);
|
|
||||||
enableTracing && console.log("resp type", typeof resp.data);
|
|
||||||
return {
|
|
||||||
responseJson: resp.data,
|
|
||||||
status: resp.status,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,6 +210,7 @@ export async function withdrawTestBalance(
|
|||||||
const reserveResponse = await myWallet.createReserve({
|
const reserveResponse = await myWallet.createReserve({
|
||||||
amount: amounts.parseOrThrow(amount),
|
amount: amounts.parseOrThrow(amount),
|
||||||
exchange: exchangeBaseUrl,
|
exchange: exchangeBaseUrl,
|
||||||
|
exchangeWire: "payto://unknown",
|
||||||
});
|
});
|
||||||
|
|
||||||
const bank = new Bank(bankBaseUrl);
|
const bank = new Bank(bankBaseUrl);
|
||||||
|
@ -103,15 +103,14 @@ program
|
|||||||
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");
|
||||||
const contractUrl = checkPayResp.contract_url;
|
const talerPayUri = checkPayResp.taler_pay_uri;
|
||||||
if (typeof contractUrl !== "string") {
|
if (!talerPayUri) {
|
||||||
console.error("fata: no contract url received from backend");
|
console.error("fatal: no taler pay URI received from backend");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = "talerpay:" + querystring.escape(contractUrl);
|
console.log("taler pay URI:", talerPayUri);
|
||||||
console.log("contract url:", url);
|
qrcode.addData(talerPayUri);
|
||||||
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 ...");
|
||||||
@ -127,6 +126,45 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("withdraw-url <withdraw-url>")
|
||||||
|
.action(async (withdrawUrl, cmdObj) => {
|
||||||
|
applyVerbose(program.verbose);
|
||||||
|
console.log("withdrawing", withdrawUrl);
|
||||||
|
const wallet = await getDefaultNodeWallet({
|
||||||
|
persistentStoragePath: walletDbPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const withdrawInfo = await wallet.downloadWithdrawInfo(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.createReserveFromWithdrawUrl(
|
||||||
|
withdrawUrl,
|
||||||
|
selectedExchange,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmTransferUrl) {
|
||||||
|
console.log("please confirm the transfer at", confirmTransferUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
await wallet.processReserve(reservePub);
|
||||||
|
|
||||||
|
console.log("finished withdrawing");
|
||||||
|
|
||||||
|
wallet.stop();
|
||||||
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("pay-url <pay-url>")
|
.command("pay-url <pay-url>")
|
||||||
.option("-y, --yes", "automatically answer yes to prompts")
|
.option("-y, --yes", "automatically answer yes to prompts")
|
||||||
@ -153,6 +191,11 @@ program
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (result.status === "session-replayed") {
|
||||||
|
console.log("already paid! (replayed in different session)");
|
||||||
|
process.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (result.status === "payment-possible") {
|
if (result.status === "payment-possible") {
|
||||||
console.log("paying ...");
|
console.log("paying ...");
|
||||||
} else {
|
} else {
|
||||||
|
@ -923,6 +923,9 @@ export class CheckPaymentResponse {
|
|||||||
@Checkable.Optional(Checkable.Value(() => ContractTerms))
|
@Checkable.Optional(Checkable.Value(() => ContractTerms))
|
||||||
contract_terms: ContractTerms | undefined;
|
contract_terms: ContractTerms | undefined;
|
||||||
|
|
||||||
|
@Checkable.Optional(Checkable.String())
|
||||||
|
taler_pay_uri: string | undefined;
|
||||||
|
|
||||||
@Checkable.Optional(Checkable.String())
|
@Checkable.Optional(Checkable.String())
|
||||||
contract_url: string | undefined;
|
contract_url: string | undefined;
|
||||||
|
|
||||||
@ -932,3 +935,36 @@ export class CheckPaymentResponse {
|
|||||||
*/
|
*/
|
||||||
static checked: (obj: any) => CheckPaymentResponse;
|
static checked: (obj: any) => CheckPaymentResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from the bank.
|
||||||
|
*/
|
||||||
|
@Checkable.Class({extra: true})
|
||||||
|
export class WithdrawOperationStatusResponse {
|
||||||
|
@Checkable.Boolean()
|
||||||
|
selection_done: boolean;
|
||||||
|
|
||||||
|
@Checkable.Boolean()
|
||||||
|
transfer_done: boolean;
|
||||||
|
|
||||||
|
@Checkable.String()
|
||||||
|
amount: string;
|
||||||
|
|
||||||
|
@Checkable.Optional(Checkable.String())
|
||||||
|
sender_wire?: string;
|
||||||
|
|
||||||
|
@Checkable.Optional(Checkable.String())
|
||||||
|
suggested_exchange?: string;
|
||||||
|
|
||||||
|
@Checkable.Optional(Checkable.String())
|
||||||
|
confirm_transfer_url?: string;
|
||||||
|
|
||||||
|
@Checkable.List(Checkable.String())
|
||||||
|
wire_types: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a value matches the schema of this class and convert it into a
|
||||||
|
* member.
|
||||||
|
*/
|
||||||
|
static checked: (obj: any) => WithdrawOperationStatusResponse;
|
||||||
|
}
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import test from "ava";
|
import test from "ava";
|
||||||
import { parsePayUri } from "./taleruri";
|
import { parsePayUri, parseWithdrawUri } from "./taleruri";
|
||||||
|
|
||||||
test("taler pay url parsing: http(s)", (t) => {
|
test("taler pay url parsing: http(s)", (t) => {
|
||||||
const url1 = "https://example.com/bar?spam=eggs";
|
const url1 = "https://example.com/bar?spam=eggs";
|
||||||
@ -77,3 +77,13 @@ test("taler pay url parsing: trailing parts", (t) => {
|
|||||||
t.is(r1.downloadUrl, "https://example.com/public/proposal?instance=default&order_id=myorder");
|
t.is(r1.downloadUrl, "https://example.com/public/proposal?instance=default&order_id=myorder");
|
||||||
t.is(r1.sessionId, "mysession");
|
t.is(r1.sessionId, "mysession");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("taler withdraw uri parsing", (t) => {
|
||||||
|
const url1 = "taler://withdraw/bank.example.com/-/12345";
|
||||||
|
const r1 = parseWithdrawUri(url1);
|
||||||
|
if (!r1) {
|
||||||
|
t.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
t.is(r1.statusUrl, "https://bank.example.com/api/withdraw-operation/12345");
|
||||||
|
});
|
@ -15,12 +15,39 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import URI = require("urijs");
|
import URI = require("urijs");
|
||||||
|
import { string } from "prop-types";
|
||||||
|
|
||||||
export interface PayUriResult {
|
export interface PayUriResult {
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WithdrawUriResult {
|
||||||
|
statusUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
|
||||||
|
const parsedUri = new URI(s);
|
||||||
|
if (parsedUri.scheme() !== "taler") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (parsedUri.authority() != "withdraw") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [host, path, withdrawId] = parsedUri.segmentCoded();
|
||||||
|
|
||||||
|
if (path === "-") {
|
||||||
|
path = "/api/withdraw-operation";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusUrl: new URI({ protocol: "https", hostname: host, path: path })
|
||||||
|
.segmentCoded(withdrawId)
|
||||||
|
.href(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function parsePayUri(s: string): PayUriResult | undefined {
|
export function parsePayUri(s: string): PayUriResult | undefined {
|
||||||
const parsedUri = new URI(s);
|
const parsedUri = new URI(s);
|
||||||
if (parsedUri.scheme() === "http" || parsedUri.scheme() === "https") {
|
if (parsedUri.scheme() === "http" || parsedUri.scheme() === "https") {
|
||||||
@ -68,10 +95,12 @@ export function parsePayUri(s: string): PayUriResult | undefined {
|
|||||||
|
|
||||||
const downloadUrl = new URI(
|
const downloadUrl = new URI(
|
||||||
"https://" + host + "/" + decodeURIComponent(maybePath),
|
"https://" + host + "/" + decodeURIComponent(maybePath),
|
||||||
).addQuery({ instance: maybeInstance, order_id: orderId }).href();
|
)
|
||||||
|
.addQuery({ instance: maybeInstance, order_id: orderId })
|
||||||
|
.href();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
downloadUrl,
|
downloadUrl,
|
||||||
sessionId: maybeSessionid,
|
sessionId: maybeSessionid,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
199
src/wallet.ts
199
src/wallet.ts
@ -81,6 +81,7 @@ import {
|
|||||||
TipPlanchetDetail,
|
TipPlanchetDetail,
|
||||||
TipResponse,
|
TipResponse,
|
||||||
TipToken,
|
TipToken,
|
||||||
|
WithdrawOperationStatusResponse,
|
||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@ -103,9 +104,10 @@ import {
|
|||||||
WalletBalance,
|
WalletBalance,
|
||||||
WalletBalanceEntry,
|
WalletBalanceEntry,
|
||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
|
DownloadedWithdrawInfo,
|
||||||
} from "./walletTypes";
|
} from "./walletTypes";
|
||||||
import { openPromise } from "./promiseUtils";
|
import { openPromise } from "./promiseUtils";
|
||||||
import Axios from "axios";
|
import { parsePayUri, parseWithdrawUri } from "./taleruri";
|
||||||
|
|
||||||
interface SpeculativePayData {
|
interface SpeculativePayData {
|
||||||
payCoinInfo: PayCoinInfo;
|
payCoinInfo: PayCoinInfo;
|
||||||
@ -183,7 +185,8 @@ export function getTotalRefreshCost(
|
|||||||
...withdrawDenoms.map(d => d.value),
|
...withdrawDenoms.map(d => d.value),
|
||||||
).amount;
|
).amount;
|
||||||
const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
|
const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
|
||||||
Wallet.enableTracing && console.log(
|
Wallet.enableTracing &&
|
||||||
|
console.log(
|
||||||
"total refresh cost for",
|
"total refresh cost for",
|
||||||
amountToPretty(amountLeft),
|
amountToPretty(amountLeft),
|
||||||
"is",
|
"is",
|
||||||
@ -255,7 +258,8 @@ export function selectPayCoins(
|
|||||||
const depositFeeToCover = Amounts.sub(accDepositFee, depositFeeLimit)
|
const depositFeeToCover = Amounts.sub(accDepositFee, depositFeeLimit)
|
||||||
.amount;
|
.amount;
|
||||||
leftAmount = Amounts.sub(leftAmount, depositFeeToCover).amount;
|
leftAmount = Amounts.sub(leftAmount, depositFeeToCover).amount;
|
||||||
Wallet.enableTracing && console.log("deposit fee to cover", amountToPretty(depositFeeToCover));
|
Wallet.enableTracing &&
|
||||||
|
console.log("deposit fee to cover", amountToPretty(depositFeeToCover));
|
||||||
|
|
||||||
let totalFees: AmountJson = Amounts.getZero(currency);
|
let totalFees: AmountJson = Amounts.getZero(currency);
|
||||||
if (coversAmountWithFee && !isBelowFee) {
|
if (coversAmountWithFee && !isBelowFee) {
|
||||||
@ -714,17 +718,22 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async preparePay(url: string): Promise<PreparePayResult> {
|
async preparePay(url: string): Promise<PreparePayResult> {
|
||||||
const talerpayPrefix = "talerpay:";
|
const uriResult = parsePayUri(url);
|
||||||
let downloadSessionId: string | undefined;
|
|
||||||
if (url.startsWith(talerpayPrefix)) {
|
if (!uriResult) {
|
||||||
let [p1, p2] = url.substring(talerpayPrefix.length).split(";");
|
return {
|
||||||
url = decodeURIComponent(p1);
|
status: "error",
|
||||||
downloadSessionId = p2;
|
error: "URI not supported",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let proposalId: number;
|
let proposalId: number;
|
||||||
let checkResult: CheckPayResult;
|
let checkResult: CheckPayResult;
|
||||||
try {
|
try {
|
||||||
proposalId = await this.downloadProposal(url, downloadSessionId);
|
proposalId = await this.downloadProposal(
|
||||||
|
uriResult.downloadUrl,
|
||||||
|
uriResult.sessionId,
|
||||||
|
);
|
||||||
checkResult = await this.checkPay(proposalId);
|
checkResult = await this.checkPay(proposalId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
@ -736,6 +745,27 @@ export class Wallet {
|
|||||||
if (!proposal) {
|
if (!proposal) {
|
||||||
throw Error("could not get proposal");
|
throw Error("could not get proposal");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("proposal", proposal);
|
||||||
|
|
||||||
|
if (uriResult.sessionId) {
|
||||||
|
const existingPayment = await this.q().getIndexed(
|
||||||
|
Stores.purchases.fulfillmentUrlIndex,
|
||||||
|
proposal.contractTerms.fulfillment_url,
|
||||||
|
);
|
||||||
|
if (existingPayment) {
|
||||||
|
console.log("existing payment", existingPayment);
|
||||||
|
await this.submitPay(
|
||||||
|
existingPayment.contractTermsHash,
|
||||||
|
uriResult.sessionId,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: "session-replayed",
|
||||||
|
contractTerms: existingPayment.contractTerms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (checkResult.status === "paid") {
|
if (checkResult.status === "paid") {
|
||||||
return {
|
return {
|
||||||
status: "paid",
|
status: "paid",
|
||||||
@ -1139,21 +1169,78 @@ export class Wallet {
|
|||||||
const op = openPromise<void>();
|
const op = openPromise<void>();
|
||||||
|
|
||||||
const processReserveInternal = async (retryDelayMs: number = 250) => {
|
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 {
|
try {
|
||||||
const reserve = await this.updateReserve(reservePub);
|
const reserve = await this.q().get<ReserveRecord>(
|
||||||
await this.depleteReserve(reserve);
|
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;
|
||||||
|
const now = new Date().getTime();
|
||||||
|
let status;
|
||||||
|
try {
|
||||||
|
const statusResp = await this.http.get(bankStatusUrl);
|
||||||
|
status = WithdrawOperationStatusResponse.checked(
|
||||||
|
statusResp.responseJson,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("bank error response", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.transfer_done) {
|
||||||
|
await this.q().mutate(Stores.reserves, reservePub, r => {
|
||||||
|
r.timestamp_confirmed = now;
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
} else if (reserve.timestamp_reserve_info_posted === 0) {
|
||||||
|
try {
|
||||||
|
if (!status.selection_done) {
|
||||||
|
const bankResp = await this.http.postJson(bankStatusUrl, {
|
||||||
|
reserve_pub: reservePub,
|
||||||
|
selected_exchange: reserve.exchangeWire,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("bank error response", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
await this.q().mutate(Stores.reserves, reservePub, r => {
|
||||||
|
r.timestamp_reserve_info_posted = now;
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
throw Error("waiting for reserve to be confirmed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedReserve = await this.updateReserve(reservePub);
|
||||||
|
await this.depleteReserve(updatedReserve);
|
||||||
op.resolve();
|
op.resolve();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// random, exponential backoff truncated at 3 minutes
|
if (isHardError) {
|
||||||
|
op.reject(e);
|
||||||
|
}
|
||||||
const nextDelay = Math.min(
|
const nextDelay = Math.min(
|
||||||
2 * retryDelayMs + retryDelayMs * Math.random(),
|
2 * retryDelayMs + retryDelayMs * Math.random(),
|
||||||
3000 * 60,
|
maxTimeout,
|
||||||
);
|
);
|
||||||
Wallet.enableTracing &&
|
|
||||||
console.warn(
|
|
||||||
`Failed to deplete reserve, trying again in ${retryDelayMs} ms`,
|
|
||||||
);
|
|
||||||
Wallet.enableTracing && console.info("Cause for retry was:", e);
|
|
||||||
this.timerGroup.after(retryDelayMs, () =>
|
this.timerGroup.after(retryDelayMs, () =>
|
||||||
processReserveInternal(nextDelay),
|
processReserveInternal(nextDelay),
|
||||||
);
|
);
|
||||||
@ -1346,7 +1433,10 @@ export class Wallet {
|
|||||||
reserve_pub: keypair.pub,
|
reserve_pub: keypair.pub,
|
||||||
senderWire: req.senderWire,
|
senderWire: req.senderWire,
|
||||||
timestamp_confirmed: 0,
|
timestamp_confirmed: 0,
|
||||||
|
timestamp_reserve_info_posted: 0,
|
||||||
timestamp_depleted: 0,
|
timestamp_depleted: 0,
|
||||||
|
bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
|
||||||
|
exchangeWire: req.exchangeWire,
|
||||||
};
|
};
|
||||||
|
|
||||||
const senderWire = req.senderWire;
|
const senderWire = req.senderWire;
|
||||||
@ -1387,6 +1477,10 @@ export class Wallet {
|
|||||||
.put(Stores.reserves, reserveRecord)
|
.put(Stores.reserves, reserveRecord)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
|
if (req.bankWithdrawStatusUrl) {
|
||||||
|
this.processReserve(keypair.pub);
|
||||||
|
}
|
||||||
|
|
||||||
const r: CreateReserveResponse = {
|
const r: CreateReserveResponse = {
|
||||||
exchange: canonExchange,
|
exchange: canonExchange,
|
||||||
reservePub: keypair.pub,
|
reservePub: keypair.pub,
|
||||||
@ -1513,6 +1607,7 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const preCoin = await this.cryptoApi.createPreCoin(denom, reserve);
|
const preCoin = await this.cryptoApi.createPreCoin(denom, reserve);
|
||||||
|
|
||||||
// This will fail and throw an exception if the remaining amount in the
|
// This will fail and throw an exception if the remaining amount in the
|
||||||
// reserve is too low to create a pre-coin.
|
// reserve is too low to create a pre-coin.
|
||||||
try {
|
try {
|
||||||
@ -1520,6 +1615,7 @@ export class Wallet {
|
|||||||
.put(Stores.precoins, preCoin)
|
.put(Stores.precoins, preCoin)
|
||||||
.mutate(Stores.reserves, reserve.reserve_pub, mutateReserve)
|
.mutate(Stores.reserves, reserve.reserve_pub, mutateReserve)
|
||||||
.finish();
|
.finish();
|
||||||
|
console.log("created precoin", preCoin.coinPub);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("can't create pre-coin:", e.name, e.message);
|
console.log("can't create pre-coin:", e.name, e.message);
|
||||||
return;
|
return;
|
||||||
@ -1542,6 +1638,11 @@ export class Wallet {
|
|||||||
if (!reserve) {
|
if (!reserve) {
|
||||||
throw Error("reserve not in db");
|
throw Error("reserve not in db");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reserve.timestamp_confirmed === 0) {
|
||||||
|
throw Error("");
|
||||||
|
}
|
||||||
|
|
||||||
const reqUrl = new URI("reserve/status").absoluteTo(
|
const reqUrl = new URI("reserve/status").absoluteTo(
|
||||||
reserve.exchange_base_url,
|
reserve.exchange_base_url,
|
||||||
);
|
);
|
||||||
@ -2462,7 +2563,14 @@ export class Wallet {
|
|||||||
refreshSession.exchangeBaseUrl,
|
refreshSession.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
Wallet.enableTracing && console.log("reveal request:", req);
|
Wallet.enableTracing && console.log("reveal request:", req);
|
||||||
const resp = await this.http.postJson(reqUrl.href(), req);
|
|
||||||
|
let resp;
|
||||||
|
try {
|
||||||
|
resp = await this.http.postJson(reqUrl.href(), req);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("got error during /refresh/reveal request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Wallet.enableTracing && console.log("session:", refreshSession);
|
Wallet.enableTracing && console.log("session:", refreshSession);
|
||||||
Wallet.enableTracing && console.log("reveal response:", resp);
|
Wallet.enableTracing && console.log("reveal response:", resp);
|
||||||
@ -3427,6 +3535,57 @@ export class Wallet {
|
|||||||
// strategy to test it.
|
// strategy to test it.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadWithdrawInfo(
|
||||||
|
talerWithdrawUri: string,
|
||||||
|
): Promise<DownloadedWithdrawInfo> {
|
||||||
|
const uriResult = parseWithdrawUri(talerWithdrawUri);
|
||||||
|
if (!uriResult) {
|
||||||
|
throw Error("can't parse URL");
|
||||||
|
}
|
||||||
|
const resp = await this.http.get(uriResult.statusUrl);
|
||||||
|
console.log("resp:", resp.responseJson);
|
||||||
|
const status = WithdrawOperationStatusResponse.checked(resp.responseJson);
|
||||||
|
return {
|
||||||
|
amount: Amounts.parseOrThrow(status.amount),
|
||||||
|
confirmTransferUrl: status.confirm_transfer_url,
|
||||||
|
extractedStatusUrl: uriResult.statusUrl,
|
||||||
|
selectionDone: status.selection_done,
|
||||||
|
senderWire: status.sender_wire,
|
||||||
|
suggestedExchange: status.suggested_exchange,
|
||||||
|
transferDone: status.transfer_done,
|
||||||
|
wireTypes: status.wire_types,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createReserveFromWithdrawUrl(
|
||||||
|
talerWithdrawUri: string,
|
||||||
|
selectedExchange: string,
|
||||||
|
): Promise<{ reservePub: string; confirmTransferUrl?: string }> {
|
||||||
|
const withdrawInfo = await this.downloadWithdrawInfo(talerWithdrawUri);
|
||||||
|
const exchangeWire = await this.getExchangePaytoUri(
|
||||||
|
selectedExchange,
|
||||||
|
withdrawInfo.wireTypes,
|
||||||
|
);
|
||||||
|
const reserve = await this.createReserve({
|
||||||
|
amount: withdrawInfo.amount,
|
||||||
|
bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
|
||||||
|
exchange: selectedExchange,
|
||||||
|
senderWire: withdrawInfo.senderWire,
|
||||||
|
exchangeWire: exchangeWire,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
reservePub: reserve.reservePub,
|
||||||
|
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the retry timeouts for ongoing operations.
|
||||||
|
*/
|
||||||
|
resetRetryTimeouts(): void {
|
||||||
|
// FIXME: implement
|
||||||
|
}
|
||||||
|
|
||||||
clearNotification(): void {
|
clearNotification(): void {
|
||||||
this.badge.clearNotification();
|
this.badge.clearNotification();
|
||||||
}
|
}
|
||||||
|
@ -324,6 +324,13 @@ export class CreateReserveRequest {
|
|||||||
@Checkable.String()
|
@Checkable.String()
|
||||||
exchange: string;
|
exchange: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payto URI that identifies the exchange's account that the funds
|
||||||
|
* for this reserve go into.
|
||||||
|
*/
|
||||||
|
@Checkable.String()
|
||||||
|
exchangeWire: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire details (as a payto URI) for the bank account that sent the funds to
|
* Wire details (as a payto URI) for the bank account that sent the funds to
|
||||||
* the exchange.
|
* the exchange.
|
||||||
@ -331,6 +338,12 @@ export class CreateReserveRequest {
|
|||||||
@Checkable.Optional(Checkable.String())
|
@Checkable.Optional(Checkable.String())
|
||||||
senderWire?: string;
|
senderWire?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL to fetch the withdraw status from the bank.
|
||||||
|
*/
|
||||||
|
@Checkable.Optional(Checkable.String())
|
||||||
|
bankWithdrawStatusUrl?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that a value matches the schema of this class and convert it into a
|
* Verify that a value matches the schema of this class and convert it into a
|
||||||
* member.
|
* member.
|
||||||
@ -474,9 +487,20 @@ export interface NextUrlResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PreparePayResult {
|
export interface PreparePayResult {
|
||||||
status: "paid" | "insufficient-balance" | "payment-possible" | "error";
|
status: "paid" | "session-replayed" | "insufficient-balance" | "payment-possible" | "error";
|
||||||
contractTerms?: ContractTerms;
|
contractTerms?: ContractTerms;
|
||||||
error?: string;
|
error?: string;
|
||||||
proposalId?: number;
|
proposalId?: number;
|
||||||
totalFees?: AmountJson;
|
totalFees?: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DownloadedWithdrawInfo {
|
||||||
|
selectionDone: boolean;
|
||||||
|
transferDone: boolean;
|
||||||
|
amount: AmountJson;
|
||||||
|
senderWire?: string;
|
||||||
|
suggestedExchange?: string;
|
||||||
|
confirmTransferUrl?: string;
|
||||||
|
wireTypes: string[];
|
||||||
|
extractedStatusUrl: string;
|
||||||
|
}
|
@ -55,6 +55,8 @@
|
|||||||
"src/promiseUtils.ts",
|
"src/promiseUtils.ts",
|
||||||
"src/query.ts",
|
"src/query.ts",
|
||||||
"src/talerTypes.ts",
|
"src/talerTypes.ts",
|
||||||
|
"src/taleruri-test.ts",
|
||||||
|
"src/taleruri.ts",
|
||||||
"src/timer.ts",
|
"src/timer.ts",
|
||||||
"src/types-test.ts",
|
"src/types-test.ts",
|
||||||
"src/wallet-test.ts",
|
"src/wallet-test.ts",
|
||||||
|
16
yarn.lock
16
yarn.lock
@ -3165,10 +3165,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer ">= 2.1.2 < 3"
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
idb-bridge@^0.0.7:
|
idb-bridge@^0.0.9:
|
||||||
version "0.0.7"
|
version "0.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.7.tgz#34705d79ab992c4b5d5fa048c313ac5f803a972f"
|
resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.9.tgz#28c9a9e50b275dc80316b29bdaec536ee96bc65b"
|
||||||
integrity sha512-vGTYbX6ni8h/6B2POS6f1Nuzp47+Tna5MggQKDmQp1MLCPFbI8RXBzs1rvKWuMx+WKX3LtMWbxSm8hQBnI9DLA==
|
integrity sha512-MOoiDJvbhskEzyDtbOz9ecfGZ2Jx0CZ6L81h71fPjdERCkUUqvkt6p7RkChPr8GmteIfja2k9mIRNFkS7enmrQ==
|
||||||
|
|
||||||
ieee754@^1.1.4:
|
ieee754@^1.1.4:
|
||||||
version "1.1.13"
|
version "1.1.13"
|
||||||
@ -6250,10 +6250,10 @@ tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
||||||
|
|
||||||
tslint@^5.14.0:
|
tslint@^5.19.0:
|
||||||
version "5.18.0"
|
version "5.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.18.0.tgz#f61a6ddcf372344ac5e41708095bbf043a147ac6"
|
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.19.0.tgz#a2cbd4a7699386da823f6b499b8394d6c47bb968"
|
||||||
integrity sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w==
|
integrity sha512-1LwwtBxfRJZnUvoS9c0uj8XQtAnyhWr9KlNvDIdB+oXyT+VpsOAaEhEgKi1HrZ8rq0ki/AAnbGSv4KM6/AfVZw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.0.0"
|
"@babel/code-frame" "^7.0.0"
|
||||||
builtin-modules "^1.1.1"
|
builtin-modules "^1.1.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user