commit cb3be034e3b6bd7422be8961485a6085d16c7190
parent 9f889207ddad3b786e4b30ea076f3ec24591a9da
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 1 Oct 2025 17:55:06 -0300
fixes #8526
Diffstat:
6 files changed, 1499 insertions(+), 517 deletions(-)
diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts
@@ -485,7 +485,7 @@ export class Amounts {
}
const number = s.substring(c_idx + 1);
const d_idx = number.indexOf(FRAC_SEPARATOR);
- const integerStr = number.substring(0, d_idx);
+ const integerStr = d_idx === -1 ? number : number.substring(0, d_idx);
const fractStr =
d_idx === -1 || d_idx === number.length
? "0"
@@ -499,7 +499,7 @@ export class Amounts {
const fraction = Math.round(
amountFractionalBase * Number.parseFloat(FRAC_SEPARATOR + fractStr),
);
- if (Number.isInteger(value) || Number.isInteger(fraction)) {
+ if (!Number.isInteger(value) || !Number.isInteger(fraction)) {
return opKnownFailure(AmountParseError.BAD_NUMBER);
}
if (value > amountMaxValue) {
@@ -650,7 +650,7 @@ export class Amounts {
return `${a.currency}:${s}` as AmountString;
}
-
+
/**
* Show an amount in a form suitable for the user.
* FIXME: In the future, this should consider currency-specific
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
@@ -231,19 +231,27 @@ export namespace Paytos {
*
* Return the canonical form.
*
+ * FIXME: new need a function that only takes una string: host+port+path and
+ * parse it without using URL to prevent parsing unnecesary components and
+ * better error reporting
+ *
+ *
* @param hostname
* @param path
* @param scheme
* @returns
*/
- export function parseHostPortPath(
+ export function parseHostPortPath2(
hostname: string,
- path: string,
+ path: string | undefined,
scheme: "http" | "https" = "https",
): HostPortPath | undefined {
// maybe it should check that it doesn't contain search or hash?
try {
// https://url.spec.whatwg.org/#concept-basic-url-parser
+ if (path === undefined) {
+ path = ""
+ }
if (!path.endsWith("/")) {
path = path + "/";
}
@@ -254,10 +262,24 @@ export namespace Paytos {
url.hash = "";
return url.href as HostPortPath;
} catch (e) {
+ console.log(e)
return undefined;
}
}
/**
+ * Same as `parseHostPortPath2` but only takes one string.
+ * This should be the definitive signature.
+ *
+ * @param hostnameAndPath
+ * @returns
+ */
+ export function parseHostPortPath(
+ hostnameAndPath: string,
+ ): HostPortPath | undefined {
+ const [host, path] = hostnameAndPath.split("/", 1);
+ return parseHostPortPath2(host, path ?? "");
+ }
+ /**
* FIXME: add ethereum address validator
* @param str
*/
@@ -510,7 +532,7 @@ export namespace Paytos {
});
}
- const host = parseHostPortPath(cs[0], cs.slice(1, -1).join("/"));
+ const host = parseHostPortPath2(cs[0], cs.slice(1, -1).join("/"));
if (!opts.ignoreComponentError && !host) {
return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
pos: 0 as const,
@@ -541,7 +563,7 @@ export namespace Paytos {
targetType,
});
}
- const exchange = parseHostPortPath(cs[0], cs.slice(1, -1).join("/"));
+ const exchange = parseHostPortPath2(cs[0], cs.slice(1, -1).join("/"));
if (!opts.ignoreComponentError && !exchange) {
return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
pos: 0 as const,
@@ -574,7 +596,7 @@ export namespace Paytos {
targetType,
});
}
- const exchange = parseHostPortPath(
+ const exchange = parseHostPortPath2(
cs[0],
cs.slice(1, -1).join("/"),
"http",
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
@@ -16,7 +16,9 @@
import test from "ava";
// import { AmountString } from "./types-taler-common.js";
+import { HostPortPath } from "./payto.js";
import {
+ // TalerUris,
parseAddExchangeUri,
parseDevExperimentUri,
parsePayPullUri,
@@ -36,524 +38,581 @@ import {
stringifyRefundUri,
stringifyRestoreUri,
stringifyWithdrawExchange,
- stringifyWithdrawUri,
+ stringifyWithdrawUri
} from "./taleruri.js";
-// import { HostPortPath } from "./payto.js";
+import { AmountString } from "./types-taler-common.js";
-/**
- * 5.1 action: withdraw https://lsd.gnunet.org/lsd0006/#name-action-withdraw
- */
+{
+ /**
+ * 5.1 action: withdraw https://lsd.gnunet.org/lsd0006/#name-action-withdraw
+ */
-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.withdrawalOperationId, "12345");
- t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/" as any);
-});
-
-test("taler withdraw uri parsing with external confirmation", (t) => {
- const url1 = "taler://withdraw/bank.example.com/12345?external-confirmation=1";
- const r1 = parseWithdrawUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.externalConfirmation, true);
- t.is(r1.withdrawalOperationId, "12345");
- t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/" as any);
-});
-
-test("taler withdraw uri parsing (http)", (t) => {
- const url1 = "taler+http://withdraw/bank.example.com/12345";
- const r1 = parseWithdrawUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.withdrawalOperationId, "12345");
- t.is(r1.bankIntegrationApiBaseUrl, "http://bank.example.com/" as any);
-});
-
-test("taler withdraw URI (stringify)", (t) => {
- const url = stringifyWithdrawUri({
- bankIntegrationApiBaseUrl: "https://bank.taler.test/integration-api/" as any,
- withdrawalOperationId: "123",
- });
- t.deepEqual(url, "taler://withdraw/bank.taler.test/integration-api/123");
-});
-
-/**
- * 5.2 action: pay https://lsd.gnunet.org/lsd0006/#name-action-pay
- */
-test("taler pay url parsing: defaults", (t) => {
- const url1 = "taler://pay/example.com/myorder/";
- const r1 = parsePayUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.merchantBaseUrl, "https://example.com/" as any);
- t.is(r1.sessionId, "");
-
- const url2 = "taler://pay/example.com/myorder/mysession";
- const r2 = parsePayUri(url2);
- if (!r2) {
- t.fail();
- return;
- }
- t.is(r2.merchantBaseUrl, "https://example.com/" as any);
- t.is(r2.sessionId, "mysession");
-});
-
-test("taler pay url parsing: instance", (t) => {
- const url1 = "taler://pay/example.com/instances/myinst/myorder/";
- const r1 = parsePayUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.merchantBaseUrl, "https://example.com/instances/myinst/" as any);
- t.is(r1.orderId, "myorder");
-});
-
-test("taler pay url parsing (claim token)", (t) => {
- const url1 = "taler://pay/example.com/instances/myinst/myorder/?c=ASDF";
- const r1 = parsePayUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.merchantBaseUrl, "https://example.com/instances/myinst/" as any);
- t.is(r1.orderId, "myorder");
- t.is(r1.claimToken, "ASDF");
-});
-
-test("taler pay uri parsing: non-https", (t) => {
- const url1 = "taler+http://pay/example.com/myorder/";
- const r1 = parsePayUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.merchantBaseUrl, "http://example.com/" as any);
- t.is(r1.orderId, "myorder");
-});
-
-test("taler pay uri parsing: missing session component", (t) => {
- const url1 = "taler+http://pay/example.com/myorder";
- const r1 = parsePayUri(url1);
- if (r1) {
- t.fail();
- return;
- }
- t.pass();
-});
-
-test("taler pay URI (stringify)", (t) => {
- const url1 = stringifyPayUri({
- merchantBaseUrl: "http://localhost:123/" as any,
- orderId: "foo",
- sessionId: "",
- });
- t.deepEqual(url1, "taler+http://pay/localhost:123/foo/");
-
- const url2 = stringifyPayUri({
- merchantBaseUrl: "http://localhost:123/" as any,
- orderId: "foo",
- sessionId: "bla",
- });
- t.deepEqual(url2, "taler+http://pay/localhost:123/foo/bla");
-});
-
-test("taler pay URI (stringify with https)", (t) => {
- const url1 = stringifyPayUri({
- merchantBaseUrl: "https://localhost:123/" as any,
- orderId: "foo",
- sessionId: "",
- });
- t.deepEqual(url1, "taler://pay/localhost:123/foo/");
-
- const url2 = stringifyPayUri({
- merchantBaseUrl: "https://localhost/" as any,
- orderId: "foo",
- sessionId: "bla",
- noncePriv: "123",
- });
- t.deepEqual(url2, "taler://pay/localhost/foo/bla?n=123");
-});
-
-/**
- * 5.3 action: refund https://lsd.gnunet.org/lsd0006/#name-action-refund
- */
+ 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.withdrawalOperationId, "12345");
+ t.is(
+ r1.bankIntegrationApiBaseUrl,
+ "https://bank.example.com/" as HostPortPath,
+ );
+ });
-test("taler refund uri parsing: non-https #1", (t) => {
- const url1 = "taler+http://refund/example.com/myorder/";
- const r1 = parseRefundUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.merchantBaseUrl, "http://example.com/" as any);
- t.is(r1.orderId, "myorder");
-});
-
-test("taler refund uri parsing", (t) => {
- const url1 = "taler://refund/merchant.example.com/1234/";
- const r1 = parseRefundUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.merchantBaseUrl, "https://merchant.example.com/" as any);
- t.is(r1.orderId, "1234");
-});
-
-test("taler refund uri parsing with instance", (t) => {
- const url1 = "taler://refund/merchant.example.com/instances/myinst/1234/";
- const r1 = parseRefundUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.orderId, "1234");
- t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/myinst/" as any);
-});
-
-test("taler refund URI (stringify)", (t) => {
- const url = stringifyRefundUri({
- merchantBaseUrl: "https://merchant.test/instance/pepe/" as any,
- orderId: "123",
- });
- t.deepEqual(url, "taler://refund/merchant.test/instance/pepe/123/");
-});
-
-/**
- * 5.5 action: pay-push https://lsd.gnunet.org/lsd0006/#name-action-pay-push
- */
+ test("taler withdraw uri parsing with external confirmation", (t) => {
+ const url1 =
+ "taler://withdraw/bank.example.com/12345?external-confirmation=1";
+ const r1 = parseWithdrawUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.externalConfirmation, true);
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(
+ r1.bankIntegrationApiBaseUrl,
+ "https://bank.example.com/" as HostPortPath,
+ );
+ });
-test("taler peer to peer push URI", (t) => {
- const url1 = "taler://pay-push/exch.example.com/foo";
- const r1 = parsePayPushUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.exchangeBaseUrl, "https://exch.example.com/" as any);
- t.is(r1.contractPriv, "foo");
-});
-
-test("taler peer to peer push URI (path)", (t) => {
- const url1 = "taler://pay-push/exch.example.com:123/bla/foo";
- const r1 = parsePayPushUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.exchangeBaseUrl, "https://exch.example.com:123/bla/" as any);
- t.is(r1.contractPriv, "foo");
-});
-
-test("taler peer to peer push URI (http)", (t) => {
- const url1 = "taler+http://pay-push/exch.example.com:123/bla/foo";
- const r1 = parsePayPushUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.exchangeBaseUrl, "http://exch.example.com:123/bla/" as any);
- t.is(r1.contractPriv, "foo");
-});
-
-test("taler peer to peer push URI (stringify)", (t) => {
- const url = stringifyPayPushUri({
- exchangeBaseUrl: "https://foo.example.com/bla/" as any,
- contractPriv: "123",
- });
- t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123");
-});
-
-/**
- * 5.6 action: pay-pull https://lsd.gnunet.org/lsd0006/#name-action-pay-pull
- */
+ test("taler withdraw uri parsing (http)", (t) => {
+ const url1 = "taler+http://withdraw/bank.example.com/12345";
+ const r1 = parseWithdrawUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(
+ r1.bankIntegrationApiBaseUrl,
+ "http://bank.example.com/" as HostPortPath,
+ );
+ });
-test("taler peer to peer pull URI", (t) => {
- const url1 = "taler://pay-pull/exch.example.com/foo";
- const r1 = parsePayPullUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.exchangeBaseUrl, "https://exch.example.com/" as any);
- t.is(r1.contractPriv, "foo");
-});
-
-test("taler peer to peer pull URI (path)", (t) => {
- const url1 = "taler://pay-pull/exch.example.com:123/bla/foo";
- const r1 = parsePayPullUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.exchangeBaseUrl, "https://exch.example.com:123/bla/" as any);
- t.is(r1.contractPriv, "foo");
-});
-
-test("taler peer to peer pull URI (http)", (t) => {
- const url1 = "taler+http://pay-pull/exch.example.com:123/bla/foo";
- const r1 = parsePayPullUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.exchangeBaseUrl, "http://exch.example.com:123/bla/" as any);
- t.is(r1.contractPriv, "foo");
-});
-
-test("taler peer to peer pull URI (stringify)", (t) => {
- const url = stringifyPayPullUri({
- exchangeBaseUrl: "https://foo.example.com/bla/" as any,
- contractPriv: "123",
- });
- t.deepEqual(url, "taler://pay-pull/foo.example.com/bla/123");
-});
-
-/**
- * 5.7 action: pay-template https://lsd.gnunet.org/lsd0006/#name-action-pay-template
- */
+ test("taler withdraw URI (stringify)", (t) => {
+ const url = stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl:
+ "https://bank.taler.test/integration-api/" as HostPortPath,
+ withdrawalOperationId: "123",
+ });
+ t.deepEqual(url, "taler://withdraw/bank.taler.test/integration-api/123");
+ });
-test("taler pay template URI (parsing)", (t) => {
- const url1 =
- "taler://pay-template/merchant.example.com/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY";
- const r1 = parsePayTemplateUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.deepEqual(r1.merchantBaseUrl, "https://merchant.example.com/" as any);
- t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
-});
-
-test("taler pay template URI (parsing, http with port)", (t) => {
- const url1 =
- "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY";
- const r1 = parsePayTemplateUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.deepEqual(r1.merchantBaseUrl, "http://merchant.example.com:1234/" as any);
- t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
-});
-
-test("taler pay template URI (stringify)", (t) => {
- const url1 = stringifyPayTemplateUri({
- merchantBaseUrl: "http://merchant.example.com:1234/" as any,
- templateId: "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY",
- });
- t.deepEqual(
- url1,
- "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY",
- );
-});
-
-/**
- * 5.10 action: restore https://lsd.gnunet.org/lsd0006/#name-action-restore
- */
-test("taler restore URI (parsing, http with port)", (t) => {
- const r1 = parseRestoreUri(
- "taler+http://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:123",
- );
- if (!r1) {
- t.fail();
- return;
- }
- t.deepEqual(
- r1.walletRootPriv,
- "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
- );
- t.deepEqual(r1.providers[0], "http://prov1.example.com/");
- t.deepEqual(r1.providers[1], "http://prov2.example.com:123/");
-});
-test("taler restore URI (parsing, https with port)", (t) => {
- const r1 = parseRestoreUri(
- "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:234,https%3A%2F%2Fprov1.example.com%2F",
- );
- if (!r1) {
- t.fail();
- return;
- }
- t.deepEqual(
- r1.walletRootPriv,
- "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
- );
- t.deepEqual(r1.providers[0], "https://prov1.example.com/");
- t.deepEqual(r1.providers[1], "https://prov2.example.com:234/");
-});
-
-test("taler restore URI (stringify)", (t) => {
- const url = stringifyRestoreUri({
- walletRootPriv: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
- providers: ["http://prov1.example.com" as any, "https://prov2.example.com:234/" as any],
- });
- t.deepEqual(
- url,
- "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/http%3A%2F%2Fprov1.example.com%2F,https%3A%2F%2Fprov2.example.com%3A234%2F",
- );
-});
-
-/**
- * 5.11 action: dev-experiment https://lsd.gnunet.org/lsd0006/#name-action-dev-experiment
- */
+ /**
+ * 5.2 action: pay https://lsd.gnunet.org/lsd0006/#name-action-pay
+ */
+ test("taler pay url parsing: defaults", (t) => {
+ const url1 = "taler://pay/example.com/myorder/";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://example.com/" as HostPortPath);
+ t.is(r1.sessionId, "");
-test("taler dev exp URI (parsing)", (t) => {
- const url1 = "taler://dev-experiment/123";
- const r1 = parseDevExperimentUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.deepEqual(r1.devExperimentId, "123");
-});
-
-test("taler dev exp URI (stringify)", (t) => {
- const url1 = stringifyDevExperimentUri({
- devExperimentId: "123",
- });
- t.deepEqual(url1, "taler://dev-experiment/123");
-});
-
-/**
- * 5.12 action: withdraw-exchange https://lsd.gnunet.org/lsd0006/#name-action-withdraw-exchange
- */
+ const url2 = "taler://pay/example.com/myorder/mysession";
+ const r2 = parsePayUri(url2);
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.is(r2.merchantBaseUrl, "https://example.com/" as HostPortPath);
+ t.is(r2.sessionId, "mysession");
+ });
-test("taler withdraw exchange URI (parse)", (t) => {
- // Pubkey has been phased out, may no longer be specified.
- {
- const rx1 = parseWithdrawExchangeUri(
- "taler://withdraw-exchange/exchange.demo.taler.net/someroot/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A2",
+ test("taler pay url parsing: instance", (t) => {
+ const url1 = "taler://pay/example.com/instances/myinst/myorder/";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.merchantBaseUrl,
+ "https://example.com/instances/myinst/" as HostPortPath,
);
- if (rx1) {
+ t.is(r1.orderId, "myorder");
+ });
+
+ test("taler pay url parsing (claim token)", (t) => {
+ const url1 = "taler://pay/example.com/instances/myinst/myorder/?c=ASDF";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
t.fail();
return;
}
- }
- {
- const rx2 = parseWithdrawExchangeUri(
- "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ t.is(
+ r1.merchantBaseUrl,
+ "https://example.com/instances/myinst/" as HostPortPath,
);
- if (rx2) {
+ t.is(r1.orderId, "myorder");
+ t.is(r1.claimToken, "ASDF");
+ });
+
+ test("taler pay uri parsing: non-https", (t) => {
+ const url1 = "taler+http://pay/example.com/myorder/";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "http://example.com/" as HostPortPath);
+ t.is(r1.orderId, "myorder");
+ });
+
+ test("taler pay uri parsing: missing session component", (t) => {
+ const url1 = "taler+http://pay/example.com/myorder";
+ const r1 = parsePayUri(url1);
+ if (r1) {
+ t.fail();
+ return;
+ }
+ t.pass();
+ });
+
+ test("taler pay URI (stringify)", (t) => {
+ const url1 = stringifyPayUri({
+ merchantBaseUrl: "http://localhost:123/" as HostPortPath,
+ orderId: "foo",
+ sessionId: "",
+ });
+ t.deepEqual(url1, "taler+http://pay/localhost:123/foo/");
+
+ const url2 = stringifyPayUri({
+ merchantBaseUrl: "http://localhost:123/" as HostPortPath,
+ orderId: "foo",
+ sessionId: "bla",
+ });
+ t.deepEqual(url2, "taler+http://pay/localhost:123/foo/bla");
+ });
+
+ test("taler pay URI (stringify with https)", (t) => {
+ const url1 = stringifyPayUri({
+ merchantBaseUrl: "https://localhost:123/" as HostPortPath,
+ orderId: "foo",
+ sessionId: "",
+ });
+ t.deepEqual(url1, "taler://pay/localhost:123/foo/");
+
+ const url2 = stringifyPayUri({
+ merchantBaseUrl: "https://localhost/" as HostPortPath,
+ orderId: "foo",
+ sessionId: "bla",
+ noncePriv: "123",
+ });
+ t.deepEqual(url2, "taler://pay/localhost/foo/bla?n=123");
+ });
+
+ /**
+ * 5.3 action: refund https://lsd.gnunet.org/lsd0006/#name-action-refund
+ */
+
+ test("taler refund uri parsing: non-https #1", (t) => {
+ const url1 = "taler+http://refund/example.com/myorder/";
+ const r1 = parseRefundUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "http://example.com/" as HostPortPath);
+ t.is(r1.orderId, "myorder");
+ });
+
+ test("taler refund uri parsing", (t) => {
+ const url1 = "taler://refund/merchant.example.com/1234/";
+ const r1 = parseRefundUri(url1);
+ if (!r1) {
t.fail();
return;
}
- }
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/" as HostPortPath);
+ t.is(r1.orderId, "1234");
+ });
- // Now test well-formed URIs
- {
- const r2 = parseWithdrawExchangeUri(
- "taler://withdraw-exchange/exchange.demo.taler.net/someroot/",
+ test("taler refund uri parsing with instance", (t) => {
+ const url1 = "taler://refund/merchant.example.com/instances/myinst/1234/";
+ const r1 = parseRefundUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.orderId, "1234");
+ t.is(
+ r1.merchantBaseUrl,
+ "https://merchant.example.com/instances/myinst/" as HostPortPath,
);
- if (!r2) {
+ });
+
+ test("taler refund URI (stringify)", (t) => {
+ const url = stringifyRefundUri({
+ merchantBaseUrl: "https://merchant.test/instance/pepe/" as HostPortPath,
+ orderId: "123",
+ });
+ t.deepEqual(url, "taler://refund/merchant.test/instance/pepe/123/");
+ });
+
+ /**
+ * 5.5 action: pay-push https://lsd.gnunet.org/lsd0006/#name-action-pay-push
+ */
+
+ test("taler peer to peer push URI", (t) => {
+ const url1 = "taler://pay-push/exch.example.com/foo";
+ const r1 = parsePayPushUri(url1);
+ if (!r1) {
t.fail();
return;
}
- t.deepEqual(r2.amount, undefined);
- t.deepEqual(
- r2.exchangeBaseUrl,
- "https://exchange.demo.taler.net/someroot/",
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com/" as HostPortPath);
+ t.is(r1.contractPriv, "foo");
+ });
+
+ test("taler peer to peer push URI (path)", (t) => {
+ const url1 = "taler://pay-push/exch.example.com:123/bla/foo";
+ const r1 = parsePayPushUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.exchangeBaseUrl,
+ "https://exch.example.com:123/bla/" as HostPortPath,
);
- }
+ t.is(r1.contractPriv, "foo");
+ });
- {
- const r3 = parseWithdrawExchangeUri(
- "taler://withdraw-exchange/exchange.demo.taler.net/",
+ test("taler peer to peer push URI (http)", (t) => {
+ const url1 = "taler+http://pay-push/exch.example.com:123/bla/foo";
+ const r1 = parsePayPushUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.exchangeBaseUrl,
+ "http://exch.example.com:123/bla/" as HostPortPath,
);
- if (!r3) {
+ t.is(r1.contractPriv, "foo");
+ });
+
+ test("taler peer to peer push URI (stringify)", (t) => {
+ const url = stringifyPayPushUri({
+ exchangeBaseUrl: "https://foo.example.com/bla/" as HostPortPath,
+ contractPriv: "123",
+ });
+ t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123");
+ });
+
+ /**
+ * 5.6 action: pay-pull https://lsd.gnunet.org/lsd0006/#name-action-pay-pull
+ */
+
+ test("taler peer to peer pull URI", (t) => {
+ const url1 = "taler://pay-pull/exch.example.com/foo";
+ const r1 = parsePayPullUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com/" as HostPortPath);
+ t.is(r1.contractPriv, "foo");
+ });
+
+ test("taler peer to peer pull URI (path)", (t) => {
+ const url1 = "taler://pay-pull/exch.example.com:123/bla/foo";
+ const r1 = parsePayPullUri(url1);
+ if (!r1) {
t.fail();
return;
}
- t.deepEqual(r3.amount, undefined);
- t.deepEqual(r3.exchangeBaseUrl, "https://exchange.demo.taler.net/" as any);
- }
-
- {
- // No trailing slash, no path component
- const r4 = parseWithdrawExchangeUri(
- "taler://withdraw-exchange/exchange.demo.taler.net",
+ t.is(
+ r1.exchangeBaseUrl,
+ "https://exch.example.com:123/bla/" as HostPortPath,
);
- if (!r4) {
+ t.is(r1.contractPriv, "foo");
+ });
+
+ test("taler peer to peer pull URI (http)", (t) => {
+ const url1 = "taler+http://pay-pull/exch.example.com:123/bla/foo";
+ const r1 = parsePayPullUri(url1);
+ if (!r1) {
t.fail();
return;
}
- t.deepEqual(r4.amount, undefined);
- t.deepEqual(r4.exchangeBaseUrl, "https://exchange.demo.taler.net/" as any);
- }
-});
-
-test("taler withdraw exchange URI (stringify)", (t) => {
- const url = stringifyWithdrawExchange({
- exchangeBaseUrl: "https://exchange.demo.taler.net" as any,
- });
- t.deepEqual(url, "taler://withdraw-exchange/exchange.demo.taler.net/");
-});
-
-test("taler withdraw exchange URI with amount (stringify)", (t) => {
- const url = stringifyWithdrawExchange({
- exchangeBaseUrl: "https://exchange.demo.taler.net" as any,
- amount: "KUDOS:19" as any,
- });
- t.deepEqual(
- url,
- "taler://withdraw-exchange/exchange.demo.taler.net/?a=KUDOS%3A19",
- );
-});
-
-/**
- * 5.13 action: add-exchange https://lsd.gnunet.org/lsd0006/#name-action-add-exchange
- */
+ t.is(
+ r1.exchangeBaseUrl,
+ "http://exch.example.com:123/bla/" as HostPortPath,
+ );
+ t.is(r1.contractPriv, "foo");
+ });
-test("taler add exchange URI (parse)", (t) => {
- {
- const r1 = parseAddExchangeUri(
- "taler://add-exchange/exchange.example.com/",
+ test("taler peer to peer pull URI (stringify)", (t) => {
+ const url = stringifyPayPullUri({
+ exchangeBaseUrl: "https://foo.example.com/bla/" as HostPortPath,
+ contractPriv: "123",
+ });
+ t.deepEqual(url, "taler://pay-pull/foo.example.com/bla/123");
+ });
+
+ /**
+ * 5.7 action: pay-template https://lsd.gnunet.org/lsd0006/#name-action-pay-template
+ */
+
+ test("taler pay template URI (parsing)", (t) => {
+ const url1 =
+ "taler://pay-template/merchant.example.com/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY";
+ const r1 = parsePayTemplateUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.merchantBaseUrl,
+ "https://merchant.example.com/" as HostPortPath,
);
+ t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
+ });
+
+ test("taler pay template URI (parsing, http with port)", (t) => {
+ const url1 =
+ "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY";
+ const r1 = parsePayTemplateUri(url1);
if (!r1) {
t.fail();
return;
}
- t.deepEqual(r1.exchangeBaseUrl, "https://exchange.example.com/" as any);
- }
- {
- const r2 = parseAddExchangeUri(
- "taler://add-exchange/exchanges.example.com/api/",
+ t.deepEqual(
+ r1.merchantBaseUrl,
+ "http://merchant.example.com:1234/" as HostPortPath,
);
- if (!r2) {
+ t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
+ });
+
+ test("taler pay template URI (stringify)", (t) => {
+ const url1 = stringifyPayTemplateUri({
+ merchantBaseUrl: "http://merchant.example.com:1234/" as HostPortPath,
+ templateId: "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY",
+ });
+ t.deepEqual(
+ url1,
+ "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY",
+ );
+ });
+
+ /**
+ * 5.10 action: restore https://lsd.gnunet.org/lsd0006/#name-action-restore
+ */
+ test("taler restore URI (parsing, http with port)", (t) => {
+ const r1 = parseRestoreUri(
+ "taler+http://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:123",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.walletRootPriv,
+ "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ t.deepEqual(r1.providers[0], "http://prov1.example.com/");
+ t.deepEqual(r1.providers[1], "http://prov2.example.com:123/");
+ });
+ test("taler restore URI (parsing, https with port)", (t) => {
+ const r1 = parseRestoreUri(
+ "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:234,https%3A%2F%2Fprov1.example.com%2F",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.walletRootPriv,
+ "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ t.deepEqual(r1.providers[0], "https://prov1.example.com/");
+ t.deepEqual(r1.providers[1], "https://prov2.example.com:234/");
+ });
+
+ test("taler restore URI (stringify)", (t) => {
+ const url = stringifyRestoreUri({
+ walletRootPriv: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ providers: [
+ // FIXME: why here the stringify version add a slash in this provider?
+ "http://prov1.example.com" as HostPortPath,
+ "https://prov2.example.com:234/" as HostPortPath,
+ ],
+ });
+ t.deepEqual(
+ url,
+ "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/http%3A%2F%2Fprov1.example.com%2F,https%3A%2F%2Fprov2.example.com%3A234%2F",
+ );
+ });
+
+ /**
+ * 5.11 action: dev-experiment https://lsd.gnunet.org/lsd0006/#name-action-dev-experiment
+ */
+
+ test("taler dev exp URI (parsing)", (t) => {
+ const url1 = "taler://dev-experiment/123";
+ const r1 = parseDevExperimentUri(url1);
+ if (!r1) {
t.fail();
return;
}
- t.deepEqual(r2.exchangeBaseUrl, "https://exchanges.example.com/api/" as any);
- }
-});
+ t.deepEqual(r1.devExperimentId, "123");
+ });
-test("taler add exchange URI (stringify)", (t) => {
- const url = stringifyAddExchange({
- exchangeBaseUrl: "https://exchange.demo.taler.net" as any,
+ test("taler dev exp URI (stringify)", (t) => {
+ const url1 = stringifyDevExperimentUri({
+ devExperimentId: "123",
+ });
+ t.deepEqual(url1, "taler://dev-experiment/123");
});
- t.deepEqual(url, "taler://add-exchange/exchange.demo.taler.net/");
-});
-/**
- * wrong uris
- */
-test("taler pay url parsing: wrong scheme", (t) => {
- const url1 = "talerfoo://";
- const r1 = parsePayUri(url1);
- t.is(r1, undefined);
-
- const url2 = "taler://refund/a/b/c/d/e/f";
- const r2 = parsePayUri(url2);
- t.is(r2, undefined);
-});
+ /**
+ * 5.12 action: withdraw-exchange https://lsd.gnunet.org/lsd0006/#name-action-withdraw-exchange
+ */
+
+ test("taler withdraw exchange URI (parse)", (t) => {
+ // Pubkey has been phased out, may no longer be specified.
+ {
+ const rx1 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/someroot/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A2",
+ );
+ if (rx1) {
+ t.fail();
+ return;
+ }
+ }
+ {
+ const rx2 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ if (rx2) {
+ t.fail();
+ return;
+ }
+ }
+
+ // Now test well-formed URIs
+ {
+ const r2 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/someroot/",
+ );
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r2.amount, undefined);
+ t.deepEqual(
+ r2.exchangeBaseUrl,
+ "https://exchange.demo.taler.net/someroot/",
+ );
+ }
+
+ {
+ const r3 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/",
+ );
+ if (!r3) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r3.amount, undefined);
+ t.deepEqual(
+ r3.exchangeBaseUrl,
+ "https://exchange.demo.taler.net/" as HostPortPath,
+ );
+ }
+
+ {
+ // FIXME: why here the parser allows no ending slash
+ // No trailing slash, no path component
+ const r4 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net",
+ );
+ if (!r4) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r4.amount, undefined);
+ t.deepEqual(
+ r4.exchangeBaseUrl,
+ "https://exchange.demo.taler.net/" as HostPortPath,
+ );
+ }
+ });
+
+ test("taler withdraw exchange URI (stringify)", (t) => {
+ const url = stringifyWithdrawExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net" as HostPortPath,
+ });
+ t.deepEqual(url, "taler://withdraw-exchange/exchange.demo.taler.net/");
+ });
+
+ test("taler withdraw exchange URI with amount (stringify)", (t) => {
+ const url = stringifyWithdrawExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net" as HostPortPath,
+ amount: "KUDOS:19" as AmountString,
+ });
+ t.deepEqual(
+ url,
+ "taler://withdraw-exchange/exchange.demo.taler.net/?a=KUDOS%3A19",
+ );
+ });
+
+ /**
+ * 5.13 action: add-exchange https://lsd.gnunet.org/lsd0006/#name-action-add-exchange
+ */
+
+ test("taler add exchange URI (parse)", (t) => {
+ {
+ const r1 = parseAddExchangeUri(
+ "taler://add-exchange/exchange.example.com/",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.exchangeBaseUrl,
+ "https://exchange.example.com/" as HostPortPath,
+ );
+ }
+ {
+ const r2 = parseAddExchangeUri(
+ "taler://add-exchange/exchanges.example.com/api/",
+ );
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r2.exchangeBaseUrl,
+ "https://exchanges.example.com/api/" as HostPortPath,
+ );
+ }
+ });
+
+ test("taler add exchange URI (stringify)", (t) => {
+ const url = stringifyAddExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net" as HostPortPath,
+ });
+ t.deepEqual(url, "taler://add-exchange/exchange.demo.taler.net/");
+ });
+
+ /**
+ * wrong uris
+ */
+ test("taler pay url parsing: wrong scheme", (t) => {
+ const url1 = "talerfoo://";
+ const r1 = parsePayUri(url1);
+ t.is(r1, undefined);
+
+ const url2 = "taler://refund/a/b/c/d/e/f";
+ const r2 = parsePayUri(url2);
+ t.is(r2, undefined);
+ });
+}
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
@@ -244,6 +244,124 @@ export namespace TalerUris {
...opts,
};
}
+ function asHost(s: HostPortPath): string {
+ const b = new URL(s);
+ // if (b.port) {
+ // return `${b.host}:${b.port}${b.pathname}`;
+ // }
+ return `${b.host}${b.pathname}`;
+ }
+
+ function getTalerParamList(p: URI): [string, string][] {
+ const result: [string, string][] = [];
+ switch (p.type) {
+ case TalerUriAction.Withdraw: {
+ if (p.externalConfirmation) result.push(["external-confirmation", "1"]);
+ return result;
+ }
+ case TalerUriAction.Pay: {
+ if (p.claimToken) result.push(["c", p.claimToken]);
+ if (p.noncePriv) result.push(["n", p.noncePriv]);
+ return result;
+ }
+ case TalerUriAction.WithdrawExchange: {
+ if (p.amount) result.push(["a", p.amount]);
+ return result;
+ }
+ case TalerUriAction.WithdrawalTransferResult: {
+ result.push(["ref", p.ref]);
+ if (p.status) result.push(["status", p.status]);
+ return result;
+ }
+ case TalerUriAction.Refund:
+ case TalerUriAction.PayPush:
+ case TalerUriAction.PayPull:
+ case TalerUriAction.PayTemplate:
+ case TalerUriAction.Restore:
+ case TalerUriAction.DevExperiment:
+ case TalerUriAction.AddExchange: {
+ return result;
+ }
+ default: {
+ assertUnreachable(p);
+ }
+ }
+ }
+
+ function getTalerPrefix(p: URI): string {
+ switch (p.type) {
+ case TalerUriAction.Withdraw:
+ return p.bankIntegrationApiBaseUrl.startsWith("http://")
+ ? TALER_HTTP_PREFIX
+ : TALER_PREFIX;
+ case TalerUriAction.Pay:
+ case TalerUriAction.Refund:
+ case TalerUriAction.PayTemplate:
+ return p.merchantBaseUrl.startsWith("http://")
+ ? TALER_HTTP_PREFIX
+ : TALER_PREFIX;
+ case TalerUriAction.PayPush:
+ case TalerUriAction.PayPull:
+ case TalerUriAction.AddExchange:
+ case TalerUriAction.WithdrawExchange:
+ return p.exchangeBaseUrl.startsWith("http://")
+ ? TALER_HTTP_PREFIX
+ : TALER_PREFIX;
+ case TalerUriAction.Restore:
+ case TalerUriAction.DevExperiment:
+ case TalerUriAction.WithdrawalTransferResult:
+ return TALER_PREFIX;
+ default:
+ assertUnreachable(p);
+ }
+ }
+
+ function getTalerPath(p: URI): string {
+ /**
+ * After the host we should not add a / since the href
+ * already adds one
+ */
+ switch (p.type) {
+ case TalerUriAction.Withdraw:
+ return `/${asHost(p.bankIntegrationApiBaseUrl)}${
+ p.withdrawalOperationId
+ }`;
+ case TalerUriAction.Pay:
+ return `/${asHost(p.merchantBaseUrl)}${p.orderId}/${p.sessionId}`;
+ case TalerUriAction.Refund:
+ // refund should end with a /
+ return `/${asHost(p.merchantBaseUrl)}${p.orderId}/`;
+ case TalerUriAction.PayTemplate:
+ return `/${asHost(p.merchantBaseUrl)}${p.templateId}`;
+ case TalerUriAction.PayPush:
+ return `/${asHost(p.exchangeBaseUrl)}${p.contractPriv}`;
+ case TalerUriAction.PayPull:
+ return `/${asHost(p.exchangeBaseUrl)}${p.contractPriv}`;
+ case TalerUriAction.AddExchange:
+ return `/${asHost(p.exchangeBaseUrl)}`;
+ case TalerUriAction.WithdrawExchange:
+ return `/${asHost(p.exchangeBaseUrl)}`;
+ case TalerUriAction.Restore:
+ return `/${p.walletRootPriv}/${p.providers
+ .map((d) => encodeURIComponent(d))
+ .join(",")}`;
+ case TalerUriAction.DevExperiment:
+ return `/${p.devExperimentId}`;
+ case TalerUriAction.WithdrawalTransferResult:
+ return `/`;
+ default:
+ assertUnreachable(p);
+ }
+ }
+
+ export function toString(p: URI): TalerUriString {
+ const prefix = getTalerPrefix(p);
+ const path = getTalerPath(p);
+ const paramList = getTalerParamList(p);
+ const url = new URL(`${prefix}${p.type}${path}`);
+ url.search = createSearchParams(paramList);
+ return url.href as TalerUriString;
+ }
export function fromString(
s: string,
@@ -310,7 +428,7 @@ export namespace TalerUris {
}
// get merchant host
- const merchant = Paytos.parseHostPortPath(
+ const merchant = Paytos.parseHostPortPath2(
cs[0],
cs.slice(1, -2).join("/"),
scheme,
@@ -352,7 +470,7 @@ export namespace TalerUris {
}
// get bank host
- const bank = Paytos.parseHostPortPath(
+ const bank = Paytos.parseHostPortPath2(
cs[0],
cs.slice(1, -1).join("/"),
scheme,
@@ -383,16 +501,26 @@ export namespace TalerUris {
}
case TalerUriAction.Refund: {
// check number of segments
- if (cs.length < 2) {
+ if (cs.length < 3) {
return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
uriType,
});
}
+ if (cs[cs.length - 1]) {
+ // last must be empty
+ return opKnownFailureWithBody(
+ TalerUriParseError.INVALID_TARGET_PATH,
+ {
+ pos: cs.length - 1,
+ uriType,
+ },
+ );
+ }
// get merchant host
- const merchant = Paytos.parseHostPortPath(
+ const merchant = Paytos.parseHostPortPath2(
cs[0],
- cs.slice(1, -1).join("/"),
+ cs.slice(1, -2).join("/"),
scheme,
);
if (!opts.ignoreComponentError && !merchant) {
@@ -407,8 +535,7 @@ export namespace TalerUris {
}
// get order id
- const orderId = cs[cs.length - 1];
-
+ const orderId = cs[cs.length - 2];
return opFixedSuccess<URI>(
createTalerRefund(merchant ?? (cs[0] as HostPortPath), orderId),
);
@@ -422,7 +549,7 @@ export namespace TalerUris {
}
// get exchange host
- const exchange = Paytos.parseHostPortPath(
+ const exchange = Paytos.parseHostPortPath2(
cs[0],
cs.slice(1, -1).join("/"),
scheme,
@@ -453,7 +580,7 @@ export namespace TalerUris {
}
// get exchange host
- const exchange = Paytos.parseHostPortPath(
+ const exchange = Paytos.parseHostPortPath2(
cs[0],
cs.slice(1, -1).join("/"),
scheme,
@@ -485,7 +612,7 @@ export namespace TalerUris {
}
// get merchant host
- const merchant = Paytos.parseHostPortPath(
+ const merchant = Paytos.parseHostPortPath2(
cs[0],
cs.slice(1, -1).join("/"),
scheme,
@@ -523,8 +650,25 @@ export namespace TalerUris {
const providers: Array<HostPortPath> = [];
// const providers = new Array<HostPortPath>();
cs[1].split(",").map((name) => {
- const [hostname, path] = decodeURIComponent(name).split("/", 1);
- const host = Paytos.parseHostPortPath(hostname, path, scheme)!;
+ const url = decodeURIComponent(name);
+
+ let isHttp = false;
+ const withoutScheme = url.startsWith("https://")
+ ? url.substring(8)
+ : (isHttp = url.startsWith("http://"))
+ ? url.substring(7)
+ : url;
+
+ // Check resolution of this issue https://bugs.gnunet.org/view.php?id=10466
+ const thisScheme =
+ url === withoutScheme ? scheme : isHttp ? "http" : "https";
+
+ const [hostname, path] = withoutScheme.split("/", 1);
+ const host = Paytos.parseHostPortPath2(
+ hostname,
+ path,
+ thisScheme,
+ )!;
providers.push(host);
});
@@ -549,16 +693,29 @@ export namespace TalerUris {
}
case TalerUriAction.WithdrawExchange: {
// check number of segments
- if (cs.length !== 1) {
+ if (cs.length < 1) {
return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
uriType,
});
}
+ // FIXME: https://bugs.gnunet.org/view.php?id=10466
+ // if (cs[cs.length-1]) {
+ if (cs.length > 1 && cs[cs.length - 1]) {
+ // last must be empty
+ return opKnownFailureWithBody(
+ TalerUriParseError.INVALID_TARGET_PATH,
+ {
+ pos: cs.length - 1,
+ uriType,
+ },
+ );
+ }
+
// get exchange host
- const exchange = Paytos.parseHostPortPath(
+ const exchange = Paytos.parseHostPortPath2(
cs[0],
- cs.slice(1).join("/"),
+ cs.slice(1, -1).join("/"),
scheme,
);
if (!opts.ignoreComponentError && !exchange) {
@@ -607,7 +764,7 @@ export namespace TalerUris {
}
// get exchange host
- const exchange = Paytos.parseHostPortPath(
+ const exchange = Paytos.parseHostPortPath2(
cs[0],
cs.slice(1).join("/"),
scheme,
@@ -766,7 +923,7 @@ export function parseWithdrawUriWithError(s: string) {
const result: WithdrawUriResult = {
type: TalerUriAction.Withdraw,
- bankIntegrationApiBaseUrl: Paytos.parseHostPortPath(
+ bankIntegrationApiBaseUrl: Paytos.parseHostPortPath2(
host,
pathSegments.join("/"),
pi.value.innerProto,
@@ -815,7 +972,7 @@ export function parseAddExchangeUriWithError(s: string) {
const result: AddExchangeUri = {
type: TalerUriAction.AddExchange,
- exchangeBaseUrl: Paytos.parseHostPortPath(
+ exchangeBaseUrl: Paytos.parseHostPortPath2(
host,
pathSegments.join("/"),
pi.value.innerProto,
@@ -912,6 +1069,13 @@ interface ProtoInfo {
rest: string;
}
+/**
+ * @deprecated
+ *
+ * @param s
+ * @param action
+ * @returns
+ */
function parseProtoInfoWithError(
s: string,
action: string,
@@ -956,6 +1120,12 @@ const parsers: { [A in TalerUriAction]: Parser } = {
},
};
+/**
+ * @deprecated
+ *
+ * @param string
+ * @returns
+ */
export function parseTalerUri(string: string): TalerUri | undefined {
const https = string.startsWith("taler://");
const http = string.startsWith("taler+http://");
@@ -968,6 +1138,12 @@ export function parseTalerUri(string: string): TalerUri | undefined {
return parsers[found](string);
}
+/**
+ * @deprecated
+ *
+ * @param uri
+ * @returns
+ */
export function stringifyTalerUri(uri: TalerUri): string {
switch (uri.type) {
case TalerUriAction.DevExperiment: {
@@ -1007,6 +1183,8 @@ export function stringifyTalerUri(uri: TalerUri): string {
}
/**
+ * @deprecated
+ *
* Parse a taler[+http]://pay URI.
* Return undefined if not passed a valid URI.
*/
@@ -1028,7 +1206,7 @@ export function parsePayUri(s: string): PayUriResult | undefined {
const orderId = parts[parts.length - 2];
const pathSegments = parts.slice(1, parts.length - 2);
// const p = [host, ...pathSegments].join("/");
- const merchantBaseUrl = Paytos.parseHostPortPath(
+ const merchantBaseUrl = Paytos.parseHostPortPath2(
host,
pathSegments.join("/"),
pi.innerProto,
@@ -1044,6 +1222,12 @@ export function parsePayUri(s: string): PayUriResult | undefined {
};
}
+/**
+ * @deprecated
+ *
+ * @param s
+ * @returns
+ */
export function parsePayTemplateUri(
uriString: string,
): PayTemplateUriResult | undefined {
@@ -1072,8 +1256,8 @@ export function parsePayTemplateUri(
// `${pi.innerProto}://${hostAndSegments}/`,
// );
- const merchantBaseUrl = Paytos.parseHostPortPath(
- host,
+ const merchantBaseUrl = Paytos.parseHostPortPath2(
+ host,
pathSegments.join("/"),
pi.innerProto,
)!;
@@ -1084,6 +1268,12 @@ export function parsePayTemplateUri(
};
}
+/**
+ * @deprecated
+ *
+ * @param s
+ * @returns
+ */
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
const pi = parseProtoInfo(s, TalerUriAction.PayPush);
if (!pi) {
@@ -1101,7 +1291,7 @@ export function parsePayPushUri(s: string): PayPushUriResult | undefined {
// const exchangeBaseUrl = canonicalizeBaseUrl(
// `${pi.innerProto}://${hostAndSegments}/`,
// );
- const exchangeBaseUrl = Paytos.parseHostPortPath(
+ const exchangeBaseUrl = Paytos.parseHostPortPath2(
host,
pathSegments.join("/"),
pi.innerProto,
@@ -1114,6 +1304,12 @@ export function parsePayPushUri(s: string): PayPushUriResult | undefined {
};
}
+/**
+ * @deprecated
+ *
+ * @param s
+ * @returns
+ */
export function parsePayPullUri(s: string): PayPullUriResult | undefined {
const pi = parseProtoInfo(s, TalerUriAction.PayPull);
if (!pi) {
@@ -1131,7 +1327,7 @@ export function parsePayPullUri(s: string): PayPullUriResult | undefined {
// const exchangeBaseUrl = canonicalizeBaseUrl(
// `${pi.innerProto}://${hostAndSegments}/`,
// );
- const exchangeBaseUrl = Paytos.parseHostPortPath(
+ const exchangeBaseUrl = Paytos.parseHostPortPath2(
host,
pathSegments.join("/"),
pi.innerProto,
@@ -1144,6 +1340,12 @@ export function parsePayPullUri(s: string): PayPullUriResult | undefined {
};
}
+/**
+ * @deprecaed
+ *
+ * @param s
+ * @returns
+ */
export function parseWithdrawExchangeUri(
s: string,
): WithdrawExchangeUri | undefined {
@@ -1170,7 +1372,7 @@ export function parseWithdrawExchangeUri(
// const exchangeBaseUrl = canonicalizeBaseUrl(
// `${pi.innerProto}://${hostAndSegments}/`,
// );
- const exchangeBaseUrl = Paytos.parseHostPortPath(
+ const exchangeBaseUrl = Paytos.parseHostPortPath2(
host,
pathSegments.join("/"),
pi.innerProto,
@@ -1187,6 +1389,7 @@ export function parseWithdrawExchangeUri(
}
/**
+ * @deprecated
* Parse a taler[+http]://refund URI.
* Return undefined if not passed a valid URI.
*/
@@ -1208,7 +1411,7 @@ export function parseRefundUri(s: string): RefundUriResult | undefined {
// const merchantBaseUrl = canonicalizeBaseUrl(
// `${pi.innerProto}://${hostAndSegments}/`,
// );
- const merchantBaseUrl = Paytos.parseHostPortPath(
+ const merchantBaseUrl = Paytos.parseHostPortPath2(
host,
pathSegments.join("/"),
pi.innerProto,
@@ -1221,6 +1424,12 @@ export function parseRefundUri(s: string): RefundUriResult | undefined {
};
}
+/**
+ * @deprecated
+ *
+ * @param s
+ * @returns
+ */
export function parseDevExperimentUri(s: string): DevExperimentUri | undefined {
const pi = parseProtoInfo(s, "dev-experiment");
const c = pi?.rest.split("?");
@@ -1235,6 +1444,12 @@ export function parseDevExperimentUri(s: string): DevExperimentUri | undefined {
};
}
+/**
+ * @deprecated
+ *
+ * @param s
+ * @returns
+ */
export function parseRestoreUri(uri: string): BackupRestoreUri | undefined {
const pi = parseProtoInfo(uri, "restore");
if (!pi) {
@@ -1250,9 +1465,17 @@ export function parseRestoreUri(uri: string): BackupRestoreUri | undefined {
if (!walletRootPriv) return undefined;
const providers = new Array<HostPortPath>();
parts[1].split(",").map((name) => {
- const [hostname, path] = decodeURIComponent(name).split("/", 1);
- const host = Paytos.parseHostPortPath(hostname, path??"/", pi.innerProto)!;
- console.log("TUVIEJA", hostname, path??"/", host)
+ const url = decodeURIComponent(name);
+ let isHttp = false;
+ const withoutScheme = url.startsWith("https://")
+ ? url.substring(8)
+ : (isHttp = url.startsWith("http://"))
+ ? url.substring(7)
+ : url;
+ const scheme =
+ url === withoutScheme ? pi.innerProto : isHttp ? "http" : "https";
+ const [hostname, path] = withoutScheme.split("/", 1);
+ const host = Paytos.parseHostPortPath2(hostname, path ?? "/", scheme)!;
providers.push(host);
});
return {
@@ -1266,6 +1489,11 @@ export function parseRestoreUri(uri: string): BackupRestoreUri | undefined {
// To string functions
// ================================================
+/**
+ * @deprecated
+ * @param param0
+ * @returns
+ */
export function stringifyPayUri({
merchantBaseUrl,
orderId,
@@ -1280,6 +1508,11 @@ export function stringifyPayUri({
return `${proto}://pay/${path}${orderId}/${sessionId}${query}`;
}
+/**
+ * @deprecated
+ * @param param0
+ * @returns
+ */
export function stringifyPayPullUri({
contractPriv,
exchangeBaseUrl,
@@ -1288,6 +1521,11 @@ export function stringifyPayPullUri({
return `${proto}://pay-pull/${path}${contractPriv}`;
}
+/**
+ * @deprecated
+ * @param param0
+ * @returns
+ */
export function stringifyPayPushUri({
contractPriv,
exchangeBaseUrl,
@@ -1297,6 +1535,11 @@ export function stringifyPayPushUri({
return `${proto}://pay-push/${path}${contractPriv}`;
}
+/**
+ * @deprecated
+ * @param param0
+ * @returns
+ */
export function stringifyRestoreUri({
providers,
walletRootPriv,
@@ -1307,6 +1550,11 @@ export function stringifyRestoreUri({
return `taler://restore/${walletRootPriv}/${list}`;
}
+/**
+ * @deprecated
+ * @param param0
+ * @returns
+ */
export function stringifyWithdrawExchange({
exchangeBaseUrl,
amount,
@@ -1317,6 +1565,11 @@ export function stringifyWithdrawExchange({
return `${proto}://withdraw-exchange/${path}${query}`;
}
+/**
+ * @deprecated
+ * @param param0
+ * @returns
+ */
export function stringifyAddExchange({
exchangeBaseUrl,
}: Omit<AddExchangeUri, "type">): string {
@@ -1324,12 +1577,22 @@ export function stringifyAddExchange({
return `${proto}://add-exchange/${path}`;
}
+/**
+ * @deprecated
+ * @param param0
+ * @returns
+ */
export function stringifyDevExperimentUri({
devExperimentId,
}: Omit<DevExperimentUri, "type">): string {
return `taler://dev-experiment/${devExperimentId}`;
}
+/**
+ * @deprecated
+ * @param param0
+ * @returns
+ */
export function stringifyPayTemplateUri({
merchantBaseUrl,
templateId,
@@ -1338,6 +1601,11 @@ export function stringifyPayTemplateUri({
return `${proto}://pay-template/${path}${templateId}${query}`;
}
+/**
+ * @deprecated
+ * @param param0
+ * @returns
+ */
export function stringifyRefundUri({
merchantBaseUrl,
orderId,
@@ -1346,6 +1614,11 @@ export function stringifyRefundUri({
return `${proto}://refund/${path}${orderId}/`;
}
+/**
+ * @deprecated
+ * @param param0
+ * @returns
+ */
export function stringifyWithdrawUri({
bankIntegrationApiBaseUrl,
withdrawalOperationId,
@@ -1403,3 +1676,24 @@ function getUrlInfo(
return { proto, path, query };
}
+
+/**
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
+ */
+function encodeRFC3986URIComponent(str: string): string {
+ return encodeURIComponent(str).replace(
+ /[!'()*]/g,
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
+ );
+}
+const rfc3986 = encodeRFC3986URIComponent;
+
+/**
+ *
+ * https://www.rfc-editor.org/rfc/rfc3986
+ */
+function createSearchParams(paramList: [string, string][]): string {
+ return paramList
+ .map(([key, value]) => `${rfc3986(key)}=${rfc3986(value)}`)
+ .join("&");
+}
diff --git a/packages/taler-util/src/taleruris.test.ts b/packages/taler-util/src/taleruris.test.ts
@@ -0,0 +1,608 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import test from "ava";
+import { TalerUriAction, TalerUriParseError, TalerUris } from "./taleruri.js";
+import { HostPortPath } from "./payto.js";
+import { AmountString } from "./types-taler-common.js";
+import { failOrThrow, succeedOrThrow } from "./operation.js";
+
+/**
+ * 5.1 action: withdraw https://lsd.gnunet.org/lsd0006/#name-action-withdraw
+ */
+
+test("taler-new withdraw uri parsing", (t) => {
+ const url1 = "taler://withdraw/bank.example.com/12345";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.Withdraw) {
+ t.fail();
+ return;
+ }
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(
+ r1.bankIntegrationApiBaseUrl,
+ "https://bank.example.com/" as HostPortPath,
+ );
+});
+
+test("taler-new withdraw uri parsing with external confirmation", (t) => {
+ const url1 =
+ "taler://withdraw/bank.example.com/12345?external-confirmation=1";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.Withdraw) {
+ t.fail();
+ return;
+ }
+ t.is(r1.externalConfirmation, true);
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(
+ r1.bankIntegrationApiBaseUrl,
+ "https://bank.example.com/" as HostPortPath,
+ );
+});
+
+test("taler-new withdraw uri parsing (http)", (t) => {
+ const url1 = "taler+http://withdraw/bank.example.com/12345";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.Withdraw) {
+ t.fail();
+ return;
+ }
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(
+ r1.bankIntegrationApiBaseUrl,
+ "http://bank.example.com/" as HostPortPath,
+ );
+});
+
+test("taler-new withdraw URI (stringify)", (t) => {
+ const url = TalerUris.toString({
+ type: TalerUriAction.Withdraw,
+ bankIntegrationApiBaseUrl:
+ "https://bank.taler.test/integration-api/" as HostPortPath,
+ withdrawalOperationId: "123",
+ });
+ t.deepEqual(url, "taler://withdraw/bank.taler.test/integration-api/123");
+});
+
+/**
+ * 5.2 action: pay https://lsd.gnunet.org/lsd0006/#name-action-pay
+ */
+test("taler-new pay url parsing: defaults", (t) => {
+ const url1 = "taler://pay/example.com/myorder/";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.Pay) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://example.com/" as HostPortPath);
+ t.is(r1.sessionId, "");
+
+ const url2 = "taler://pay/example.com/myorder/mysession";
+ const r2 = succeedOrThrow(TalerUris.fromString(url2));
+ if (r2.type !== TalerUriAction.Pay) {
+ t.fail();
+ return;
+ }
+ t.is(r2.merchantBaseUrl, "https://example.com/" as HostPortPath);
+ t.is(r2.sessionId, "mysession");
+});
+
+test("taler-new pay url parsing: instance", (t) => {
+ const url1 = "taler://pay/example.com/instances/myinst/myorder/";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.Pay) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.merchantBaseUrl,
+ "https://example.com/instances/myinst/" as HostPortPath,
+ );
+ t.is(r1.orderId, "myorder");
+});
+
+test("taler-new pay url parsing (claim token)", (t) => {
+ const url1 = "taler://pay/example.com/instances/myinst/myorder/?c=ASDF";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.Pay) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.merchantBaseUrl,
+ "https://example.com/instances/myinst/" as HostPortPath,
+ );
+ t.is(r1.orderId, "myorder");
+ t.is(r1.claimToken, "ASDF");
+});
+
+test("taler-new pay uri parsing: non-https", (t) => {
+ const url1 = "taler+http://pay/example.com/myorder/";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.Pay) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "http://example.com/" as HostPortPath);
+ t.is(r1.orderId, "myorder");
+});
+
+test("taler-new pay uri parsing: missing session component", (t) => {
+ const url1 = "taler+http://pay/example.com/myorder";
+ failOrThrow(TalerUris.fromString(url1), TalerUriParseError.COMPONENTS_LENGTH);
+ t.pass();
+});
+
+test("taler-new pay URI (stringify)", (t) => {
+ const url1 = TalerUris.toString({
+ type: TalerUriAction.Pay,
+ merchantBaseUrl: "http://localhost:123/" as HostPortPath,
+ orderId: "foo",
+ sessionId: "",
+ });
+ t.deepEqual(url1, "taler+http://pay/localhost:123/foo/");
+
+ const url2 = TalerUris.toString({
+ type: TalerUriAction.Pay,
+ merchantBaseUrl: "http://localhost:123/" as HostPortPath,
+ orderId: "foo",
+ sessionId: "bla",
+ });
+ t.deepEqual(url2, "taler+http://pay/localhost:123/foo/bla");
+});
+
+test("taler-new pay URI (stringify with https)", (t) => {
+ const url1 = TalerUris.toString({
+ type: TalerUriAction.Pay,
+ merchantBaseUrl: "https://localhost:123/" as HostPortPath,
+ orderId: "foo",
+ sessionId: "",
+ });
+ t.deepEqual(url1, "taler://pay/localhost:123/foo/");
+
+ const url2 = TalerUris.toString({
+ type: TalerUriAction.Pay,
+ merchantBaseUrl: "https://localhost/" as HostPortPath,
+ orderId: "foo",
+ sessionId: "bla",
+ noncePriv: "123",
+ });
+ t.deepEqual(url2, "taler://pay/localhost/foo/bla?n=123");
+});
+
+/**
+ * 5.3 action: refund https://lsd.gnunet.org/lsd0006/#name-action-refund
+ */
+
+test("taler-new refund uri parsing: non-https #1", (t) => {
+ const url1 = "taler+http://refund/example.com/myorder/";
+ // const r1 = parseRefundUri(url1);
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.Refund) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "http://example.com/" as HostPortPath);
+ t.is(r1.orderId, "myorder");
+});
+
+test("taler-new refund uri parsing", (t) => {
+ const url1 = "taler://refund/merchant.example.com/1234/";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.Refund) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/" as HostPortPath);
+ t.is(r1.orderId, "1234");
+});
+
+test("taler-new refund uri parsing with instance", (t) => {
+ const url1 = "taler://refund/merchant.example.com/instances/myinst/1234/";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.Refund) {
+ t.fail();
+ return;
+ }
+ t.is(r1.orderId, "1234");
+ t.is(
+ r1.merchantBaseUrl,
+ "https://merchant.example.com/instances/myinst/" as HostPortPath,
+ );
+});
+
+test("taler-new refund URI (stringify)", (t) => {
+ const url = TalerUris.toString({
+ type: TalerUriAction.Refund,
+ merchantBaseUrl: "https://merchant.test/instance/pepe/" as HostPortPath,
+ orderId: "123",
+ });
+ t.deepEqual(url, "taler://refund/merchant.test/instance/pepe/123/");
+});
+
+/**
+ * 5.5 action: pay-push https://lsd.gnunet.org/lsd0006/#name-action-pay-push
+ */
+
+test("taler-new peer to peer push URI", (t) => {
+ const url1 = "taler://pay-push/exch.example.com/foo";
+ // const r1 = parsePayPushUri(url1);
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.PayPush) {
+ t.fail();
+ return;
+ }
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com/" as HostPortPath);
+ t.is(r1.contractPriv, "foo");
+});
+
+test("taler-new peer to peer push URI (path)", (t) => {
+ const url1 = "taler://pay-push/exch.example.com:123/bla/foo";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.PayPush) {
+ t.fail();
+ return;
+ }
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com:123/bla/" as HostPortPath);
+ t.is(r1.contractPriv, "foo");
+});
+
+test("taler-new peer to peer push URI (http)", (t) => {
+ const url1 = "taler+http://pay-push/exch.example.com:123/bla/foo";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.PayPush) {
+ t.fail();
+ return;
+ }
+ t.is(r1.exchangeBaseUrl, "http://exch.example.com:123/bla/" as HostPortPath);
+ t.is(r1.contractPriv, "foo");
+});
+
+test("taler-new peer to peer push URI (stringify)", (t) => {
+ const url = TalerUris.toString({
+ type: TalerUriAction.PayPush,
+ exchangeBaseUrl: "https://foo.example.com/bla/" as HostPortPath,
+ contractPriv: "123",
+ });
+ t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123");
+});
+
+/**
+ * 5.6 action: pay-pull https://lsd.gnunet.org/lsd0006/#name-action-pay-pull
+ */
+
+test("taler-new peer to peer pull URI", (t) => {
+ const url1 = "taler://pay-pull/exch.example.com/foo";
+ // const r1 = parsePayPullUri(url1);
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+
+ if (r1.type !== TalerUriAction.PayPull) {
+ t.fail();
+ return;
+ }
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com/" as HostPortPath);
+ t.is(r1.contractPriv, "foo");
+});
+
+test("taler-new peer to peer pull URI (path)", (t) => {
+ const url1 = "taler://pay-pull/exch.example.com:123/bla/foo";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.PayPull) {
+ t.fail();
+ return;
+ }
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com:123/bla/" as HostPortPath);
+ t.is(r1.contractPriv, "foo");
+});
+
+test("taler-new peer to peer pull URI (http)", (t) => {
+ const url1 = "taler+http://pay-pull/exch.example.com:123/bla/foo";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.PayPull) {
+ t.fail();
+ return;
+ }
+ t.is(r1.exchangeBaseUrl, "http://exch.example.com:123/bla/" as HostPortPath);
+ t.is(r1.contractPriv, "foo");
+});
+
+test("taler-new peer to peer pull URI (stringify)", (t) => {
+ const url = TalerUris.toString({
+ type: TalerUriAction.PayPull,
+ exchangeBaseUrl: "https://foo.example.com/bla/" as HostPortPath,
+ contractPriv: "123",
+ });
+ t.deepEqual(url, "taler://pay-pull/foo.example.com/bla/123");
+});
+
+/**
+ * 5.7 action: pay-template https://lsd.gnunet.org/lsd0006/#name-action-pay-template
+ */
+
+test("taler-new pay template URI (parsing)", (t) => {
+ const url1 =
+ "taler://pay-template/merchant.example.com/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY";
+ // const r1 = parsePayTemplateUri(url1);
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+
+ if (r1.type !== TalerUriAction.PayTemplate) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.merchantBaseUrl,
+ "https://merchant.example.com/" as HostPortPath,
+ );
+ t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
+});
+
+test("taler-new pay template URI (parsing, http with port)", (t) => {
+ const url1 =
+ "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.PayTemplate) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.merchantBaseUrl,
+ "http://merchant.example.com:1234/" as HostPortPath,
+ );
+ t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
+});
+
+test("taler-new pay template URI (stringify)", (t) => {
+ const url1 = TalerUris.toString({
+ type: TalerUriAction.PayTemplate,
+ merchantBaseUrl: "http://merchant.example.com:1234/" as HostPortPath,
+ templateId: "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY",
+ });
+ t.deepEqual(
+ url1,
+ "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY",
+ );
+});
+
+/**
+ * 5.10 action: restore https://lsd.gnunet.org/lsd0006/#name-action-restore
+ */
+test("taler-new restore URI (parsing, http with port)", (t) => {
+ const url1 =
+ "taler+http://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:123";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ // const r1 = parseRestoreUri(
+ // ,
+ // );
+ if (r1.type !== TalerUriAction.Restore) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.walletRootPriv,
+ "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ t.deepEqual(r1.providers[0], "http://prov1.example.com/");
+ t.deepEqual(r1.providers[1], "http://prov2.example.com:123/");
+});
+test("taler-new restore URI (parsing, https with port)", (t) => {
+ const url1 =
+ "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:234,https%3A%2F%2Fprov1.example.com%2F";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+
+ if (r1.type !== TalerUriAction.Restore) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.walletRootPriv,
+ "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ t.deepEqual(r1.providers[0], "https://prov1.example.com/");
+ t.deepEqual(r1.providers[1], "https://prov2.example.com:234/");
+});
+
+test("taler-new restore URI (stringify)", (t) => {
+ const url = TalerUris.toString({
+ type: TalerUriAction.Restore,
+ walletRootPriv: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ providers: [
+ "http://prov1.example.com/" as HostPortPath,
+ "https://prov2.example.com:234/" as HostPortPath,
+ ],
+ });
+ t.deepEqual(
+ url,
+ "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/http%3A%2F%2Fprov1.example.com%2F,https%3A%2F%2Fprov2.example.com%3A234%2F",
+ );
+});
+
+/**
+ * 5.11 action: dev-experiment https://lsd.gnunet.org/lsd0006/#name-action-dev-experiment
+ */
+
+test("taler-new dev exp URI (parsing)", (t) => {
+ const url1 = "taler://dev-experiment/123";
+ // const r1 = parseDevExperimentUri(url1);
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+
+ if (r1.type !== TalerUriAction.DevExperiment) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.devExperimentId, "123");
+});
+
+test("taler-new dev exp URI (stringify)", (t) => {
+ const url1 = TalerUris.toString({
+ type: TalerUriAction.DevExperiment,
+ devExperimentId: "123",
+ });
+ t.deepEqual(url1, "taler://dev-experiment/123");
+});
+
+/**
+ * 5.12 action: withdraw-exchange https://lsd.gnunet.org/lsd0006/#name-action-withdraw-exchange
+ */
+
+test("taler-new withdraw exchange URI (parse)", (t) => {
+ // Pubkey has been phased out, may no longer be specified.
+ {
+ const url1 =
+ "taler://withdraw-exchange/exchange.demo.taler.net/someroot/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A2";
+ failOrThrow(
+ TalerUris.fromString(url1),
+ TalerUriParseError.INVALID_TARGET_PATH,
+ );
+
+ // if (r1.type !== TalerUriAction.WithdrawExchange) {
+ // t.fail();
+ // return;
+ // }
+ }
+ {
+ const url1 =
+ "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0";
+ failOrThrow(
+ TalerUris.fromString(url1),
+ TalerUriParseError.INVALID_TARGET_PATH,
+ );
+ }
+
+ // Now test well-formed URIs
+ {
+ const url1 = "taler://withdraw-exchange/exchange.demo.taler.net/someroot/";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.WithdrawExchange) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.amount, undefined);
+ t.deepEqual(
+ r1.exchangeBaseUrl,
+ "https://exchange.demo.taler.net/someroot/",
+ );
+ }
+
+ {
+ const url1 = "taler://withdraw-exchange/exchange.demo.taler.net/";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.WithdrawExchange) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.amount, undefined);
+ t.deepEqual(
+ r1.exchangeBaseUrl,
+ "https://exchange.demo.taler.net/" as HostPortPath,
+ );
+ }
+
+ {
+ const url1 = "taler://withdraw-exchange/exchange.demo.taler.net";
+ // No trailing slash, no path component
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.WithdrawExchange) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.amount, undefined);
+ t.deepEqual(
+ r1.exchangeBaseUrl,
+ "https://exchange.demo.taler.net/" as HostPortPath,
+ );
+ }
+});
+
+test("taler-new withdraw exchange URI (stringify)", (t) => {
+ const url = TalerUris.toString({
+ type: TalerUriAction.WithdrawExchange,
+ exchangeBaseUrl: "https://exchange.demo.taler.net" as HostPortPath,
+ });
+ t.deepEqual(url, "taler://withdraw-exchange/exchange.demo.taler.net/");
+});
+
+test("taler-new withdraw exchange URI with amount (stringify)", (t) => {
+ const url = TalerUris.toString({
+ type: TalerUriAction.WithdrawExchange,
+ exchangeBaseUrl: "https://exchange.demo.taler.net" as HostPortPath,
+ amount: "KUDOS:19" as AmountString,
+ });
+ t.deepEqual(
+ url,
+ "taler://withdraw-exchange/exchange.demo.taler.net/?a=KUDOS%3A19",
+ );
+});
+
+/**
+ * 5.13 action: add-exchange https://lsd.gnunet.org/lsd0006/#name-action-add-exchange
+ */
+
+test("taler-new add exchange URI (parse)", (t) => {
+ {
+ const url1 = "taler://add-exchange/exchange.example.com/";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.AddExchange) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.exchangeBaseUrl,
+ "https://exchange.example.com/" as HostPortPath,
+ );
+ }
+ {
+ const url1 = "taler://add-exchange/exchanges.example.com/api/";
+ const r1 = succeedOrThrow(TalerUris.fromString(url1));
+ if (r1.type !== TalerUriAction.AddExchange) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.exchangeBaseUrl,
+ "https://exchanges.example.com/api/" as HostPortPath,
+ );
+ }
+});
+
+test("taler-new add exchange URI (stringify)", (t) => {
+ const url = TalerUris.toString({
+ type: TalerUriAction.AddExchange,
+ exchangeBaseUrl: "https://exchange.demo.taler.net" as HostPortPath,
+ });
+ t.deepEqual(url, "taler://add-exchange/exchange.demo.taler.net/");
+});
+
+/**
+ * wrong uris
+ */
+test("taler-new pay url parsing: wrong scheme", (t) => {
+ const url1 = "talerfoo://";
+ const r1 = failOrThrow(
+ TalerUris.fromString(url1),
+ TalerUriParseError.WRONG_PREFIX,
+ );
+ // const r1 = parsePayUri(url1);
+ t.is(r1, undefined);
+
+ const url2 = "taler://refund/a/b/c/d/e/f";
+ const r2 = failOrThrow(
+ TalerUris.fromString(url2),
+ TalerUriParseError.INVALID_TARGET_PATH,
+ );
+ // const r2 = parsePayUri(url2);
+ t.is(r2, undefined);
+});
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
@@ -28,7 +28,6 @@ import {
AmountJson,
AmountLike,
Amounts,
- amountToPretty,
assertUnreachable,
BlindedDenominationSignature,
checkDbInvariant,
@@ -479,9 +478,9 @@ export function getTotalRefreshCostInternal(
).amount;
const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
logger.trace(
- `total refresh cost for ${amountToPretty(amountLeft)} is ${amountToPretty(
- totalCost,
- )}`,
+ `total refresh cost for ${Amounts.toPretty(
+ amountLeft,
+ )} is ${Amounts.toPretty(totalCost)}`,
);
return totalCost;
}
@@ -571,7 +570,7 @@ async function initRefreshSession(
if (newCoinDenoms.selectedDenoms.length === 0) {
logger.trace(
- `not refreshing, available amount ${amountToPretty(
+ `not refreshing, available amount ${Amounts.toPretty(
availableAmount,
)} too small`,
);