Add bch-mnemonic protocol

This commit is contained in:
Jim Hamill
2025-06-27 06:44:14 +00:00
parent 1545255a4a
commit f01699a961
5 changed files with 217 additions and 14 deletions
@@ -0,0 +1,167 @@
import { Bytes } from '@xocash/stack';
import { z } from 'zod';
export type BCHMnemonicURLRaw = {
entropy: Uint8Array;
passphrase?: string;
language?: (typeof BCHMnemonicURL.SUPPORTED_LANGUAGES)[number];
comment?: string;
path?: string;
startHeight?: number;
};
/**
* Handles BCHMnemonic URLs
*/
export class BCHMnemonicURL {
static PROTOCOL = 'bch-mnemonic';
/**
* Check if a URL is a valid wallet backup URL
*
* @param url The URL to check
* @returns True if the URL is a valid wallet backup URL, false otherwise
*/
public static canHandle(urlStr: string): boolean {
try {
BCHMnemonicURL.fromURL(urlStr);
return true;
} catch (_error) {
return false;
}
}
/**
* Creates a BCHMnemonic from a URL-encoded string
* @param urlStr - The URL-encoded mnemonic string
* @returns A new BCHMnemonic instance
* @throws Error if the URL format is invalid or entropy is invalid
*/
static fromURL(urlStr: string): BCHMnemonicURL {
const url = new URL(urlStr);
if (url.protocol !== `${BCHMnemonicURL.PROTOCOL}:`) {
throw new Error(`Invalid URL protocol: ${url.protocol}`);
}
// Decode the entropy.
const entropy = Bytes.fromBase64(url.pathname).toUint8Array();
// Pick out our encoding keys from the URL
const params = BCHMnemonicURL.schema.parse(
Object.fromEntries(url.searchParams.entries()),
);
// Create and return the backup with validated parameters
return BCHMnemonicURL.fromRaw({
entropy,
language: params[BCHMnemonicURL.ENCODING_KEYS.language],
comment: params[BCHMnemonicURL.ENCODING_KEYS.comment],
path: params[BCHMnemonicURL.ENCODING_KEYS.path],
startHeight: params[BCHMnemonicURL.ENCODING_KEYS.startHeight],
});
}
/**
* Create a new WalletBackup from a raw object
*
* @param raw - The raw object to create the WalletBackup from
* @returns The created WalletBackup
*/
static fromRaw(raw: BCHMnemonicURLRaw): BCHMnemonicURL {
// Add entropy validation
if (!raw.entropy || raw.entropy.length === 0) {
throw new Error('Invalid entropy: must be non-empty');
}
// Validate entropy length (typically 16, 20, 24, 28, or 32 bytes for BIP39)
const validLengths = [16, 20, 24, 28, 32];
if (!validLengths.includes(raw.entropy.length)) {
throw new Error(`Invalid entropy length: ${raw.entropy.length} bytes`);
}
return new BCHMnemonicURL(raw);
}
constructor(protected raw: BCHMnemonicURLRaw) {}
toObject() {
return this.raw;
}
/**
* Encode the backup into a URL encoding
*
* @param prefix - The prefix to use for the URL encoding
* @returns The URL encoding of the backup
*/
toURL(): string {
// Conver the mnemonic words into the entropy used to derive the mnemonic words
const entropyBase64 = Bytes.from(this.raw.entropy).toBase64();
// Create a new URL object with the prefix and the base64 encoded mnemonic
const url = new URL(`${BCHMnemonicURL.PROTOCOL}:${entropyBase64}`);
// Add the raw values to the url encoded string. Only add the values that are defined.
if (this.raw.language !== undefined) {
url.searchParams.set(
BCHMnemonicURL.ENCODING_KEYS.language,
this.raw.language,
);
}
if (this.raw.comment !== undefined) {
url.searchParams.set(
BCHMnemonicURL.ENCODING_KEYS.comment,
this.raw.comment,
);
}
if (this.raw.path !== undefined) {
url.searchParams.set(BCHMnemonicURL.ENCODING_KEYS.path, this.raw.path);
}
if (this.raw.startHeight !== undefined) {
url.searchParams.set(
BCHMnemonicURL.ENCODING_KEYS.startHeight,
this.raw.startHeight.toString(),
);
}
return url.toString();
}
static ENCODING_KEYS = {
language: 'l',
passphrase: 'p',
comment: 'c',
path: 'd',
startHeight: 'h',
} as const;
static SUPPORTED_LANGUAGES = [
'en',
'zh-CN',
'zh-TW',
'ja',
'es',
'pt',
'ko',
'fr',
'it',
'cs',
] as const;
/**
* Zod schema for validating URL parameters
*/
static schema = z.object({
[BCHMnemonicURL.ENCODING_KEYS.language]: z
.enum(BCHMnemonicURL.SUPPORTED_LANGUAGES)
.optional(),
[BCHMnemonicURL.ENCODING_KEYS.passphrase]: z.string().optional(),
[BCHMnemonicURL.ENCODING_KEYS.comment]: z.string().optional(),
[BCHMnemonicURL.ENCODING_KEYS.path]: z.string().optional(),
[BCHMnemonicURL.ENCODING_KEYS.startHeight]: z.coerce.number().optional(),
});
}
+1
View File
@@ -0,0 +1 @@
export * from './bch-mnemonic-url.js';