summaryrefslogtreecommitdiff
path: root/packages/taler-util/src/payto.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src/payto.ts')
-rw-r--r--packages/taler-util/src/payto.ts230
1 files changed, 226 insertions, 4 deletions
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index 504db533b..a471d0b87 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -14,16 +14,135 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { generateFakeSegwitAddress } from "./bitcoin.js";
+import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { URLSearchParams } from "./url.js";
-interface PaytoUri {
- targetType: string;
+export type PaytoUri =
+ | PaytoUriUnknown
+ | PaytoUriIBAN
+ | PaytoUriTalerBank
+ | PaytoUriBitcoin;
+
+declare const __payto_str: unique symbol;
+export type PaytoString = string & { [__payto_str]: true };
+
+export function codecForPaytoString(): Codec<PaytoString> {
+ return {
+ decode(x: any, c?: Context): PaytoString {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (!x.startsWith(paytoPfx)) {
+ throw new DecodingError(
+ `expected start with payto at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ return x as PaytoString;
+ },
+ };
+}
+
+export interface PaytoUriGeneric {
+ targetType: PaytoType | string;
targetPath: string;
params: { [name: string]: string };
}
+export interface PaytoUriUnknown extends PaytoUriGeneric {
+ isKnown: false;
+}
+
+export interface PaytoUriIBAN extends PaytoUriGeneric {
+ isKnown: true;
+ targetType: "iban";
+ iban: string;
+ bic?: string;
+}
+
+export interface PaytoUriTalerBank extends PaytoUriGeneric {
+ isKnown: true;
+ targetType: "x-taler-bank";
+ host: string;
+ account: string;
+}
+
+export interface PaytoUriBitcoin extends PaytoUriGeneric {
+ isKnown: true;
+ targetType: "bitcoin";
+ address: string;
+ segwitAddrs: Array<string>;
+}
+
const paytoPfx = "payto://";
+export type PaytoType = "iban" | "bitcoin" | "x-taler-bank";
+
+export function buildPayto(
+ type: "iban",
+ iban: string,
+ bic: string | undefined,
+): PaytoUriIBAN;
+export function buildPayto(
+ type: "bitcoin",
+ address: string,
+ reserve: string | undefined,
+): PaytoUriBitcoin;
+export function buildPayto(
+ type: "x-taler-bank",
+ host: string,
+ account: string,
+): PaytoUriTalerBank;
+export function buildPayto(
+ type: PaytoType,
+ first: string,
+ second?: string,
+): PaytoUriGeneric {
+ switch (type) {
+ case "bitcoin": {
+ const uppercased = first.toUpperCase();
+ const result: PaytoUriBitcoin = {
+ isKnown: true,
+ targetType: "bitcoin",
+ targetPath: first,
+ address: uppercased,
+ params: {},
+ segwitAddrs: !second ? [] : generateFakeSegwitAddress(second, first),
+ };
+ return result;
+ }
+ case "iban": {
+ const uppercased = first.toUpperCase();
+ const result: PaytoUriIBAN = {
+ isKnown: true,
+ targetType: "iban",
+ iban: uppercased,
+ params: {},
+ targetPath: !second ? uppercased : `${second}/${uppercased}`,
+ };
+ return result;
+ }
+ case "x-taler-bank": {
+ if (!second) throw Error("missing account for payto://x-taler-bank");
+ const result: PaytoUriTalerBank = {
+ isKnown: true,
+ targetType: "x-taler-bank",
+ host: first,
+ account: second,
+ params: {},
+ targetPath: `${first}/${second}`,
+ };
+ return result;
+ }
+ default: {
+ const unknownType: never = type;
+ throw Error(`unknown payto:// type ${unknownType}`);
+ }
+ }
+}
+
/**
* Add query parameters to a payto URI
*/
@@ -33,12 +152,38 @@ export function addPaytoQueryParams(
): string {
const [acct, search] = s.slice(paytoPfx.length).split("?");
const searchParams = new URLSearchParams(search || "");
- for (const k of Object.keys(params)) {
+ const keys = Object.keys(params);
+ if (keys.length === 0) {
+ return paytoPfx + acct;
+ }
+ for (const k of keys) {
searchParams.set(k, params[k]);
}
return paytoPfx + acct + "?" + searchParams.toString();
}
+/**
+ * Serialize a PaytoURI into a valid payto:// string
+ *
+ * @param p
+ * @returns
+ */
+export function stringifyPaytoUri(p: PaytoUri): PaytoString {
+ const url = new URL(`${paytoPfx}${p.targetType}/${p.targetPath}`);
+ const paramList = !p.params ? [] : Object.entries(p.params);
+ paramList.forEach(([key, value]) => {
+ url.searchParams.set(key, value);
+ });
+ return url.href as PaytoString;
+}
+
+/**
+ * Parse a valid payto:// uri into a PaytoUri object
+ * RFC 8905
+ *
+ * @param s
+ * @returns
+ */
export function parsePaytoUri(s: string): PaytoUri | undefined {
if (!s.startsWith(paytoPfx)) {
return undefined;
@@ -60,12 +205,89 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
const searchParams = new URLSearchParams(search || "");
searchParams.forEach((v, k) => {
- params[v] = k;
+ params[k] = v;
});
+ if (targetType === "x-taler-bank") {
+ const parts = targetPath.split("/");
+ const host = parts[0];
+ const account = parts[1];
+ return {
+ targetPath,
+ targetType,
+ params,
+ isKnown: true,
+ host,
+ account,
+ };
+ }
+ if (targetType === "iban") {
+ const parts = targetPath.split("/");
+ let iban: string | undefined = undefined;
+ let bic: string | undefined = undefined;
+ if (parts.length === 1) {
+ iban = parts[0].toUpperCase();
+ }
+ if (parts.length === 2) {
+ bic = parts[0];
+ iban = parts[1].toUpperCase();
+ } else {
+ iban = targetPath.toUpperCase();
+ }
+ return {
+ isKnown: true,
+ targetPath,
+ targetType,
+ params,
+ iban,
+ bic,
+ };
+ }
+ if (targetType === "bitcoin") {
+ const msg = /\b([A-Z0-9]{52})\b/.exec(params["message"]);
+ const reserve = !msg ? params["subject"] : msg[0];
+ const segwitAddrs = !reserve
+ ? []
+ : generateFakeSegwitAddress(reserve, targetPath);
+
+ const uppercased = targetType.toUpperCase();
+ const result: PaytoUriBitcoin = {
+ isKnown: true,
+ targetPath,
+ targetType,
+ address: uppercased,
+ params,
+ segwitAddrs,
+ };
+
+ return result;
+ }
return {
targetPath,
targetType,
params,
+ isKnown: false,
};
}
+
+export function talerPaytoFromExchangeReserve(
+ exchangeBaseUrl: string,
+ reservePub: string,
+): string {
+ const url = new URL(exchangeBaseUrl);
+ let proto: string;
+ if (url.protocol === "http:") {
+ proto = "taler-reserve-http";
+ } else if (url.protocol === "https:") {
+ proto = "taler-reserve";
+ } else {
+ throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
+ }
+
+ let path = url.pathname;
+ if (!path.endsWith("/")) {
+ path = path + "/";
+ }
+
+ return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
+}