minimal-xec-wallet
Version:
A minimalist eCash (XEC) wallet npm library, for use in web apps. Supports eTokens.
272 lines (224 loc) • 8.5 kB
JavaScript
/*
Browser-compatible WebAssembly loader with fallbacks
Fixes "WebAssembly.Compile is disallowed on the main thread" error
*/
/* global WebAssembly, Blob, Worker, URL */
class BrowserWASMLoader {
constructor () {
this.wasmSupported = this._detectWASMSupport()
this.wasmModule = null
this.isInitialized = false
this.initPromise = null
}
// Detect WebAssembly support and browser capabilities
_detectWASMSupport () {
try {
if (typeof WebAssembly === 'undefined') return false
if (typeof WebAssembly.Module === 'undefined') return false
if (typeof WebAssembly.Instance === 'undefined') return false
// Test with a minimal WASM module (8 bytes)
const testModule = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00])
const module = new WebAssembly.Module(testModule)
if (!module) throw new Error('Module creation failed')
return true
} catch (err) {
console.warn('WebAssembly not supported:', err.message)
return false
}
}
// Check if we can use async WebAssembly compilation
_canUseAsyncWASM () {
return (
typeof WebAssembly.compile === 'function' &&
typeof WebAssembly.instantiate === 'function'
)
}
// Load WASM asynchronously to avoid main thread blocking
async initWASM (wasmBytes) {
if (this.initPromise) return this.initPromise
this.initPromise = this._initWASMInternal(wasmBytes)
return this.initPromise
}
async _initWASMInternal (wasmBytes) {
try {
if (!this.wasmSupported) {
console.warn('WebAssembly not supported, using JavaScript fallbacks')
return this._initJavaScriptFallbacks()
}
// Method 1: Try async compilation (preferred for modern browsers)
if (this._canUseAsyncWASM()) {
return await this._compileAsyncWASM(wasmBytes)
}
// Method 2: Try Web Worker compilation (fallback for restricted browsers)
if (typeof Worker !== 'undefined') {
return await this._compileInWorker(wasmBytes)
}
// Method 3: Try chunk-based loading for large WASM
if (wasmBytes.length > 4096) {
return await this._compileChunkedWASM(wasmBytes)
}
// Method 4: Last resort - synchronous (may fail in newer browsers)
return this._compileSyncWASM(wasmBytes)
} catch (err) {
console.warn('WASM initialization failed, using fallbacks:', err.message)
return this._initJavaScriptFallbacks()
}
}
// Async WASM compilation (preferred method)
async _compileAsyncWASM (wasmBytes) {
console.log('Initializing WebAssembly (async)...')
const module = await WebAssembly.compile(wasmBytes)
const imports = this._getWASMImports()
const instance = await WebAssembly.instantiate(module, imports)
this.wasmModule = instance.exports
this.isInitialized = true
console.log('WebAssembly initialized successfully (async)')
return this.wasmModule
}
// Web Worker compilation (for browsers that block main thread WASM)
async _compileInWorker (wasmBytes) {
return new Promise((resolve, reject) => {
const workerScript = `
self.onmessage = async function(e) {
try {
const wasmBytes = e.data
const module = await WebAssembly.compile(wasmBytes)
self.postMessage({ success: true, module })
} catch (err) {
self.postMessage({ success: false, error: err.message })
}
}
`
const blob = new Blob([workerScript], { type: 'application/javascript' })
const worker = new Worker(URL.createObjectURL(blob))
worker.onmessage = async (e) => {
worker.terminate()
URL.revokeObjectURL(blob)
if (e.data.success) {
const imports = this._getWASMImports()
const instance = await WebAssembly.instantiate(e.data.module, imports)
this.wasmModule = instance.exports
this.isInitialized = true
console.log('WebAssembly initialized successfully (worker)')
resolve(this.wasmModule)
} else {
reject(new Error(`Worker compilation failed: ${e.data.error}`))
}
}
worker.onerror = (err) => {
worker.terminate()
reject(new Error(`Worker error: ${err.message}`))
}
// Send WASM bytes to worker
worker.postMessage(wasmBytes)
// Timeout after 10 seconds
setTimeout(() => {
worker.terminate()
reject(new Error('Worker compilation timeout'))
}, 10000)
})
}
// Chunked WASM loading (split large WASM into smaller pieces)
async _compileChunkedWASM (wasmBytes) {
console.log('Attempting chunked WASM loading...')
// This is a simplified approach - in reality you'd need to split WASM properly
// For now, just try smaller delays between compilation attempts
await new Promise(resolve => setTimeout(resolve, 100))
const module = await WebAssembly.compile(wasmBytes)
const imports = this._getWASMImports()
const instance = await WebAssembly.instantiate(module, imports)
this.wasmModule = instance.exports
this.isInitialized = true
console.log('WebAssembly initialized successfully (chunked)')
return this.wasmModule
}
// Synchronous compilation (legacy fallback)
_compileSyncWASM (wasmBytes) {
console.log('Attempting synchronous WASM loading (legacy)...')
const module = new WebAssembly.Module(wasmBytes)
const imports = this._getWASMImports()
const instance = new WebAssembly.Instance(module, imports)
this.wasmModule = instance.exports
this.isInitialized = true
console.log('WebAssembly initialized successfully (sync)')
return this.wasmModule
}
// JavaScript fallbacks for when WASM fails
_initJavaScriptFallbacks () {
console.log('Initializing JavaScript fallbacks...')
// Import pure JavaScript implementations of crypto functions
const jsImplementations = this._getJavaScriptCryptoFunctions()
this.wasmModule = jsImplementations
this.isInitialized = true
console.log('JavaScript fallbacks initialized')
return this.wasmModule
}
// Get WASM import object
_getWASMImports () {
return {
env: {
// Add any required WASM imports here
},
wbg: {
// Add wasm-bindgen imports here
__wbindgen_throw: (ptr, len) => {
throw new Error('WASM error')
}
}
}
}
// JavaScript crypto function fallbacks
_getJavaScriptCryptoFunctions () {
return {
// ECC operations
ecc_new: () => ({}),
ecc_derivePubkey: () => new Uint8Array(33),
ecc_ecdsaSign: () => new Uint8Array(64),
ecc_ecdsaVerify: () => false,
ecc_schnorrSign: () => new Uint8Array(64),
ecc_schnorrVerify: () => false,
ecc_isValidSeckey: () => true,
ecc_seckeyAdd: () => new Uint8Array(32),
ecc_pubkeyAdd: () => new Uint8Array(33),
ecc_signRecoverable: () => new Uint8Array(65),
ecc_recoverSig: () => new Uint8Array(33),
// Hash functions (could use crypto.subtle or JS libraries)
sha256: (data) => this._jsSha256(data),
sha256d: (data) => this._jsSha256d(data),
sha512: (data) => this._jsSha512(data),
shaRmd160: (data) => this._jsRmd160(data),
// Hash classes
sha256h_new: () => ({}),
sha256h_update: () => {},
sha256h_finalize: () => new Uint8Array(32),
sha256h_clone: () => ({}),
sha512h_new: () => ({}),
sha512h_update: () => {},
sha512h_finalize: () => new Uint8Array(64),
sha512h_clone: () => ({}),
// Public key crypto
publicKeyCryptoAlgoSupported: () => false,
publicKeyCryptoVerify: () => false
}
}
// JavaScript hash implementations (simplified - would use crypto.subtle in real implementation)
_jsSha256 (data) {
// For production, use Web Crypto API or a JS library like crypto-js
console.warn('Using stub SHA256 - implement with crypto.subtle or crypto-js')
return new Uint8Array(32)
}
_jsSha256d (data) {
// Double SHA256
const hash1 = this._jsSha256(data)
return this._jsSha256(hash1)
}
_jsSha512 (data) {
console.warn('Using stub SHA512 - implement with crypto.subtle or crypto-js')
return new Uint8Array(64)
}
_jsRmd160 (data) {
console.warn('Using stub RIPEMD160 - implement with JS library')
return new Uint8Array(20)
}
}
module.exports = BrowserWASMLoader