nicer HTTP helper in preparation for better error handling
This commit is contained in:
parent
5a8931d903
commit
dd2efc3d78
@ -1,8 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
function setup_config() {
|
set -eu
|
||||||
set -eu
|
|
||||||
|
|
||||||
|
function setup_config() {
|
||||||
echo -n "Testing for taler-bank-manage"
|
echo -n "Testing for taler-bank-manage"
|
||||||
taler-bank-manage -h >/dev/null </dev/null || exit_skip " MISSING"
|
taler-bank-manage -h >/dev/null </dev/null || exit_skip " MISSING"
|
||||||
echo " FOUND"
|
echo " FOUND"
|
||||||
@ -135,11 +135,31 @@ function wait_for_services() {
|
|||||||
echo " DONE"
|
echo " DONE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Configure merchant instances
|
||||||
|
function configure_merchant() {
|
||||||
|
json='
|
||||||
|
{
|
||||||
|
"id": "default",
|
||||||
|
"name": "GNU Taler Merchant",
|
||||||
|
"payto_uris": ["payto://x-taler-bank/test_merchant"],
|
||||||
|
"address": {},
|
||||||
|
"jurisdiction": {},
|
||||||
|
"default_max_wire_fee": "TESTKUDOS:1",
|
||||||
|
"default_wire_fee_amortization": 3,
|
||||||
|
"default_max_deposit_fee": "TESTKUDOS:1",
|
||||||
|
"default_wire_transfer_delay": {"d_ms": "forever"},
|
||||||
|
"default_pay_delay": {"d_ms": "forever"}
|
||||||
|
}
|
||||||
|
'
|
||||||
|
curl -v -XPOST --data "$json" "${MERCHANT_URL}private/instances"
|
||||||
|
}
|
||||||
|
|
||||||
function normal_start_and_wait() {
|
function normal_start_and_wait() {
|
||||||
setup_config "$1"
|
setup_config "$1"
|
||||||
setup_services
|
setup_services
|
||||||
launch_services
|
launch_services
|
||||||
wait_for_services
|
wait_for_services
|
||||||
|
configure_merchant
|
||||||
}
|
}
|
||||||
|
|
||||||
# provide the service URL as first parameter
|
# provide the service URL as first parameter
|
||||||
|
@ -37,9 +37,8 @@ export class MerchantBackendConnection {
|
|||||||
reason: string,
|
reason: string,
|
||||||
refundAmount: string,
|
refundAmount: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const reqUrl = new URL("refund", this.merchantBaseUrl);
|
const reqUrl = new URL(`private/orders/${orderId}/refund`, this.merchantBaseUrl);
|
||||||
const refundReq = {
|
const refundReq = {
|
||||||
order_id: orderId,
|
|
||||||
reason,
|
reason,
|
||||||
refund: refundAmount,
|
refund: refundAmount,
|
||||||
};
|
};
|
||||||
@ -66,10 +65,11 @@ export class MerchantBackendConnection {
|
|||||||
constructor(public merchantBaseUrl: string, public apiKey: string) {}
|
constructor(public merchantBaseUrl: string, public apiKey: string) {}
|
||||||
|
|
||||||
async authorizeTip(amount: string, justification: string): Promise<string> {
|
async authorizeTip(amount: string, justification: string): Promise<string> {
|
||||||
const reqUrl = new URL("tip-authorize", this.merchantBaseUrl).href;
|
const reqUrl = new URL("private/tips", this.merchantBaseUrl).href;
|
||||||
const tipReq = {
|
const tipReq = {
|
||||||
amount,
|
amount,
|
||||||
justification,
|
justification,
|
||||||
|
next_url: "about:blank",
|
||||||
};
|
};
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
@ -93,7 +93,7 @@ export class MerchantBackendConnection {
|
|||||||
fulfillmentUrl: string,
|
fulfillmentUrl: string,
|
||||||
): Promise<{ orderId: string }> {
|
): Promise<{ orderId: string }> {
|
||||||
const t = Math.floor(new Date().getTime() / 1000) + 15 * 60;
|
const t = Math.floor(new Date().getTime() / 1000) + 15 * 60;
|
||||||
const reqUrl = new URL("order", this.merchantBaseUrl).href;
|
const reqUrl = new URL("private/orders", this.merchantBaseUrl).href;
|
||||||
const orderReq = {
|
const orderReq = {
|
||||||
order: {
|
order: {
|
||||||
amount,
|
amount,
|
||||||
@ -123,11 +123,10 @@ export class MerchantBackendConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async checkPayment(orderId: string): Promise<CheckPaymentResponse> {
|
async checkPayment(orderId: string): Promise<CheckPaymentResponse> {
|
||||||
const reqUrl = new URL("check-payment", this.merchantBaseUrl).href;
|
const reqUrl = new URL(`private/orders/${orderId}`, this.merchantBaseUrl).href;
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: reqUrl,
|
url: reqUrl,
|
||||||
params: { order_id: orderId },
|
|
||||||
responseType: "json",
|
responseType: "json",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `ApiKey ${this.apiKey}`,
|
Authorization: `ApiKey ${this.apiKey}`,
|
||||||
|
@ -53,64 +53,6 @@ export class OperationFailedError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Process an HTTP response that we expect to contain Taler-specific JSON.
|
|
||||||
*
|
|
||||||
* Depending on the status code, we throw an exception. This function
|
|
||||||
* will try to extract Taler-specific error information from the HTTP response
|
|
||||||
* if possible.
|
|
||||||
*/
|
|
||||||
export async function scrutinizeTalerJsonResponse<T>(
|
|
||||||
resp: HttpResponse,
|
|
||||||
codec: Codec<T>,
|
|
||||||
): Promise<T> {
|
|
||||||
// FIXME: We should distinguish between different types of error status
|
|
||||||
// to react differently (throttle, report permanent failure)
|
|
||||||
|
|
||||||
// FIXME: Make sure that when we receive an error message,
|
|
||||||
// it looks like a Taler error message
|
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
|
||||||
let exc: OperationFailedError | undefined = undefined;
|
|
||||||
try {
|
|
||||||
const errorJson = await resp.json();
|
|
||||||
const m = `received error response (status ${resp.status})`;
|
|
||||||
exc = new OperationFailedError({
|
|
||||||
type: "protocol",
|
|
||||||
message: m,
|
|
||||||
details: {
|
|
||||||
httpStatusCode: resp.status,
|
|
||||||
errorResponse: errorJson,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
const m = "could not parse response JSON";
|
|
||||||
exc = new OperationFailedError({
|
|
||||||
type: "network",
|
|
||||||
message: m,
|
|
||||||
details: {
|
|
||||||
status: resp.status,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw exc;
|
|
||||||
}
|
|
||||||
let json: any;
|
|
||||||
try {
|
|
||||||
json = await resp.json();
|
|
||||||
} catch (e) {
|
|
||||||
const m = "could not parse response JSON";
|
|
||||||
throw new OperationFailedError({
|
|
||||||
type: "network",
|
|
||||||
message: m,
|
|
||||||
details: {
|
|
||||||
status: resp.status,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return codec.decode(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run an operation and call the onOpError callback
|
* Run an operation and call the onOpError callback
|
||||||
* when there was an exception or operation error that must be reported.
|
* when there was an exception or operation error that must be reported.
|
||||||
|
@ -52,12 +52,13 @@ import {
|
|||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import { AmountJson } from "../util/amounts";
|
import { AmountJson } from "../util/amounts";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { getOrderDownloadUrl, parsePayUri } from "../util/taleruri";
|
import { parsePayUri } from "../util/taleruri";
|
||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { getTimestampNow, timestampAddDuration } from "../util/time";
|
import { getTimestampNow, timestampAddDuration } from "../util/time";
|
||||||
import { strcmp, canonicalJson } from "../util/helpers";
|
import { strcmp, canonicalJson } from "../util/helpers";
|
||||||
|
import { httpPostTalerJson } from "../util/http";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -534,7 +535,9 @@ async function incrementProposalRetry(
|
|||||||
pr.lastError = err;
|
pr.lastError = err;
|
||||||
await tx.put(Stores.proposals, pr);
|
await tx.put(Stores.proposals, pr);
|
||||||
});
|
});
|
||||||
ws.notify({ type: NotificationType.ProposalOperationError });
|
if (err) {
|
||||||
|
ws.notify({ type: NotificationType.ProposalOperationError, error: err });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function incrementPurchasePayRetry(
|
async function incrementPurchasePayRetry(
|
||||||
@ -600,25 +603,25 @@ async function processDownloadProposalImpl(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedUrl = new URL(
|
const orderClaimUrl = new URL(
|
||||||
getOrderDownloadUrl(proposal.merchantBaseUrl, proposal.orderId),
|
`orders/${proposal.orderId}/claim`,
|
||||||
);
|
proposal.merchantBaseUrl,
|
||||||
parsedUrl.searchParams.set("nonce", proposal.noncePub);
|
).href;
|
||||||
const urlWithNonce = parsedUrl.href;
|
logger.trace("downloading contract from '" + orderClaimUrl + "'");
|
||||||
console.log("downloading contract from '" + urlWithNonce + "'");
|
|
||||||
let resp;
|
|
||||||
try {
|
|
||||||
resp = await ws.http.get(urlWithNonce);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("contract download failed", e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
|
||||||
throw Error(`contract download failed with status ${resp.status}`);
|
const proposalResp = await httpPostTalerJson({
|
||||||
}
|
url: orderClaimUrl,
|
||||||
|
body: {
|
||||||
|
nonce: proposal.noncePub,
|
||||||
|
},
|
||||||
|
codec: codecForProposal(),
|
||||||
|
http: ws.http,
|
||||||
|
});
|
||||||
|
|
||||||
const proposalResp = codecForProposal().decode(await resp.json());
|
// The proposalResp contains the contract terms as raw JSON,
|
||||||
|
// as the coded to parse them doesn't necessarily round-trip.
|
||||||
|
// We need this raw JSON to compute the contract terms hash.
|
||||||
|
|
||||||
const contractTermsHash = await ws.cryptoApi.hashString(
|
const contractTermsHash = await ws.cryptoApi.hashString(
|
||||||
canonicalJson(proposalResp.contract_terms),
|
canonicalJson(proposalResp.contract_terms),
|
||||||
|
@ -48,7 +48,8 @@ import { RefreshReason, OperationError } from "../types/walletTypes";
|
|||||||
import { TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow } from "../util/time";
|
||||||
import { guardOperationException, scrutinizeTalerJsonResponse } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
|
import { httpPostTalerJson } from "../util/http";
|
||||||
|
|
||||||
async function incrementRecoupRetry(
|
async function incrementRecoupRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -146,11 +147,12 @@ async function recoupWithdrawCoin(
|
|||||||
|
|
||||||
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
|
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
|
||||||
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
||||||
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
const recoupConfirmation = await httpPostTalerJson({
|
||||||
const recoupConfirmation = await scrutinizeTalerJsonResponse(
|
url: reqUrl.href,
|
||||||
resp,
|
body: recoupRequest,
|
||||||
codecForRecoupConfirmation(),
|
codec: codecForRecoupConfirmation(),
|
||||||
);
|
http: ws.http,
|
||||||
|
});
|
||||||
|
|
||||||
if (recoupConfirmation.reserve_pub !== reservePub) {
|
if (recoupConfirmation.reserve_pub !== reservePub) {
|
||||||
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
||||||
@ -220,11 +222,13 @@ async function recoupRefreshCoin(
|
|||||||
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
|
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
|
||||||
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
||||||
console.log("making recoup request");
|
console.log("making recoup request");
|
||||||
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
|
||||||
const recoupConfirmation = await scrutinizeTalerJsonResponse(
|
const recoupConfirmation = await httpPostTalerJson({
|
||||||
resp,
|
url: reqUrl.href,
|
||||||
codecForRecoupConfirmation(),
|
body: recoupRequest,
|
||||||
);
|
codec: codecForRecoupConfirmation(),
|
||||||
|
http: ws.http,
|
||||||
|
});
|
||||||
|
|
||||||
if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
|
if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
|
||||||
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
||||||
|
@ -46,7 +46,7 @@ import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
|
|||||||
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions";
|
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions";
|
||||||
|
|
||||||
import * as LibtoolVersion from "../util/libtoolVersion";
|
import * as LibtoolVersion from "../util/libtoolVersion";
|
||||||
import { guardOperationException, scrutinizeTalerJsonResponse } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import {
|
import {
|
||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
@ -54,6 +54,7 @@ import {
|
|||||||
timestampCmp,
|
timestampCmp,
|
||||||
timestampSubtractDuraction,
|
timestampSubtractDuraction,
|
||||||
} from "../util/time";
|
} from "../util/time";
|
||||||
|
import { httpPostTalerJson } from "../util/http";
|
||||||
|
|
||||||
const logger = new Logger("withdraw.ts");
|
const logger = new Logger("withdraw.ts");
|
||||||
|
|
||||||
@ -308,8 +309,13 @@ async function processPlanchet(
|
|||||||
`reserves/${planchet.reservePub}/withdraw`,
|
`reserves/${planchet.reservePub}/withdraw`,
|
||||||
exchange.baseUrl,
|
exchange.baseUrl,
|
||||||
).href;
|
).href;
|
||||||
const resp = await ws.http.postJson(reqUrl, wd);
|
|
||||||
const r = await scrutinizeTalerJsonResponse(resp, codecForWithdrawResponse());
|
const r = await httpPostTalerJson({
|
||||||
|
url: reqUrl,
|
||||||
|
body: wd,
|
||||||
|
codec: codecForWithdrawResponse(),
|
||||||
|
http: ws.http,
|
||||||
|
});
|
||||||
|
|
||||||
logger.trace(`got response for /withdraw`);
|
logger.trace(`got response for /withdraw`);
|
||||||
|
|
||||||
|
@ -168,6 +168,7 @@ export interface PayOperationErrorNotification {
|
|||||||
|
|
||||||
export interface ProposalOperationErrorNotification {
|
export interface ProposalOperationErrorNotification {
|
||||||
type: NotificationType.ProposalOperationError;
|
type: NotificationType.ProposalOperationError;
|
||||||
|
error: OperationError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TipOperationErrorNotification {
|
export interface TipOperationErrorNotification {
|
||||||
|
115
src/util/http.ts
115
src/util/http.ts
@ -14,6 +14,9 @@
|
|||||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Codec } from "./codec";
|
||||||
|
import { OperationFailedError } from "../operations/errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
|
* Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
|
||||||
* Allows for easy mocking for test cases.
|
* Allows for easy mocking for test cases.
|
||||||
@ -172,3 +175,115 @@ export class BrowserHttpLib implements HttpRequestLibrary {
|
|||||||
// Nothing to do
|
// Nothing to do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PostJsonRequest<RespType> {
|
||||||
|
http: HttpRequestLibrary;
|
||||||
|
url: string;
|
||||||
|
body: any;
|
||||||
|
codec: Codec<RespType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for making Taler-style HTTP POST requests with a JSON payload and response.
|
||||||
|
*/
|
||||||
|
export async function httpPostTalerJson<RespType>(
|
||||||
|
req: PostJsonRequest<RespType>,
|
||||||
|
): Promise<RespType> {
|
||||||
|
const resp = await req.http.postJson(req.url, req.body);
|
||||||
|
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
let exc: OperationFailedError | undefined = undefined;
|
||||||
|
try {
|
||||||
|
const errorJson = await resp.json();
|
||||||
|
const m = `received error response (status ${resp.status})`;
|
||||||
|
exc = new OperationFailedError({
|
||||||
|
type: "protocol",
|
||||||
|
message: m,
|
||||||
|
details: {
|
||||||
|
httpStatusCode: resp.status,
|
||||||
|
errorResponse: errorJson,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const m = "could not parse response JSON";
|
||||||
|
exc = new OperationFailedError({
|
||||||
|
type: "network",
|
||||||
|
message: m,
|
||||||
|
details: {
|
||||||
|
status: resp.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw exc;
|
||||||
|
}
|
||||||
|
let json: any;
|
||||||
|
try {
|
||||||
|
json = await resp.json();
|
||||||
|
} catch (e) {
|
||||||
|
const m = "could not parse response JSON";
|
||||||
|
throw new OperationFailedError({
|
||||||
|
type: "network",
|
||||||
|
message: m,
|
||||||
|
details: {
|
||||||
|
status: resp.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return req.codec.decode(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface GetJsonRequest<RespType> {
|
||||||
|
http: HttpRequestLibrary;
|
||||||
|
url: string;
|
||||||
|
codec: Codec<RespType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for making Taler-style HTTP GET requests with a JSON payload.
|
||||||
|
*/
|
||||||
|
export async function httpGetTalerJson<RespType>(
|
||||||
|
req: GetJsonRequest<RespType>,
|
||||||
|
): Promise<RespType> {
|
||||||
|
const resp = await req.http.get(req.url);
|
||||||
|
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
let exc: OperationFailedError | undefined = undefined;
|
||||||
|
try {
|
||||||
|
const errorJson = await resp.json();
|
||||||
|
const m = `received error response (status ${resp.status})`;
|
||||||
|
exc = new OperationFailedError({
|
||||||
|
type: "protocol",
|
||||||
|
message: m,
|
||||||
|
details: {
|
||||||
|
httpStatusCode: resp.status,
|
||||||
|
errorResponse: errorJson,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const m = "could not parse response JSON";
|
||||||
|
exc = new OperationFailedError({
|
||||||
|
type: "network",
|
||||||
|
message: m,
|
||||||
|
details: {
|
||||||
|
status: resp.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw exc;
|
||||||
|
}
|
||||||
|
let json: any;
|
||||||
|
try {
|
||||||
|
json = await resp.json();
|
||||||
|
} catch (e) {
|
||||||
|
const m = "could not parse response JSON";
|
||||||
|
throw new OperationFailedError({
|
||||||
|
type: "network",
|
||||||
|
message: m,
|
||||||
|
details: {
|
||||||
|
status: resp.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return req.codec.decode(json);
|
||||||
|
}
|
||||||
|
@ -97,15 +97,6 @@ export function classifyTalerUri(s: string): TalerUriType {
|
|||||||
return TalerUriType.Unknown;
|
return TalerUriType.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOrderDownloadUrl(
|
|
||||||
merchantBaseUrl: string,
|
|
||||||
orderId: string,
|
|
||||||
): string {
|
|
||||||
const u = new URL("proposal", merchantBaseUrl);
|
|
||||||
u.searchParams.set("order_id", orderId);
|
|
||||||
return u.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parsePayUri(s: string): PayUriResult | undefined {
|
export function parsePayUri(s: string): PayUriResult | undefined {
|
||||||
const pfx = "taler://pay/";
|
const pfx = "taler://pay/";
|
||||||
if (!s.toLowerCase().startsWith(pfx)) {
|
if (!s.toLowerCase().startsWith(pfx)) {
|
||||||
@ -133,7 +124,7 @@ export function parsePayUri(s: string): PayUriResult | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (maybePath === "-") {
|
if (maybePath === "-") {
|
||||||
maybePath = "public/";
|
maybePath = "";
|
||||||
} else {
|
} else {
|
||||||
maybePath = decodeURIComponent(maybePath) + "/";
|
maybePath = decodeURIComponent(maybePath) + "/";
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user