Some checks failed
Build and deploy Docs site to GitHub Pages / github-pages (push) Has been cancelled
119 lines
3.4 KiB
JavaScript
119 lines
3.4 KiB
JavaScript
export class VanityKeyGenerator {
|
|
constructor() {
|
|
this.workers = [];
|
|
this.running = false;
|
|
this._resolve = null;
|
|
this._reject = null;
|
|
this._totalAttempts = 0;
|
|
this.onProgress = null;
|
|
}
|
|
|
|
static get numCores() {
|
|
return navigator.hardwareConcurrency || 4;
|
|
}
|
|
|
|
/**
|
|
* Estimate time for a given hex prefix length
|
|
* @param {number} prefixLen - number of hex chars
|
|
* @param {number} keysPerSec - estimated throughput
|
|
* @returns {string} human-readable estimate
|
|
*/
|
|
static estimateTime(prefixLen, keysPerSec) {
|
|
if (prefixLen === 0) return 'instant';
|
|
const expected = Math.pow(16, prefixLen);
|
|
const seconds = expected / keysPerSec;
|
|
|
|
if (seconds < 1) return 'less than a second';
|
|
if (seconds < 60) return `~${Math.ceil(seconds)} seconds`;
|
|
if (seconds < 3600) return `~${Math.ceil(seconds / 60)} minutes`;
|
|
if (seconds < 86400) return `~${Math.ceil(seconds / 3600)} hours`;
|
|
return `~${Math.ceil(seconds / 86400)} days`;
|
|
}
|
|
|
|
get attempts() {
|
|
return this._totalAttempts;
|
|
}
|
|
|
|
/**
|
|
* Start generating a vanity key
|
|
* @param {string} prefix - hex prefix to match (1-6 chars)
|
|
* @returns {Promise<{ privKey: string, pubKey: string, attempts: number } | null>}
|
|
*/
|
|
generate(prefix) {
|
|
if (this.running) throw new Error('Already running');
|
|
|
|
prefix = prefix.replace(/[^0-9a-fA-F]/g, '');
|
|
if (prefix.length === 0 || prefix.length > 6) {
|
|
throw new Error('Prefix must be 1-6 hex characters');
|
|
}
|
|
|
|
this.running = true;
|
|
this._totalAttempts = 0;
|
|
const numWorkers = VanityKeyGenerator.numCores;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this._resolve = resolve;
|
|
this._reject = reject;
|
|
|
|
for (let i = 0; i < numWorkers; i++) {
|
|
const worker = new Worker(
|
|
new URL('./vanity-key-worker.js', import.meta.url),
|
|
{ type: 'module' }
|
|
);
|
|
|
|
worker.onmessage = (e) => {
|
|
if (!this.running) return;
|
|
const data = e.data;
|
|
|
|
if (data.type === 'progress') {
|
|
this._totalAttempts += data.attempts;
|
|
if (this.onProgress) this.onProgress(this._totalAttempts);
|
|
} else if (data.type === 'match') {
|
|
this._totalAttempts += data.attempts;
|
|
const result = {
|
|
privKey: data.privKey,
|
|
pubKey: data.pubKey,
|
|
attempts: this._totalAttempts,
|
|
};
|
|
this._stopWorkers();
|
|
this.running = false;
|
|
resolve(result);
|
|
} else if (data.type === 'error') {
|
|
this._stopWorkers();
|
|
this.running = false;
|
|
reject(new Error(data.message));
|
|
} else if (data.type === 'stopped') {
|
|
this._totalAttempts += data.attempts;
|
|
}
|
|
};
|
|
|
|
worker.onerror = (err) => {
|
|
this._stopWorkers();
|
|
this.running = false;
|
|
reject(new Error(err.message || 'Worker error'));
|
|
};
|
|
|
|
worker.postMessage({ type: 'start', prefix, progressInterval: 200 });
|
|
this.workers.push(worker);
|
|
}
|
|
});
|
|
}
|
|
|
|
_stopWorkers() {
|
|
for (const worker of this.workers) {
|
|
try { worker.postMessage({ type: 'stop' }); } catch (e) {}
|
|
setTimeout(() => worker.terminate(), 500);
|
|
}
|
|
this.workers = [];
|
|
}
|
|
|
|
cancel() {
|
|
const reject = this._reject;
|
|
this._stopWorkers();
|
|
this.running = false;
|
|
if (reject) reject(new Error('Cancelled'));
|
|
this._resolve = null;
|
|
this._reject = null;
|
|
}
|
|
}
|