rudimentary taler://withdraw support
This commit is contained in:
parent
70c0a557f9
commit
1390175a9a
@ -38,7 +38,7 @@
|
||||
"structured-clone": "^0.2.2",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"through2": "3.0.1",
|
||||
"tslint": "^5.14.0",
|
||||
"tslint": "^5.19.0",
|
||||
"typedoc": "^0.15.0",
|
||||
"typescript": "^3.3.4000",
|
||||
"uglify-js": "^3.0.27",
|
||||
@ -54,7 +54,7 @@
|
||||
"@types/urijs": "^1.19.3",
|
||||
"axios": "^0.19.0",
|
||||
"commander": "^3.0.0",
|
||||
"idb-bridge": "^0.0.7",
|
||||
"idb-bridge": "^0.0.9",
|
||||
"qrcode-generator": "^1.4.3",
|
||||
"source-map-support": "^0.5.12",
|
||||
"urijs": "^1.18.10"
|
||||
|
@ -96,6 +96,8 @@ test("precoin creation", async t => {
|
||||
reserve_pub: pub,
|
||||
timestamp_confirmed: 0,
|
||||
timestamp_depleted: 0,
|
||||
timestamp_reserve_info_posted: 0,
|
||||
exchangeWire: "payto://foo"
|
||||
};
|
||||
|
||||
const precoin = await crypto.createPreCoin(denomValid1, r);
|
||||
|
@ -93,13 +93,19 @@ export class SynchronousCryptoWorker {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: any;
|
||||
try {
|
||||
const result = (impl as any)[operation](...args);
|
||||
this.dispatchMessage({ result, id });
|
||||
result = (impl as any)[operation](...args);
|
||||
} catch (e) {
|
||||
console.log("error during operation", e);
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@ -117,6 +127,14 @@ export interface ReserveRecord {
|
||||
* transfered funds for this reserve.
|
||||
*/
|
||||
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,
|
||||
exchangePaytoUri: string,
|
||||
) {
|
||||
const reqUrl = new URI("taler/withdraw")
|
||||
const reqUrl = new URI("api/withdraw-headless")
|
||||
.absoluteTo(this.bankBaseUrl)
|
||||
.href();
|
||||
|
||||
@ -80,7 +80,7 @@ export class Bank {
|
||||
}
|
||||
|
||||
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 bankUser: BankUser = {
|
||||
username: `testuser-${randId}`,
|
||||
|
@ -54,17 +54,21 @@ class ConsoleBadge implements Badge {
|
||||
export class NodeHttpLib implements HttpRequestLibrary {
|
||||
async get(url: string): Promise<import("../http").HttpResponse> {
|
||||
enableTracing && console.log("making GET request to", url);
|
||||
const resp = await Axios({
|
||||
method: "get",
|
||||
url: url,
|
||||
responseType: "json",
|
||||
});
|
||||
enableTracing && console.log("got response", resp.data);
|
||||
enableTracing && console.log("resp type", typeof resp.data);
|
||||
return {
|
||||
responseJson: resp.data,
|
||||
status: resp.status,
|
||||
};
|
||||
try {
|
||||
const resp = await Axios({
|
||||
method: "get",
|
||||
url: url,
|
||||
responseType: "json",
|
||||
});
|
||||
enableTracing && console.log("got response", resp.data);
|
||||
enableTracing && console.log("resp type", typeof resp.data);
|
||||
return {
|
||||
responseJson: resp.data,
|
||||
status: resp.status,
|
||||
};
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async postJson(
|
||||
@ -72,37 +76,22 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
||||
body: any,
|
||||
): Promise<import("../http").HttpResponse> {
|
||||
enableTracing && console.log("making POST request to", url);
|
||||
const resp = await Axios({
|
||||
method: "post",
|
||||
url: url,
|
||||
responseType: "json",
|
||||
data: body,
|
||||
});
|
||||
enableTracing && console.log("got response", resp.data);
|
||||
enableTracing && console.log("resp type", typeof resp.data);
|
||||
return {
|
||||
responseJson: resp.data,
|
||||
status: resp.status,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
try {
|
||||
const resp = await Axios({
|
||||
method: "post",
|
||||
url: url,
|
||||
responseType: "json",
|
||||
data: body,
|
||||
});
|
||||
enableTracing && console.log("got response", resp.data);
|
||||
enableTracing && console.log("resp type", typeof resp.data);
|
||||
return {
|
||||
responseJson: resp.data,
|
||||
status: resp.status,
|
||||
};
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,6 +210,7 @@ export async function withdrawTestBalance(
|
||||
const reserveResponse = await myWallet.createReserve({
|
||||
amount: amounts.parseOrThrow(amount),
|
||||
exchange: exchangeBaseUrl,
|
||||
exchangeWire: "payto://unknown",
|
||||
});
|
||||
|
||||
const bank = new Bank(bankBaseUrl);
|
||||
|
@ -103,15 +103,14 @@ program
|
||||
console.log("created new order with order ID", orderResp.orderId);
|
||||
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
|
||||
const qrcode = qrcodeGenerator(0, "M");
|
||||
const contractUrl = checkPayResp.contract_url;
|
||||
if (typeof contractUrl !== "string") {
|
||||
console.error("fata: no contract url received from backend");
|
||||
const talerPayUri = checkPayResp.taler_pay_uri;
|
||||
if (!talerPayUri) {
|
||||
console.error("fatal: no taler pay URI received from backend");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
const url = "talerpay:" + querystring.escape(contractUrl);
|
||||
console.log("contract url:", url);
|
||||
qrcode.addData(url);
|
||||
console.log("taler pay URI:", talerPayUri);
|
||||
qrcode.addData(talerPayUri);
|
||||
qrcode.make();
|
||||
console.log(qrcode.createASCII());
|
||||
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
|
||||
.command("pay-url <pay-url>")
|
||||
.option("-y, --yes", "automatically answer yes to prompts")
|
||||
@ -153,6 +191,11 @@ program
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
if (result.status === "session-replayed") {
|
||||
console.log("already paid! (replayed in different session)");
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
if (result.status === "payment-possible") {
|
||||
console.log("paying ...");
|
||||
} else {
|
||||
@ -179,7 +222,7 @@ program
|
||||
|
||||
if (pay) {
|
||||
const payRes = await wallet.confirmPay(result.proposalId!, undefined);
|
||||
console.log("paid!");
|
||||
console.log("paid!");
|
||||
} else {
|
||||
console.log("not paying");
|
||||
}
|
||||
|
@ -923,6 +923,9 @@ export class CheckPaymentResponse {
|
||||
@Checkable.Optional(Checkable.Value(() => ContractTerms))
|
||||
contract_terms: ContractTerms | undefined;
|
||||
|
||||
@Checkable.Optional(Checkable.String())
|
||||
taler_pay_uri: string | undefined;
|
||||
|
||||
@Checkable.Optional(Checkable.String())
|
||||
contract_url: string | undefined;
|
||||
|
||||
@ -931,4 +934,37 @@ export class CheckPaymentResponse {
|
||||
* member.
|
||||
*/
|
||||
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 { parsePayUri } from "./taleruri";
|
||||
import { parsePayUri, parseWithdrawUri } from "./taleruri";
|
||||
|
||||
test("taler pay url parsing: http(s)", (t) => {
|
||||
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.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 { string } from "prop-types";
|
||||
|
||||
export interface PayUriResult {
|
||||
downloadUrl: 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 {
|
||||
const parsedUri = new URI(s);
|
||||
if (parsedUri.scheme() === "http" || parsedUri.scheme() === "https") {
|
||||
@ -68,10 +95,12 @@ export function parsePayUri(s: string): PayUriResult | undefined {
|
||||
|
||||
const downloadUrl = new URI(
|
||||
"https://" + host + "/" + decodeURIComponent(maybePath),
|
||||
).addQuery({ instance: maybeInstance, order_id: orderId }).href();
|
||||
)
|
||||
.addQuery({ instance: maybeInstance, order_id: orderId })
|
||||
.href();
|
||||
|
||||
return {
|
||||
downloadUrl,
|
||||
sessionId: maybeSessionid,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
209
src/wallet.ts
209
src/wallet.ts
@ -81,6 +81,7 @@ import {
|
||||
TipPlanchetDetail,
|
||||
TipResponse,
|
||||
TipToken,
|
||||
WithdrawOperationStatusResponse,
|
||||
} from "./talerTypes";
|
||||
import {
|
||||
Badge,
|
||||
@ -103,9 +104,10 @@ import {
|
||||
WalletBalance,
|
||||
WalletBalanceEntry,
|
||||
PreparePayResult,
|
||||
DownloadedWithdrawInfo,
|
||||
} from "./walletTypes";
|
||||
import { openPromise } from "./promiseUtils";
|
||||
import Axios from "axios";
|
||||
import { parsePayUri, parseWithdrawUri } from "./taleruri";
|
||||
|
||||
interface SpeculativePayData {
|
||||
payCoinInfo: PayCoinInfo;
|
||||
@ -183,12 +185,13 @@ export function getTotalRefreshCost(
|
||||
...withdrawDenoms.map(d => d.value),
|
||||
).amount;
|
||||
const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
|
||||
Wallet.enableTracing && console.log(
|
||||
"total refresh cost for",
|
||||
amountToPretty(amountLeft),
|
||||
"is",
|
||||
amountToPretty(totalCost),
|
||||
);
|
||||
Wallet.enableTracing &&
|
||||
console.log(
|
||||
"total refresh cost for",
|
||||
amountToPretty(amountLeft),
|
||||
"is",
|
||||
amountToPretty(totalCost),
|
||||
);
|
||||
return totalCost;
|
||||
}
|
||||
|
||||
@ -255,7 +258,8 @@ export function selectPayCoins(
|
||||
const depositFeeToCover = Amounts.sub(accDepositFee, depositFeeLimit)
|
||||
.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);
|
||||
if (coversAmountWithFee && !isBelowFee) {
|
||||
@ -714,17 +718,22 @@ export class Wallet {
|
||||
}
|
||||
|
||||
async preparePay(url: string): Promise<PreparePayResult> {
|
||||
const talerpayPrefix = "talerpay:";
|
||||
let downloadSessionId: string | undefined;
|
||||
if (url.startsWith(talerpayPrefix)) {
|
||||
let [p1, p2] = url.substring(talerpayPrefix.length).split(";");
|
||||
url = decodeURIComponent(p1);
|
||||
downloadSessionId = p2;
|
||||
const uriResult = parsePayUri(url);
|
||||
|
||||
if (!uriResult) {
|
||||
return {
|
||||
status: "error",
|
||||
error: "URI not supported",
|
||||
};
|
||||
}
|
||||
|
||||
let proposalId: number;
|
||||
let checkResult: CheckPayResult;
|
||||
try {
|
||||
proposalId = await this.downloadProposal(url, downloadSessionId);
|
||||
proposalId = await this.downloadProposal(
|
||||
uriResult.downloadUrl,
|
||||
uriResult.sessionId,
|
||||
);
|
||||
checkResult = await this.checkPay(proposalId);
|
||||
} catch (e) {
|
||||
return {
|
||||
@ -736,6 +745,27 @@ export class Wallet {
|
||||
if (!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") {
|
||||
return {
|
||||
status: "paid",
|
||||
@ -1139,21 +1169,78 @@ export class Wallet {
|
||||
const op = openPromise<void>();
|
||||
|
||||
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 {
|
||||
const reserve = await this.updateReserve(reservePub);
|
||||
await this.depleteReserve(reserve);
|
||||
const reserve = await this.q().get<ReserveRecord>(
|
||||
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();
|
||||
} catch (e) {
|
||||
// random, exponential backoff truncated at 3 minutes
|
||||
if (isHardError) {
|
||||
op.reject(e);
|
||||
}
|
||||
const nextDelay = Math.min(
|
||||
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, () =>
|
||||
processReserveInternal(nextDelay),
|
||||
);
|
||||
@ -1346,7 +1433,10 @@ export class Wallet {
|
||||
reserve_pub: keypair.pub,
|
||||
senderWire: req.senderWire,
|
||||
timestamp_confirmed: 0,
|
||||
timestamp_reserve_info_posted: 0,
|
||||
timestamp_depleted: 0,
|
||||
bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
|
||||
exchangeWire: req.exchangeWire,
|
||||
};
|
||||
|
||||
const senderWire = req.senderWire;
|
||||
@ -1387,6 +1477,10 @@ export class Wallet {
|
||||
.put(Stores.reserves, reserveRecord)
|
||||
.finish();
|
||||
|
||||
if (req.bankWithdrawStatusUrl) {
|
||||
this.processReserve(keypair.pub);
|
||||
}
|
||||
|
||||
const r: CreateReserveResponse = {
|
||||
exchange: canonExchange,
|
||||
reservePub: keypair.pub,
|
||||
@ -1513,6 +1607,7 @@ export class Wallet {
|
||||
}
|
||||
|
||||
const preCoin = await this.cryptoApi.createPreCoin(denom, reserve);
|
||||
|
||||
// This will fail and throw an exception if the remaining amount in the
|
||||
// reserve is too low to create a pre-coin.
|
||||
try {
|
||||
@ -1520,6 +1615,7 @@ export class Wallet {
|
||||
.put(Stores.precoins, preCoin)
|
||||
.mutate(Stores.reserves, reserve.reserve_pub, mutateReserve)
|
||||
.finish();
|
||||
console.log("created precoin", preCoin.coinPub);
|
||||
} catch (e) {
|
||||
console.log("can't create pre-coin:", e.name, e.message);
|
||||
return;
|
||||
@ -1542,6 +1638,11 @@ export class Wallet {
|
||||
if (!reserve) {
|
||||
throw Error("reserve not in db");
|
||||
}
|
||||
|
||||
if (reserve.timestamp_confirmed === 0) {
|
||||
throw Error("");
|
||||
}
|
||||
|
||||
const reqUrl = new URI("reserve/status").absoluteTo(
|
||||
reserve.exchange_base_url,
|
||||
);
|
||||
@ -2462,7 +2563,14 @@ export class Wallet {
|
||||
refreshSession.exchangeBaseUrl,
|
||||
);
|
||||
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("reveal response:", resp);
|
||||
@ -3427,6 +3535,57 @@ export class Wallet {
|
||||
// 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 {
|
||||
this.badge.clearNotification();
|
||||
}
|
||||
|
@ -324,6 +324,13 @@ export class CreateReserveRequest {
|
||||
@Checkable.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
|
||||
* the exchange.
|
||||
@ -331,6 +338,12 @@ export class CreateReserveRequest {
|
||||
@Checkable.Optional(Checkable.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
|
||||
* member.
|
||||
@ -474,9 +487,20 @@ export interface NextUrlResult {
|
||||
}
|
||||
|
||||
export interface PreparePayResult {
|
||||
status: "paid" | "insufficient-balance" | "payment-possible" | "error";
|
||||
status: "paid" | "session-replayed" | "insufficient-balance" | "payment-possible" | "error";
|
||||
contractTerms?: ContractTerms;
|
||||
error?: string;
|
||||
proposalId?: number;
|
||||
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/query.ts",
|
||||
"src/talerTypes.ts",
|
||||
"src/taleruri-test.ts",
|
||||
"src/taleruri.ts",
|
||||
"src/timer.ts",
|
||||
"src/types-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:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
idb-bridge@^0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.7.tgz#34705d79ab992c4b5d5fa048c313ac5f803a972f"
|
||||
integrity sha512-vGTYbX6ni8h/6B2POS6f1Nuzp47+Tna5MggQKDmQp1MLCPFbI8RXBzs1rvKWuMx+WKX3LtMWbxSm8hQBnI9DLA==
|
||||
idb-bridge@^0.0.9:
|
||||
version "0.0.9"
|
||||
resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.9.tgz#28c9a9e50b275dc80316b29bdaec536ee96bc65b"
|
||||
integrity sha512-MOoiDJvbhskEzyDtbOz9ecfGZ2Jx0CZ6L81h71fPjdERCkUUqvkt6p7RkChPr8GmteIfja2k9mIRNFkS7enmrQ==
|
||||
|
||||
ieee754@^1.1.4:
|
||||
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"
|
||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
||||
|
||||
tslint@^5.14.0:
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.18.0.tgz#f61a6ddcf372344ac5e41708095bbf043a147ac6"
|
||||
integrity sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w==
|
||||
tslint@^5.19.0:
|
||||
version "5.19.0"
|
||||
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.19.0.tgz#a2cbd4a7699386da823f6b499b8394d6c47bb968"
|
||||
integrity sha512-1LwwtBxfRJZnUvoS9c0uj8XQtAnyhWr9KlNvDIdB+oXyT+VpsOAaEhEgKi1HrZ8rq0ki/AAnbGSv4KM6/AfVZw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
builtin-modules "^1.1.1"
|
||||
|
Loading…
Reference in New Issue
Block a user