taler-typescript-core

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

commit 616f7b3beadd0a103b8cfc0b2bc755810ebea117
parent 4db1a1fecefb5c373077223321a6688e3fc2719c
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Mon, 12 Jan 2026 19:03:52 -0300

fix #10867

Diffstat:
Mpackages/taler-util/src/payto.test.ts | 33+++++++++++++++++++++++++++++++--
Mpackages/taler-util/src/payto.ts | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mpackages/taler-util/src/paytos.test.ts | 35++++++++++++++++++++++++++++++++++-
3 files changed, 145 insertions(+), 5 deletions(-)

diff --git a/packages/taler-util/src/payto.test.ts b/packages/taler-util/src/payto.test.ts @@ -38,10 +38,10 @@ test("basic payto parsing", (t) => { test("basic x-taler-bank payto string", (t) => { const result = parsePaytoUri( - "payto://x-taler-bank/bank.demo.taler.net/accountName", + "payto://x-taler-bank/bank.demo.taler.net/asd/accountName", ); t.is(result?.targetType, "x-taler-bank"); - t.is(result?.targetPath, "bank.demo.taler.net/accountName"); + t.is(result?.targetPath, "bank.demo.taler.net/asd/accountName"); if (!result) { t.fail(); throw Error(); @@ -85,3 +85,32 @@ test("adding payto query params", (t) => { "payto://iban/DE1231231231?receiver-name=John%20Doe&foo=42", ); }); + +test("parsing payto and stringify again but with cyclos", (t) => { + const payto1 = + "payto://cyclos/communities.cyclos.org/utrecht/31000163100000000?reciever-name=John%20Doe" as PaytoString; + + t.is(stringifyPaytoUri(parsePaytoUri(payto1)!), payto1); +}); + +test("basic cyclos payto string", (t) => { + const result = parsePaytoUri( + "payto://cyclos/communities.cyclos.org/utrecht/31000163100000000", + ); + t.is(result?.targetType, "cyclos"); + t.is(result?.targetPath, "communities.cyclos.org/utrecht/31000163100000000"); + if (!result) { + t.fail(); + throw Error(); + } + if (!result.isKnown) { + t.fail(); + throw Error(); + } + if (result.targetType !== "cyclos") { + t.fail(); + throw Error(); + } + t.is(result.host, "communities.cyclos.org"); + t.is(result.account, "31000163100000000"); +}); diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts @@ -43,6 +43,7 @@ const PAYTO_PREFIX = "payto://"; export enum PaytoType { IBAN = "iban", Bitcoin = "bitcoin", + Cyclos = "cyclos", TalerBank = "x-taler-bank", TalerReserve = "taler-reserve", TalerReserveHttp = "taler-reserve-http", @@ -96,6 +97,7 @@ export namespace Paytos { | PaytoUnsupported | PaytoIBAN | PaytoTalerReserve + | PaytoCyclos | PaytoTalerReserveHttp | PaytoTalerBank | PaytoEthereum @@ -147,6 +149,12 @@ export namespace Paytos { reservePub: Uint8Array; } + export interface PaytoCyclos extends PaytoGeneric { + targetType: PaytoType.Cyclos; + url: HostPortPath; + account: string; + } + export interface PaytoTalerReserveHttp extends PaytoGeneric { targetType: PaytoType.TalerReserveHttp; exchange: HostPortPath; @@ -183,6 +191,7 @@ export namespace Paytos { "taler-reserve": true, "taler-reserve-http": true, ethereum: true, + cyclos: true, }; export function hash(p: NormalizedPaytoString | FullPaytoString): Uint8Array { @@ -406,6 +415,22 @@ export namespace Paytos { displayName: `${path}@${pub}`, }; } + export function createCyclos( + url: HostPortPath, + account: string, + params: Record<string, string> = {}, + ): PaytoCyclos { + const path = withoutScheme(url); + return { + targetType: PaytoType.Cyclos, + url, + account, + params, + normalizedPath: `${path.toLocaleLowerCase()}${account}`, + fullPath: `${path}${account}`, + displayName: `${account}@${path}`, + }; + } export function createTalerReserveHttp( exchange: HostPortPath, reservePub: Uint8Array, @@ -673,6 +698,31 @@ export namespace Paytos { createEthereum(address ?? (cs[0] as EthAddrString), params), ); } + case PaytoType.Cyclos: { + if (cs.length < 2) { + return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, { + targetType, + }); + } + const host = parseHostPortPath2(cs[0], cs.slice(1, -1).join("/")); + if (!opts.ignoreComponentError && !host) { + return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, { + pos: 0 as const, + targetType, + error: host, + }); + } + + const accountId = cs[cs.length - 1]; + + return opFixedSuccess<URI>( + createCyclos( + host ?? (cs[0] as HostPortPath), + accountId, + params, + ), + ); + } default: { if (opts.allowUnsupported) { return opFixedSuccess<URI>( @@ -744,6 +794,7 @@ export type PaytoUri = | PaytoUriUnknown | PaytoUriIBAN | PaytoUriTaler + | PaytoUriCyclos | PaytoUriTalerHttp | PaytoUriTalerBank | PaytoUriEthereum @@ -846,6 +897,16 @@ export interface PaytoUriEthereum extends PaytoUriGeneric { } /** + * @deprecated use Paytos namespace + */ +export interface PaytoUriCyclos extends PaytoUriGeneric { + isKnown: true; + targetType: "cyclos"; + host: string; + account: string; +} + +/** * Add query parameters to a payto URI. * * Existing parameters are preserved. @@ -946,6 +1007,9 @@ export function hashNormalizedPaytoUri(p: PaytoUri | string): Uint8Array { case "ethereum": paytoStr = `payto://ethereum/${p.address}`; break; + case "cyclos": + paytoStr = `payto://cyclos/${p.host}/${p.account}`; + break; case "taler-reserve": paytoStr = `payto://taler-reserve/${p.exchange}/${p.reservePub}`; break; @@ -1088,7 +1152,7 @@ export function parsePaytoUri(s: string): PaytoUri | undefined { case "x-taler-bank": { const parts = targetPath.split("/"); const host = parts[0]; - const account = parts[1]; + const account = parts[parts.length-1]; return { targetPath, targetType, @@ -1098,6 +1162,21 @@ export function parsePaytoUri(s: string): PaytoUri | undefined { account, }; } + case "cyclos": { + const parts = targetPath.split("/"); + const host = parts[0]; + const account = parts[parts.length-1]; + const result: PaytoUriCyclos = { + isKnown: true, + targetPath, + targetType, + host, + account, + params, + }; + + return result; + } case "taler-reserve": { const parts = targetPath.split("/"); const exchange = parts[0]; @@ -1119,7 +1198,6 @@ export function parsePaytoUri(s: string): PaytoUri | undefined { address: targetPath, params, }; - return result; } default: { diff --git a/packages/taler-util/src/paytos.test.ts b/packages/taler-util/src/paytos.test.ts @@ -99,9 +99,42 @@ test("adding payto query params", (t) => { const p = succeedOrThrow(Paytos.fromString(payto1)); p.params["foo"] = "42"; - + t.deepEqual( Paytos.toFullString(p), "payto://iban/DE1231231231?receiver-name=John%20Doe&foo=42", ); }); + +test("basic cyclos payto string", (t) => { + { + const result = succeedOrThrow( + Paytos.fromString("payto://cyclos/demo.cyclos.org/31000163100000000?receiver-name=John%20Doe"), + ); + + if (result.targetType !== PaytoType.Cyclos) { + t.fail(); + throw Error(); + } + t.is(result.normalizedPath, "demo.cyclos.org/31000163100000000"); + t.is(result.url, "https://demo.cyclos.org/" as HostPortPath); + t.is(result.account, "31000163100000000"); + t.is(result.params["receiver-name"], "John Doe"); + } + + { + const result = succeedOrThrow( + Paytos.fromString("payto://cyclos/communities.cyclos.org/utrecht/31000163100000000?receiver-name=John%20Doe"), + ); + + if (result.targetType !== PaytoType.Cyclos) { + t.fail(); + throw Error(); + } + t.is(result.normalizedPath, "communities.cyclos.org/utrecht/31000163100000000"); + t.is(result.url, "https://communities.cyclos.org/utrecht/" as HostPortPath); + t.is(result.account, "31000163100000000"); + t.is(result.params["receiver-name"], "John Doe"); + } +}); +