You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

179 lines
6.7 KiB

/**
* payto: URI handling (RFC-8905 / NIP-A3)
* Parse and normalize payto://<type>/<authority> URIs; known types for UI (icons, labels, dialogs).
*/
export const PAYTO_URI_REGEX = /payto:\/\/([a-z0-9-]+)\/([^\s\]\)\<\"']+)/gi
export interface ParsedPayto {
type: string
authority: string
raw: string
}
/**
* Parse a payto URI into type and authority. Returns null if invalid.
*/
export function parsePaytoUri(uri: string): ParsedPayto | null {
const trimmed = uri.trim()
const m = /^payto:\/\/([a-z0-9-]+)\/(.+)$/i.exec(trimmed)
if (!m) return null
const typeRaw = m[1].toLowerCase()
const authority = decodeURIComponent(m[2].replace(/\+/g, ' '))
if (!typeRaw || !authority) return null
const type = getCanonicalPaytoType(typeRaw)
return { type, authority, raw: trimmed }
}
/**
* Build payto URI from type and authority.
*/
export function buildPaytoUri(type: string, authority: string): string {
const t = type.toLowerCase().replace(/[^a-z0-9-]/g, '')
const a = encodeURIComponent(authority.trim())
return `payto://${t}/${a}`
}
/** Known payment types: NIP-A3 recommended + common extras (crypto, fiat, tipping) */
export const PAYTO_KNOWN_TYPES: Record<
string,
{ label: string; symbol?: string; category: 'bitcoin' | 'crypto' | 'stablecoin' | 'fiat' | 'lightning' | 'tip' }
> = {
bitcoin: { label: 'Bitcoin', symbol: '₿', category: 'bitcoin' },
sats: { label: 'Satoshis', symbol: '丰', category: 'bitcoin' },
lightning: { label: 'Lightning Network', symbol: '⚡', category: 'lightning' },
ethereum: { label: 'Ethereum', symbol: 'Ξ', category: 'crypto' },
monero: { label: 'Monero', symbol: 'ɱ', category: 'crypto' },
nano: { label: 'Nano', symbol: 'Ӿ', category: 'crypto' },
cashme: { label: 'Cash App', symbol: '$', category: 'fiat' },
revolut: { label: 'Revolut', symbol: '💳', category: 'fiat' },
venmo: { label: 'Venmo', symbol: '$', category: 'fiat' },
// Common crypto
'bitcoin-cash': { label: 'Bitcoin Cash', symbol: '₿', category: 'crypto' },
dogecoin: { label: 'Dogecoin', symbol: 'Ð', category: 'crypto' },
litecoin: { label: 'Litecoin', symbol: 'Ł', category: 'crypto' },
usdt: { label: 'Tether', symbol: '₮', category: 'stablecoin' },
usdc: { label: 'USD Coin', symbol: '◎', category: 'stablecoin' },
dai: { label: 'Dai', symbol: '◈', category: 'crypto' },
euroc: { label: 'Euro Coin', symbol: '€', category: 'stablecoin' },
solana: { label: 'Solana', symbol: '◎', category: 'crypto' },
// Tipping / donation
paypal: { label: 'PayPal', symbol: '💙', category: 'fiat' },
buymeacoffee: { label: 'Buy Me a Coffee', symbol: '☕', category: 'tip' },
'ko-fi': { label: 'Ko-fi', symbol: '☕', category: 'tip' },
kofi: { label: 'Ko-fi', symbol: '☕', category: 'tip' },
patreon: { label: 'Patreon', symbol: '🎭', category: 'tip' },
github: { label: 'GitHub Sponsors', symbol: '🐙', category: 'tip' },
// Fiat / wallets
'apple-pay': { label: 'Apple Pay', symbol: '🍎', category: 'fiat' },
'google-pay': { label: 'Google Pay', symbol: 'G', category: 'fiat' },
// Crowdfunding / fundraising
geyser: { label: 'Geyser Fund', symbol: '⛲', category: 'tip' },
gofundme: { label: 'GoFundMe', symbol: '🎯', category: 'tip' },
kickstarter: { label: 'Kickstarter', symbol: '🚀', category: 'tip' }
}
/**
* Short labels accepted after payto:// that map to a canonical type.
* e.g. payto://BTC/..., payto://LBTC/..., payto://DOGE/... are recognized as bitcoin, lightning, dogecoin.
*/
export const PAYTO_TYPE_ALIASES: Record<string, string> = {
btc: 'bitcoin',
lbtc: 'lightning',
doge: 'dogecoin',
eth: 'ethereum',
xmr: 'monero',
ltc: 'litecoin',
xno: 'nano',
sol: 'solana',
bch: 'bitcoin-cash'
}
export function getCanonicalPaytoType(type: string): string {
const key = type.toLowerCase().trim()
return PAYTO_TYPE_ALIASES[key] ?? key
}
/** Icon character/symbol for known types; null for unknown (render HelpCircle or ?) */
export function getPaytoIconChar(type: string): string | null {
const info = getPaytoTypeInfo(type)
return info?.symbol ?? null
}
/** Logo filename in /payto_logos/ for types that have an asset. Any image format works: .svg, .gif, .jpg, .png, .webp, etc. */
export const PAYTO_LOGO_FILES: Record<string, string> = {
ethereum: 'ethereum-eth-logo.svg',
monero: 'Monero.png',
litecoin: 'Litecoin.png',
dogecoin: 'dogecoin-doge-logo.svg',
usdt: 'tether-usdt-logo.svg',
usdc: 'usd-coin-usdc-logo.svg',
dai: 'multi-collateral-dai-dai-logo.svg',
euroc: 'EurC.png',
solana: 'solana.png',
bnb: 'BNB.png',
tron: 'Tron.png',
xrp: 'XRP.gif',
'bitcoin-cash': 'bitcoin-cash-bch-logo.svg',
cashme: 'cashapp.webp',
venmo: 'venmo.png',
paypal: 'paypal.webp',
revolut: 'revolut.webp',
buymeacoffee: 'buymeacoffee.png',
'ko-fi': 'ko-fi.png',
kofi: 'ko-fi.png',
patreon: 'patreon.png',
github: 'github_sponsors.png',
'apple-pay': 'apple_pay.svg',
'google-pay': 'google_pay.jpeg',
geyser: 'geyser_fund.webp',
gofundme: 'gofundme.jpeg',
kickstarter: 'kickstarter.webp'
}
/** Profile/page URL template for types that have a web profile. Use {authority} as placeholder. Null = no direct link. */
export const PAYTO_PROFILE_URL_TEMPLATES: Record<string, string> = {
paypal: 'https://paypal.me/{authority}',
venmo: 'https://venmo.com/{authority}',
revolut: 'https://revolut.me/{authority}',
buymeacoffee: 'https://buymeacoffee.com/{authority}',
'ko-fi': 'https://ko-fi.com/{authority}',
kofi: 'https://ko-fi.com/{authority}',
patreon: 'https://patreon.com/{authority}',
github: 'https://github.com/sponsors/{authority}',
geyser: 'https://geyser.fund/project/{authority}',
gofundme: 'https://www.gofundme.com/f/{authority}',
kickstarter: 'https://www.kickstarter.com/projects/{authority}',
cashme: 'https://cash.app/{authority}'
}
export function getPaytoProfileUrl(type: string, authority: string): string | null {
const key = type.toLowerCase()
const template = PAYTO_PROFILE_URL_TEMPLATES[key]
if (!template || !authority) return null
return template.replace('{authority}', encodeURIComponent(authority.trim()))
}
export function getPaytoLogoPath(type: string): string | null {
const key = type.toLowerCase()
const file = PAYTO_LOGO_FILES[key]
if (!file) return null
return `/payto_logos/${file}`
}
export function getPaytoTypeInfo(type: string): (typeof PAYTO_KNOWN_TYPES)[string] | undefined {
return PAYTO_KNOWN_TYPES[getCanonicalPaytoType(type)]
}
export function isKnownPaytoType(type: string): boolean {
return getCanonicalPaytoType(type) in PAYTO_KNOWN_TYPES
}
/** Check if type is lightning (opens Zap flow when pubkey available) */
export function isLightningPaytoType(type: string): boolean {
return getCanonicalPaytoType(type) === 'lightning'
}