commit 616f7b3beadd0a103b8cfc0b2bc755810ebea117
parent 4db1a1fecefb5c373077223321a6688e3fc2719c
Author: Sebastian <sebasjm@taler-systems.com>
Date: Mon, 12 Jan 2026 19:03:52 -0300
fix #10867
Diffstat:
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");
+ }
+});
+