synor/apps/web/src/lib/wasm-crypto.ts
Gulshan Yadav a439389943 feat(web-wallet): add QR code generation and scanning support
- Add QRCode component using qrcode.react (SVG-based, H error correction)
- Add QRScanner component using html5-qrcode for camera-based scanning
- Update Receive page with real QR code and payment request amount
- Update Send page with QR scan button to auto-fill recipient address
- Support Synor payment URI format: synor:address?amount=X
- Add Dockerfile for web wallet with nginx serving
- Add web-wallet service to docker-compose.testnet.yml (port 17300)
- Fix TypeScript WebCrypto ArrayBuffer type issues

QR features:
- SVG rendering for crisp display at any size
- Error correction level H (30% recovery) for printed codes
- Camera permission handling with user-friendly error states
- Auto-fill amount when scanning payment requests
2026-01-10 05:53:34 +05:30

237 lines
6.9 KiB
TypeScript

/**
* WASM Crypto Module Loader
*
* This module provides lazy-loading of the synor-crypto-wasm module
* for client-side Dilithium3 post-quantum signatures.
*
* ## Why Lazy Loading?
*
* The WASM module is ~2MB, so we only load it when needed:
* - User opts into client-side signing
* - Hardware wallet mode (all keys local)
* - Offline signing scenarios
*
* ## Usage
*
* ```typescript
* import { loadWasmCrypto, isWasmLoaded } from './wasm-crypto';
*
* // Load the WASM module (only loads once)
* const wasm = await loadWasmCrypto();
*
* // Create Dilithium keypair from seed
* const dilithiumKey = wasm.DilithiumSigningKey.fromSeed(seed);
* const signature = dilithiumKey.sign(message);
* ```
*/
// Re-export types from the generated WASM module
// These are auto-generated by wasm-bindgen and match synor-crypto-wasm exports
import type {
DilithiumSigningKey as WasmDilithiumSigningKeyType,
Keypair as WasmKeypairType,
Mnemonic as WasmMnemonicType,
} from '../wasm/synor_crypto';
// Export the types for external use
export type WasmDilithiumSigningKey = WasmDilithiumSigningKeyType;
export type WasmKeypair = WasmKeypairType;
export type WasmMnemonic = WasmMnemonicType;
// Interface for the loaded WASM module
export interface SynorCryptoWasm {
// Classes
Keypair: typeof WasmKeypairType;
DilithiumSigningKey: typeof WasmDilithiumSigningKeyType;
Mnemonic: typeof WasmMnemonicType;
// Standalone functions
verifyWithPublicKey(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
dilithiumVerify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean;
dilithiumSizes(): { publicKey: number; secretKey: number; signature: number };
sha3_256(data: Uint8Array): Uint8Array;
blake3(data: Uint8Array): Uint8Array;
deriveKey(inputKey: Uint8Array, salt: Uint8Array, info: Uint8Array, outputLen: number): Uint8Array;
validateAddress(address: string): boolean;
decodeAddress(address: string): unknown;
}
// Singleton instance of the loaded WASM module
let wasmModule: SynorCryptoWasm | null = null;
let loadingPromise: Promise<SynorCryptoWasm> | null = null;
/**
* Check if the WASM module is already loaded.
*/
export function isWasmLoaded(): boolean {
return wasmModule !== null;
}
/**
* Load the WASM crypto module.
*
* This function is idempotent - calling it multiple times will return
* the same module instance.
*
* @returns Promise resolving to the WASM module
* @throws Error if WASM loading fails
*/
export async function loadWasmCrypto(): Promise<SynorCryptoWasm> {
// Return cached module if already loaded
if (wasmModule) {
return wasmModule;
}
// Return existing loading promise to avoid parallel loads
if (loadingPromise) {
return loadingPromise;
}
// Start loading the WASM module
loadingPromise = (async () => {
try {
// Dynamic import for code splitting - Vite handles WASM bundling
const wasm = await import('../wasm/synor_crypto');
// For wasm-bindgen bundler target, the default export is the init function
// Some bundlers auto-init, others require explicit init call
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const initFn = (wasm as any).default;
if (typeof initFn === 'function') {
await initFn();
}
// Cast to our interface type
wasmModule = {
Keypair: wasm.Keypair,
DilithiumSigningKey: wasm.DilithiumSigningKey,
Mnemonic: wasm.Mnemonic,
verifyWithPublicKey: wasm.verifyWithPublicKey,
dilithiumVerify: wasm.dilithiumVerify,
dilithiumSizes: wasm.dilithiumSizes as () => { publicKey: number; secretKey: number; signature: number },
sha3_256: wasm.sha3_256,
blake3: wasm.blake3,
deriveKey: wasm.deriveKey,
validateAddress: wasm.validateAddress,
decodeAddress: wasm.decodeAddress,
};
return wasmModule;
} catch (error) {
loadingPromise = null; // Reset so it can be retried
throw new Error(
`Failed to load WASM crypto module: ${error instanceof Error ? error.message : 'Unknown error'}. ` +
'Make sure the WASM module is built and copied to apps/web/src/wasm/'
);
}
})();
return loadingPromise;
}
/**
* Unload the WASM module to free memory.
* This is useful for single-page apps that want to reclaim memory
* after signing operations are complete.
*/
export function unloadWasmCrypto(): void {
wasmModule = null;
loadingPromise = null;
}
// ==================== High-Level WASM Crypto Functions ====================
/**
* Create a Dilithium3 keypair from a 32-byte seed.
*
* The seed should be derived from the same mnemonic as the Ed25519 key
* to maintain key correlation for the hybrid signature scheme.
*
* @param seed - 32-byte seed (typically from BIP-39 mnemonic)
* @returns Dilithium signing key
*/
export async function createDilithiumKeyFromSeed(
seed: Uint8Array
): Promise<WasmDilithiumSigningKey> {
const wasm = await loadWasmCrypto();
return wasm.DilithiumSigningKey.fromSeed(seed);
}
/**
* Sign a message with Dilithium3.
*
* @param message - Message to sign
* @param seed - 32-byte seed for key derivation
* @returns 3293-byte Dilithium signature
*/
export async function signWithDilithium(
message: Uint8Array,
seed: Uint8Array
): Promise<Uint8Array> {
const key = await createDilithiumKeyFromSeed(seed);
try {
return key.sign(message);
} finally {
key.free(); // Clean up WASM memory
}
}
/**
* Verify a Dilithium3 signature.
*
* @param message - Original message
* @param signature - Dilithium signature (3293 bytes)
* @param publicKey - Dilithium public key (1952 bytes)
* @returns true if signature is valid
*/
export async function verifyDilithiumSignature(
message: Uint8Array,
signature: Uint8Array,
publicKey: Uint8Array
): Promise<boolean> {
const wasm = await loadWasmCrypto();
return wasm.dilithiumVerify(signature, message, publicKey);
}
/**
* Get Dilithium3 key and signature sizes.
*/
export async function getDilithiumSizes(): Promise<{
publicKey: number;
secretKey: number;
signature: number;
}> {
const wasm = await loadWasmCrypto();
return wasm.dilithiumSizes();
}
// ==================== Ed25519 WASM Functions ====================
/**
* Create an Ed25519 keypair from seed using WASM.
*
* This is an alternative to @noble/ed25519 that keeps all crypto in WASM.
* Useful for consistency or when noble packages aren't available.
*/
export async function createEd25519KeyFromSeed(
seed: Uint8Array
): Promise<WasmKeypair> {
const wasm = await loadWasmCrypto();
return wasm.Keypair.fromSeed(seed);
}
/**
* Sign a message with Ed25519 using WASM.
*/
export async function signWithEd25519Wasm(
message: Uint8Array,
seed: Uint8Array
): Promise<Uint8Array> {
const key = await createEd25519KeyFromSeed(seed);
try {
return key.sign(message);
} finally {
key.free();
}
}