143 lines
3.4 KiB
TypeScript
143 lines
3.4 KiB
TypeScript
import { Address, Satoshis } from '@xocash/primitives';
|
|
import { z } from 'zod/v4';
|
|
|
|
import { hasDuplicateParams } from '../utils/misc.js';
|
|
|
|
export type BIP21Raw = {
|
|
address: Address;
|
|
satoshis: Satoshis;
|
|
message?: string;
|
|
};
|
|
|
|
export class BIP21 {
|
|
/**
|
|
* Checks if the URL is a valid BIP21 URL.
|
|
*
|
|
* @param url - The URL to check
|
|
* @returns True if the URL is a valid BIP21 URL, false otherwise
|
|
*/
|
|
static canHandle(url: string | URL): boolean {
|
|
try {
|
|
BIP21.from(url);
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a Bip21 instance from a URL.
|
|
*
|
|
* @param url - The URL to create a Bip21 instance from
|
|
* @returns A Bip21 instance
|
|
* @throws {Error} If the URL is invalid, has duplicate parameters, or has a request URL (r)
|
|
*/
|
|
static from(url: string | URL): BIP21 {
|
|
// If it's a string, convert it to a URL
|
|
if (typeof url === 'string') {
|
|
url = new URL(url);
|
|
}
|
|
|
|
// Check the url scheme is bitcoincash
|
|
if (url.protocol !== 'bitcoincash:') {
|
|
throw new Error('Invalid url scheme');
|
|
}
|
|
|
|
// Check for duplicate params
|
|
if (hasDuplicateParams(url)) {
|
|
throw new Error('Duplicate params');
|
|
}
|
|
|
|
// Make sure the address is valid
|
|
const address = Address.fromCashAddr(url.pathname);
|
|
|
|
// Get the params
|
|
const params = BIP21.schema.parse(Object.fromEntries(url.searchParams));
|
|
|
|
// If we have a request URL (for JPP, BIP70, etc), we don't want to handle it here.
|
|
if (params.r) {
|
|
throw new Error(
|
|
'Request URL (r) is not supported in the BIP21 Payment URL protocol',
|
|
);
|
|
}
|
|
|
|
// Convert the amount to sats
|
|
const requestedAmount = params.amount
|
|
? Satoshis.fromBCH(params.amount)
|
|
: Satoshis.fromSats(0);
|
|
|
|
return new BIP21({
|
|
address,
|
|
satoshis: requestedAmount,
|
|
message: params.message,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates a Bip21 instance from a raw object.
|
|
*
|
|
* @param raw - The raw object to create a Bip21 instance from
|
|
* @returns A Bip21 instance
|
|
*/
|
|
static fromRaw(raw: BIP21Raw): BIP21 {
|
|
return new BIP21(raw);
|
|
}
|
|
|
|
constructor(public readonly params: BIP21Raw) {}
|
|
|
|
/**
|
|
* Converts the Bip21 instance to a raw object.
|
|
*
|
|
* @returns A raw object with the address, satoshis, and message
|
|
*/
|
|
toRaw() {
|
|
return this.params;
|
|
}
|
|
|
|
/**
|
|
* Converts the Bip21 instance to a URL.
|
|
*
|
|
* @returns A URL string with the bitcoincash: protocol
|
|
*/
|
|
toUrl(): string {
|
|
const address = this.params.address;
|
|
|
|
const url = new URL(address.toCashAddr('bitcoincash'));
|
|
|
|
const paramMappings: [
|
|
keyof typeof BIP21.ENCODING_KEYS,
|
|
keyof typeof this.params,
|
|
(v: Satoshis | Address | string) => string,
|
|
][] = [
|
|
[
|
|
'amount',
|
|
'satoshis',
|
|
(v) => (v instanceof Satoshis ? v.toBCH().toString() : v.toString()),
|
|
],
|
|
['message', 'message', String],
|
|
];
|
|
|
|
for (const [key, paramKey, transform] of paramMappings) {
|
|
const value = this.params[paramKey];
|
|
if (value != null) {
|
|
url.searchParams.set(BIP21.ENCODING_KEYS[key], transform(value));
|
|
}
|
|
}
|
|
|
|
return url.toString();
|
|
}
|
|
|
|
static ENCODING_KEYS = {
|
|
amount: 'amount',
|
|
message: 'message',
|
|
};
|
|
|
|
static schema = z
|
|
.object({
|
|
amount: z.coerce.number().optional(),
|
|
message: z.string().optional(),
|
|
t: z.string().optional(),
|
|
})
|
|
.catchall(z.string()); // Allow any unknown keys with string values
|
|
}
|