diff options
Diffstat (limited to 'packages/taler-util/src/payto.ts')
-rw-r--r-- | packages/taler-util/src/payto.ts | 230 |
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}`; +} |