rudimentary taler://withdraw support

This commit is contained in:
Florian Dold 2019-08-28 02:49:27 +02:00
parent 70c0a557f9
commit 1390175a9a
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
14 changed files with 411 additions and 92 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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