taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit cb3be034e3b6bd7422be8961485a6085d16c7190
parent 9f889207ddad3b786e4b30ea076f3ec24591a9da
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed,  1 Oct 2025 17:55:06 -0300

fixes #8526

Diffstat:
Mpackages/taler-util/src/amounts.ts | 6+++---
Mpackages/taler-util/src/payto.ts | 32+++++++++++++++++++++++++++-----
Mpackages/taler-util/src/taleruri.test.ts | 1011++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mpackages/taler-util/src/taleruri.ts | 350++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Apackages/taler-util/src/taleruris.test.ts | 608+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/refresh.ts | 9++++-----
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`, );