/* 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 */ import { generateFakeSegwitAddress } from "./bitcoin.js"; import { Codec, Context, DecodingError, renderContext } from "./codec.js"; import { URLSearchParams } from "./url.js"; export type PaytoUri = | PaytoUriUnknown | PaytoUriIBAN | PaytoUriTalerBank | PaytoUriBitcoin; declare const __payto_str: unique symbol; export type PaytoString = string & { [__payto_str]: true }; export function codecForPaytoString(): Codec { 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; } 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 */ export function addPaytoQueryParams( s: string, params: { [name: string]: string }, ): string { const [acct, search] = s.slice(paytoPfx.length).split("?"); const searchParams = new URLSearchParams(search || ""); 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; } const [acct, search] = s.slice(paytoPfx.length).split("?"); const firstSlashPos = acct.indexOf("/"); if (firstSlashPos === -1) { return undefined; } const targetType = acct.slice(0, firstSlashPos); const targetPath = acct.slice(firstSlashPos + 1); const params: { [k: string]: string } = {}; const searchParams = new URLSearchParams(search || ""); searchParams.forEach((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}`; }