**Motivations :** - this.updateUserStatus is not a function dans la méthode statique getInstance() - Dans une méthode statique, this n'existe pas - Besoin d'une fonction helper pour mettre à jour le statut depuis un contexte statique **Modifications :** - service.ts : Création de la fonction helper updateUserStatusHelper() pour être utilisée dans les méthodes statiques - service.ts : Remplacement de this.updateUserStatus() par updateUserStatusHelper() dans getInstance() - service.ts : La méthode privée updateUserStatus() reste disponible pour les méthodes d'instance **Pages affectées :** - src/services/service.ts : Correction des appels à updateUserStatus dans le contexte statique
3266 lines
115 KiB
TypeScript
Executable File
3266 lines
115 KiB
TypeScript
Executable File
import { initWebsocket, sendMessage } from '../websockets';
|
|
import { memoryManager } from './memory-manager';
|
|
import { secureLogger } from './secure-logger';
|
|
import {
|
|
ApiReturn,
|
|
Device,
|
|
HandshakeMessage,
|
|
Member,
|
|
MerkleProofResult,
|
|
NewTxMessage,
|
|
OutPointProcessMap,
|
|
Process,
|
|
ProcessState,
|
|
RoleDefinition,
|
|
SecretsStore,
|
|
UserDiff,
|
|
} from '../../pkg/sdk_client';
|
|
import ModalService from './modal.service';
|
|
import Database from './database.service';
|
|
import { storeData, retrieveData } from './storage.service';
|
|
import { BackUp } from '../models/backup.model';
|
|
import { DATABASE_CONFIG } from './database-config';
|
|
|
|
export const U32_MAX = 4294967295;
|
|
|
|
const BASEURL = import.meta.env.VITE_BASEURL || `http://localhost`;
|
|
const BOOTSTRAPURL = [import.meta.env.VITE_BOOTSTRAPURL || `${BASEURL}:8090`];
|
|
const STORAGEURL = import.meta.env.VITE_STORAGEURL || `${BASEURL}:8081`;
|
|
const BLINDBITURL = import.meta.env.VITE_BLINDBITURL || `${BASEURL}:8000`;
|
|
const DEFAULTAMOUNT = 1000n;
|
|
|
|
// Global loading spinner functions removed - now using updateUserStatus instead
|
|
|
|
// Helper function to update user status (can be called from static methods)
|
|
function updateUserStatusHelper(message: string): void {
|
|
try {
|
|
const container = document.querySelector('login-4nk-component') as HTMLElement;
|
|
const mainStatus = container?.querySelector('#main-status') as HTMLElement;
|
|
if (mainStatus) {
|
|
// Add timestamp for better user experience
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
mainStatus.innerHTML = `<span style="color: var(--info-color)">[${timestamp}] ${message}</span>`;
|
|
}
|
|
} catch (error) {
|
|
console.warn('Could not update user status:', error);
|
|
}
|
|
}
|
|
|
|
const EMPTY32BYTES = String('').padStart(64, '0');
|
|
|
|
export default class Services {
|
|
private static initializing: Promise<Services> | null = null;
|
|
private static instance: Services;
|
|
private processId: string | null = null;
|
|
private stateId: string | null = null;
|
|
private sdkClient: any;
|
|
private processesCache: Record<string, Process> = {};
|
|
private myProcesses: Set<string> = new Set();
|
|
private notifications: any[] | null = null;
|
|
// private subscriptions: { element: Element; event: string; eventHandler: string }[] = [];
|
|
private maxCacheSize = 0; // Disabled caches completely
|
|
private cacheExpiry = 0; // No cache expiry
|
|
// private database: any;
|
|
private routingInstance!: ModalService;
|
|
private relayAddresses: { [wsurl: string]: string } = {};
|
|
private membersList: Record<string, Member> = {};
|
|
private currentBlockHeight: number = -1;
|
|
private relayReadyResolver: (() => void) | null = null;
|
|
private relayReadyPromise: Promise<void> | null = null;
|
|
private processedHandshakes: Set<string> = new Set();
|
|
// Private constructor to prevent direct instantiation from outside
|
|
private constructor() {}
|
|
|
|
// Method to access the singleton instance of Services
|
|
public static async getInstance(): Promise<Services> {
|
|
if (Services.instance) {
|
|
return Services.instance;
|
|
}
|
|
|
|
if (!Services.initializing) {
|
|
Services.initializing = (async () => {
|
|
const instance = new Services();
|
|
// Initialize WebAssembly when needed
|
|
await instance.init();
|
|
instance.routingInstance = await ModalService.getInstance();
|
|
return instance;
|
|
})();
|
|
}
|
|
|
|
console.log('initializing services');
|
|
|
|
// Debug: Check memory usage before any operations
|
|
if ((performance as any).memory) {
|
|
const memory = (performance as any).memory;
|
|
const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
|
|
console.log(`🔍 Initial memory usage: ${usedPercent.toFixed(1)}% (${(memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB / ${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(1)}MB)`);
|
|
|
|
// Si la mémoire est déjà très élevée, faire un nettoyage agressif immédiat
|
|
if (usedPercent > 90) {
|
|
console.log('🧹 High memory detected, performing immediate cleanup...');
|
|
|
|
// Nettoyage agressif immédiat
|
|
if (window.gc) {
|
|
for (let i = 0; i < 5; i++) {
|
|
window.gc();
|
|
}
|
|
}
|
|
|
|
// Nettoyer les caches
|
|
if ('caches' in window) {
|
|
const cacheNames = await caches.keys();
|
|
await Promise.all(cacheNames.map(name => caches.delete(name)));
|
|
}
|
|
|
|
// Nettoyer localStorage
|
|
if (window.localStorage) {
|
|
const keys = Object.keys(localStorage);
|
|
keys.forEach(key => {
|
|
if (key.startsWith('temp_') || key.startsWith('cache_') || key.startsWith('vite_')) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Vérifier la mémoire après nettoyage
|
|
const memoryAfter = (performance as any).memory;
|
|
const usedPercentAfter = (memoryAfter.usedJSHeapSize / memoryAfter.jsHeapSizeLimit) * 100;
|
|
console.log(`📊 Memory after cleanup: ${usedPercentAfter.toFixed(1)}% (${(memoryAfter.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB)`);
|
|
}
|
|
}
|
|
|
|
// Update user status during initialization (using helper function)
|
|
updateUserStatusHelper('🔄 Initializing services...');
|
|
|
|
// Add WebAssembly memory optimization and error handling
|
|
try {
|
|
// Check if WebAssembly is supported
|
|
if (typeof WebAssembly === 'undefined') {
|
|
throw new Error('WebAssembly is not supported in this browser');
|
|
}
|
|
|
|
// Optimize WebAssembly memory before initialization
|
|
console.log('🔧 Optimizing WebAssembly memory...');
|
|
|
|
// Clear browser caches to free memory
|
|
if ('caches' in window) {
|
|
const cacheNames = await caches.keys();
|
|
await Promise.all(cacheNames.map(name => caches.delete(name)));
|
|
console.log('🧹 Browser caches cleared');
|
|
}
|
|
|
|
// Clear unused objects from memory
|
|
if (window.gc) {
|
|
window.gc();
|
|
console.log('🗑️ Garbage collection triggered');
|
|
}
|
|
|
|
// Force memory cleanup
|
|
if (window.gc) {
|
|
window.gc();
|
|
await new Promise(resolve => setTimeout(resolve, 100)); // Wait for GC
|
|
window.gc();
|
|
console.log('🗑️ Additional garbage collection triggered');
|
|
}
|
|
|
|
// DO NOT clear user data - only clear non-essential caches
|
|
console.log('⚠️ Skipping storage cleanup to preserve user data');
|
|
|
|
// Light memory cleanup only
|
|
console.log('🔧 Performing light memory cleanup...');
|
|
|
|
// Minimal cleanup to avoid memory leaks
|
|
try {
|
|
// Only clear HTTP caches if they exist
|
|
if ('caches' in window) {
|
|
const cacheNames = await caches.keys();
|
|
if (cacheNames.length > 0) {
|
|
const httpCaches = cacheNames.filter(name => name.startsWith('http'));
|
|
if (httpCaches.length > 0) {
|
|
await Promise.all(httpCaches.map(name => caches.delete(name)));
|
|
console.log('🧹 HTTP caches cleared (user data preserved)');
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('⚠️ Safe cleanup error:', e);
|
|
}
|
|
|
|
// Check available memory (Chrome-specific API)
|
|
if ((performance as any).memory) {
|
|
const memory = (performance as any).memory;
|
|
const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
|
|
console.log(`📊 Memory usage after cleanup: ${usedPercent.toFixed(1)}% (${(memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB)`);
|
|
|
|
if (usedPercent > 75) {
|
|
console.warn('⚠️ High memory usage detected, performing aggressive cleanup...');
|
|
|
|
// More aggressive cleanup
|
|
console.log('🔍 Debugging memory usage...');
|
|
console.log('📦 Document elements:', document.querySelectorAll('*').length);
|
|
|
|
// Multiple garbage collections
|
|
if (window.gc) {
|
|
for (let i = 0; i < 3; i++) {
|
|
window.gc();
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
}
|
|
|
|
// Clear any cached data
|
|
if (window.localStorage) {
|
|
const keys = Object.keys(localStorage);
|
|
keys.forEach(key => {
|
|
if (key.startsWith('temp_') || key.startsWith('cache_')) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log('🧹 Aggressive memory cleanup completed');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ WebAssembly optimization error:', error);
|
|
// Don't throw here, continue with initialization
|
|
}
|
|
|
|
// Initialize services with conditional WebAssembly loading
|
|
try {
|
|
// Check memory before loading WebAssembly
|
|
if ((performance as any).memory) {
|
|
const memory = (performance as any).memory;
|
|
const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
|
|
const availableMB = (memory.jsHeapSizeLimit - memory.usedJSHeapSize) / 1024 / 1024;
|
|
|
|
console.log(`📊 Memory check before WebAssembly: ${usedPercent.toFixed(1)}% used, ${availableMB.toFixed(1)}MB available`);
|
|
|
|
// WebAssembly nécessite généralement au moins 100-200MB de mémoire disponible
|
|
// Si moins de 150MB disponibles ou plus de 85% utilisé, ne pas initialiser
|
|
if (usedPercent > 85 || availableMB < 150) {
|
|
console.error(`🚫 Memory insufficient for WebAssembly: ${usedPercent.toFixed(1)}% used, ${availableMB.toFixed(1)}MB available`);
|
|
Services.initializing = null;
|
|
throw new Error(`Insufficient memory for WebAssembly initialization. Current usage: ${usedPercent.toFixed(1)}%, Available: ${availableMB.toFixed(1)}MB. Please close other tabs and refresh.`);
|
|
}
|
|
}
|
|
|
|
// Memory is sufficient, load WebAssembly
|
|
Services.instance = await Services.initializing;
|
|
Services.initializing = null;
|
|
console.log('✅ Services initialized with WebAssembly');
|
|
|
|
} catch (error) {
|
|
console.error('❌ Service initialization failed:', error);
|
|
// Réinitialiser initializing pour permettre une nouvelle tentative après un délai
|
|
Services.initializing = null;
|
|
|
|
// Si c'est une erreur de mémoire, ne pas réessayer immédiatement
|
|
const errorMessage = (error as Error).message || String(error);
|
|
if (errorMessage.includes('Out of memory') || errorMessage.includes('memory')) {
|
|
console.error('🚫 Memory error detected - cannot retry immediately');
|
|
throw new Error('WebAssembly initialization failed due to insufficient memory. Please refresh the page.');
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
// Update user status after successful initialization
|
|
updateUserStatusHelper('✅ Services initialized successfully');
|
|
|
|
return Services.instance;
|
|
}
|
|
|
|
public async init(): Promise<void> {
|
|
this.notifications = this.getNotifications();
|
|
|
|
// Vérifier la mémoire avant d'importer WebAssembly
|
|
if ((performance as any).memory) {
|
|
const memory = (performance as any).memory;
|
|
const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
|
|
const availableMB = (memory.jsHeapSizeLimit - memory.usedJSHeapSize) / 1024 / 1024;
|
|
|
|
console.log(`📊 Memory check before WebAssembly import: ${usedPercent.toFixed(1)}% used, ${availableMB.toFixed(1)}MB available`);
|
|
|
|
// WebAssembly nécessite au moins 150MB de mémoire disponible
|
|
if (usedPercent > 85 || availableMB < 150) {
|
|
console.error(`🚫 Memory insufficient for WebAssembly import: ${usedPercent.toFixed(1)}% used, ${availableMB.toFixed(1)}MB available`);
|
|
throw new Error(`Insufficient memory for WebAssembly. Current usage: ${usedPercent.toFixed(1)}%, Available: ${availableMB.toFixed(1)}MB. Please close other tabs and refresh.`);
|
|
}
|
|
}
|
|
|
|
this.sdkClient = await import('../../pkg/sdk_client');
|
|
this.sdkClient.setup();
|
|
for (const wsurl of Object.values(BOOTSTRAPURL)) {
|
|
this.updateRelay(wsurl, '');
|
|
}
|
|
|
|
// Démarrer le monitoring de la mémoire
|
|
memoryManager.startMonitoring();
|
|
|
|
// Nettoyer les caches périodiquement
|
|
this.startCacheCleanup();
|
|
|
|
// Initialiser le service PBKDF2 pour les credentials sécurisés
|
|
try {
|
|
const { secureCredentialsService } = await import('./secure-credentials.service');
|
|
// Use secureCredentialsService variable
|
|
console.log('Secure credentials service imported:', secureCredentialsService);
|
|
secureLogger.info('PBKDF2 service initialized for secure credentials', {
|
|
component: 'Services',
|
|
operation: 'pbkdf2_init'
|
|
});
|
|
} catch (error) {
|
|
secureLogger.warn('Failed to initialize PBKDF2 service', {
|
|
component: 'Services',
|
|
operation: 'pbkdf2_init',
|
|
error: error as Error
|
|
});
|
|
}
|
|
|
|
secureLogger.info('Services initialized', {
|
|
component: 'Services',
|
|
operation: 'initialization'
|
|
});
|
|
}
|
|
|
|
public setProcessId(processId: string | null) {
|
|
this.processId = processId;
|
|
}
|
|
|
|
/**
|
|
* Démarre le nettoyage périodique des caches
|
|
*/
|
|
private startCacheCleanup(): void {
|
|
setInterval(() => {
|
|
this.cleanupCaches();
|
|
}, this.cacheExpiry);
|
|
}
|
|
|
|
/**
|
|
* Nettoie les caches expirés
|
|
*/
|
|
private cleanupCaches(): void {
|
|
const now = Date.now();
|
|
const expiredKeys: string[] = [];
|
|
|
|
// Nettoyer le cache des processus
|
|
Object.keys(this.processesCache).forEach(key => {
|
|
const process = this.processesCache[key];
|
|
if (process && now - (process as any).timestamp > this.cacheExpiry) {
|
|
expiredKeys.push(key);
|
|
}
|
|
});
|
|
|
|
expiredKeys.forEach(key => {
|
|
delete this.processesCache[key];
|
|
});
|
|
|
|
// Nettoyer le cache des membres
|
|
Object.keys(this.membersList).forEach(key => {
|
|
const member = this.membersList[key];
|
|
if (member && now - (member as any).timestamp > this.cacheExpiry) {
|
|
delete this.membersList[key];
|
|
}
|
|
});
|
|
|
|
if (expiredKeys.length > 0) {
|
|
secureLogger.debug('Cache cleanup completed', {
|
|
component: 'Services',
|
|
operation: 'cache_cleanup',
|
|
expiredEntries: expiredKeys.length
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Met en cache un processus avec timestamp
|
|
*/
|
|
private cacheProcess(processId: string, process: Process): void {
|
|
// Use parameters
|
|
console.log('Caching process:', { processId, process });
|
|
if (Object.keys(this.processesCache).length >= this.maxCacheSize) {
|
|
// Supprimer le plus ancien
|
|
const oldestKey = Object.keys(this.processesCache)[0];
|
|
delete this.processesCache[oldestKey];
|
|
}
|
|
|
|
(process as any).timestamp = Date.now();
|
|
this.processesCache[processId] = process;
|
|
}
|
|
|
|
/**
|
|
* Récupère un processus du cache
|
|
*/
|
|
private getCachedProcess(processId: string): Process | null {
|
|
// Use processId parameter
|
|
console.log('Getting cached process:', processId);
|
|
const process = this.processesCache[processId];
|
|
if (!process) {return null;}
|
|
|
|
const now = Date.now();
|
|
if (now - (process as any).timestamp > this.cacheExpiry) {
|
|
delete this.processesCache[processId];
|
|
return null;
|
|
}
|
|
|
|
return process;
|
|
}
|
|
|
|
/**
|
|
* Nettoie tous les caches
|
|
*/
|
|
public clearAllCaches(): void {
|
|
this.processesCache = {};
|
|
this.membersList = {};
|
|
this.myProcesses.clear();
|
|
|
|
secureLogger.info('All caches cleared', {
|
|
component: 'Services',
|
|
operation: 'cache_clear'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Récupère les statistiques des caches
|
|
*/
|
|
public getCacheStats(): {
|
|
processes: number;
|
|
members: number;
|
|
myProcesses: number;
|
|
memory: any;
|
|
} {
|
|
return {
|
|
processes: Object.keys(this.processesCache).length,
|
|
members: Object.keys(this.membersList).length,
|
|
myProcesses: this.myProcesses.size,
|
|
memory: memoryManager.getMemoryReport()
|
|
};
|
|
}
|
|
|
|
public setStateId(stateId: string | null) {
|
|
this.stateId = stateId;
|
|
}
|
|
|
|
public getProcessId(): string | null {
|
|
return this.processId;
|
|
}
|
|
|
|
public getStateId(): string | null {
|
|
return this.stateId;
|
|
}
|
|
|
|
/**
|
|
* Calls `this.addWebsocketConnection` for each `wsurl` in relayAddresses.
|
|
* Waits for at least one handshake message before returning.
|
|
*/
|
|
public async connectAllRelays(): Promise<void> {
|
|
const relayUrls = Object.keys(this.relayAddresses);
|
|
console.log(`🚀 Connecting to ${relayUrls.length} relays in parallel...`);
|
|
|
|
// Create the relay ready promise immediately when starting connections
|
|
this.getRelayReadyPromise();
|
|
|
|
// Connect to all relays in parallel
|
|
const connectionPromises = relayUrls.map(async wsurl => {
|
|
try {
|
|
console.log(`🔗 Connecting to: ${wsurl}`);
|
|
await this.addWebsocketConnection(wsurl);
|
|
console.log(`✅ Successfully connected to: ${wsurl}`);
|
|
return wsurl;
|
|
} catch (error) {
|
|
console.error(`❌ Failed to connect to ${wsurl}:`, error);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Wait for all connections to complete (success or failure)
|
|
const results = await Promise.allSettled(connectionPromises);
|
|
const connectedUrls = results
|
|
.filter(
|
|
(result): result is PromiseFulfilledResult<string> =>
|
|
result.status === 'fulfilled' && result.value !== null
|
|
)
|
|
.map(result => result.value);
|
|
|
|
console.log(`✅ Connected to ${connectedUrls.length}/${relayUrls.length} relays`);
|
|
|
|
// Wait for at least one handshake message if we have connections
|
|
if (connectedUrls.length > 0) {
|
|
try {
|
|
await this.waitForHandshakeMessage(10000); // Augmenter le timeout à 10 secondes
|
|
console.log(`✅ Handshake received from at least one relay`);
|
|
} catch (error) {
|
|
console.warn(
|
|
`⚠️ No handshake received within timeout, but continuing with ${connectedUrls.length} connections`
|
|
);
|
|
// Continue anyway - we have connections even without handshake
|
|
// Resolve the relay ready promise manually since we have connections
|
|
this.resolveRelayReady();
|
|
}
|
|
} else {
|
|
console.warn(`⚠️ No relay connections established`);
|
|
}
|
|
}
|
|
|
|
private getRelayReadyPromise(): Promise<void> {
|
|
console.log('🔍 DEBUG: getRelayReadyPromise called, promise exists:', !!this.relayReadyPromise);
|
|
|
|
// If we already have a relay with spAddress, return resolved promise
|
|
const hasRelayWithAddress = Object.values(this.relayAddresses).some(address => address && address.trim() !== '');
|
|
if (hasRelayWithAddress) {
|
|
console.log('🔍 DEBUG: Relay already ready with spAddress, returning resolved promise');
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (!this.relayReadyPromise) {
|
|
console.log('🔍 DEBUG: Creating new relay ready promise');
|
|
this.relayReadyPromise = new Promise<void>((resolve) => {
|
|
this.relayReadyResolver = resolve;
|
|
|
|
// Timeout après 10 secondes si aucun handshake n'arrive
|
|
setTimeout(() => {
|
|
if (this.relayReadyResolver) {
|
|
console.warn('⚠️ Relay ready timeout - resolving anyway');
|
|
this.relayReadyResolver();
|
|
this.relayReadyResolver = null;
|
|
this.relayReadyPromise = null;
|
|
}
|
|
}, 10000);
|
|
});
|
|
} else {
|
|
console.log('🔍 DEBUG: Returning existing relay ready promise');
|
|
}
|
|
return this.relayReadyPromise;
|
|
}
|
|
|
|
private resolveRelayReady(): void {
|
|
console.log('🔍 DEBUG: resolveRelayReady called, resolver exists:', !!this.relayReadyResolver);
|
|
if (this.relayReadyResolver) {
|
|
console.log('✅ DEBUG: Resolving relay ready promise');
|
|
this.relayReadyResolver();
|
|
this.relayReadyResolver = null;
|
|
this.relayReadyPromise = null;
|
|
} else {
|
|
console.warn('⚠️ DEBUG: No resolver to resolve - promise may have been resolved already or never created');
|
|
}
|
|
}
|
|
|
|
public async addWebsocketConnection(url: string): Promise<void> {
|
|
console.log('Opening new websocket connection');
|
|
await initWebsocket(url);
|
|
}
|
|
|
|
/**
|
|
* Add or update a key/value pair in relayAddresses.
|
|
* @param wsurl - The WebSocket URL (key).
|
|
* @param spAddress - The SP Address (value).
|
|
*/
|
|
public updateRelay(url: string, spAddress: string) {
|
|
console.log(`✅ Updating relay ${url} with spAddress ${spAddress}`);
|
|
this.relayAddresses[url] = spAddress;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the spAddress for a given wsurl.
|
|
* @param wsurl - The WebSocket URL to look up.
|
|
* @returns The SP Address if found, or undefined if not.
|
|
*/
|
|
public getSpAddress(wsurl: string): string | undefined {
|
|
return this.relayAddresses[wsurl];
|
|
}
|
|
|
|
/**
|
|
* Get all key/value pairs from relayAddresses.
|
|
* @returns An array of objects containing wsurl and spAddress.
|
|
*/
|
|
public getAllRelays(): { wsurl: string; spAddress: string }[] {
|
|
return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({
|
|
wsurl,
|
|
spAddress,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Print all key/value pairs for debugging.
|
|
*/
|
|
public printAllRelays(): void {
|
|
console.log('Current relay addresses:');
|
|
for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) {
|
|
console.log(`${wsurl} -> ${spAddress}`);
|
|
}
|
|
}
|
|
|
|
public isPaired(): boolean {
|
|
try {
|
|
if (!this.sdkClient) {
|
|
console.log('WebAssembly SDK not initialized - assuming not paired');
|
|
return false;
|
|
}
|
|
return this.sdkClient.is_paired();
|
|
} catch (e) {
|
|
// During pairing process, it's normal for the device to not be paired yet
|
|
console.warn(`Device pairing status check failed (normal during pairing): ${e}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async unpairDevice(): Promise<void> {
|
|
try {
|
|
this.sdkClient.unpair_device();
|
|
const newDevice = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(newDevice);
|
|
} catch (e) {
|
|
throw new Error(`Failed to unpair device: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async getSecretForAddress(address: string): Promise<string | null> {
|
|
const db = await Database.getInstance();
|
|
return await db.getObject('shared_secrets', address);
|
|
}
|
|
|
|
public async getAllSecrets(): Promise<SecretsStore> {
|
|
const db = await Database.getInstance();
|
|
const sharedSecrets = await db.dumpStore('shared_secrets');
|
|
const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); // keys are numeric values
|
|
|
|
const secretsStore = {
|
|
shared_secrets: sharedSecrets,
|
|
unconfirmed_secrets: Object.values(unconfirmedSecrets),
|
|
};
|
|
|
|
return secretsStore;
|
|
}
|
|
|
|
public async getAllDiffs(): Promise<Record<string, UserDiff>> {
|
|
const db = await Database.getInstance();
|
|
return await db.dumpStore('diffs');
|
|
}
|
|
|
|
public async getDiffByValue(value: string): Promise<UserDiff | null> {
|
|
const db = await Database.getInstance();
|
|
const store = 'diffs';
|
|
const res = await db.getObject(store, value);
|
|
return res;
|
|
}
|
|
|
|
private async getTokensFromFaucet(): Promise<void> {
|
|
await this.ensureSufficientAmount();
|
|
}
|
|
|
|
// If we're updating a process, we must call that after update especially if roles are part of it
|
|
// We will take the roles from the last state, wheter it's commited or not
|
|
public async checkConnections(process: Process, stateId: string | null = null): Promise<void> {
|
|
if (process.states.length < 2) {
|
|
throw new Error("Process doesn't have any state yet");
|
|
}
|
|
let roles: Record<string, RoleDefinition> | null = null;
|
|
if (!stateId) {
|
|
roles = process.states[process.states.length - 2].roles;
|
|
} else {
|
|
roles = process.states.find(state => state.state_id === stateId)?.roles || null;
|
|
}
|
|
if (!roles) {
|
|
throw new Error('No roles found');
|
|
}
|
|
const members: Set<Member> = new Set();
|
|
for (const role of Object.values(roles!)) {
|
|
for (const member of role.members) {
|
|
// Check if we know the member that matches this id
|
|
const memberAddresses = this.getAddressesForMemberId(member);
|
|
if (memberAddresses && memberAddresses.length != 0) {
|
|
members.add({ sp_addresses: memberAddresses });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (members.size === 0) {
|
|
// This must be a pairing process
|
|
// Check if we have a pairedAddresses in the public data
|
|
let publicData: Record<string, any> | null = null;
|
|
if (!stateId) {
|
|
publicData = process.states[process.states.length - 2]?.public_data;
|
|
} else {
|
|
publicData = process.states.find(state => state.state_id === stateId)?.public_data || null;
|
|
}
|
|
|
|
// If pairedAddresses is not in the current state, look in previous states
|
|
if (!publicData?.['pairedAddresses']) {
|
|
// Look for pairedAddresses in previous states
|
|
for (let i = process.states.length - 1; i >= 0; i--) {
|
|
const state = process.states[i];
|
|
if (state.public_data && state.public_data['pairedAddresses']) {
|
|
publicData = state.public_data;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!publicData?.['pairedAddresses']) {
|
|
throw new Error('Not a pairing process');
|
|
}
|
|
const decodedAddresses = this.decodeValue(publicData['pairedAddresses']);
|
|
if (decodedAddresses.length === 0) {
|
|
throw new Error('Not a pairing process');
|
|
}
|
|
members.add({ sp_addresses: decodedAddresses });
|
|
}
|
|
|
|
// Ensure the amount is available before proceeding
|
|
await this.getTokensFromFaucet();
|
|
const unconnectedAddresses = new Set<string>();
|
|
const myAddress = await this.getDeviceAddress();
|
|
for (const member of Array.from(members)) {
|
|
const sp_addresses = member.sp_addresses;
|
|
if (!sp_addresses || sp_addresses.length === 0) {continue;}
|
|
for (const address of sp_addresses) {
|
|
// For now, we ignore our own device address, although there might be use cases for having a secret with ourselves
|
|
if (address === myAddress) {continue;}
|
|
if ((await this.getSecretForAddress(address)) === null) {
|
|
unconnectedAddresses.add(address);
|
|
}
|
|
}
|
|
}
|
|
if (unconnectedAddresses && unconnectedAddresses.size != 0) {
|
|
const apiResult = await this.connectAddresses(Array.from(unconnectedAddresses));
|
|
await this.handleApiReturn(apiResult);
|
|
}
|
|
}
|
|
|
|
public async connectAddresses(addresses: string[]): Promise<ApiReturn> {
|
|
if (addresses.length === 0) {
|
|
throw new Error('Trying to connect to empty addresses list');
|
|
}
|
|
|
|
try {
|
|
return this.sdkClient.create_transaction(addresses, 1);
|
|
} catch (e) {
|
|
console.error('Failed to connect member:', e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private async ensureSufficientAmount(): Promise<void> {
|
|
const availableAmt = await this.getAmount();
|
|
const target: bigint = DEFAULTAMOUNT * 10n;
|
|
|
|
console.log(`💰 Current amount: ${availableAmt}, target: ${target}`);
|
|
|
|
if (availableAmt < target) {
|
|
console.log('🪙 Requesting tokens from faucet...');
|
|
this.updateUserStatus('🪙 Requesting test tokens from faucet...');
|
|
const faucetMsg = this.createFaucetMessage();
|
|
console.log('🪙 Faucet message created:', faucetMsg);
|
|
this.sendFaucetMessage(faucetMsg);
|
|
console.log('🪙 Faucet message sent, waiting for tokens...');
|
|
this.updateUserStatus('⏳ Waiting for tokens to be sent...');
|
|
|
|
await this.waitForAmount(target);
|
|
} else {
|
|
console.log('✅ Sufficient tokens already available');
|
|
this.updateUserStatus('✅ Sufficient tokens available');
|
|
}
|
|
}
|
|
|
|
private isScanningBlocks = false;
|
|
private hasReceivedTransaction = false; // Track if we've received any transaction
|
|
|
|
private async safeScanBlocks(): Promise<void> {
|
|
if (this.isScanningBlocks) {
|
|
console.log('⏳ Block scan already in progress, skipping...');
|
|
return;
|
|
}
|
|
|
|
this.isScanningBlocks = true;
|
|
try {
|
|
console.log('🔄 Starting block scan...');
|
|
await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL);
|
|
console.log('✅ Block scan completed');
|
|
} catch (error) {
|
|
console.error('❌ Block scan failed:', error);
|
|
throw error;
|
|
} finally {
|
|
this.isScanningBlocks = false;
|
|
}
|
|
}
|
|
|
|
private updateUserStatus(message: string): void {
|
|
try {
|
|
const container = document.querySelector('login-4nk-component') as HTMLElement;
|
|
const mainStatus = container?.querySelector('#main-status') as HTMLElement;
|
|
if (mainStatus) {
|
|
// Add timestamp for better user experience
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
mainStatus.innerHTML = `<span style="color: var(--info-color)">[${timestamp}] ${message}</span>`;
|
|
}
|
|
} catch (error) {
|
|
console.warn('Could not update user status:', error);
|
|
}
|
|
}
|
|
|
|
private async waitForAmount(target: bigint): Promise<bigint> {
|
|
let attempts = 20; // Increased attempts for blockchain confirmation
|
|
|
|
while (attempts > 0) {
|
|
const amount = await this.getAmount();
|
|
console.log(`🪙 Attempt ${21 - attempts}: current amount ${amount}, target ${target}`);
|
|
|
|
if (amount >= target) {
|
|
console.log('✅ Sufficient tokens received!');
|
|
this.updateUserStatus('✅ Tokens received successfully!');
|
|
return amount;
|
|
}
|
|
|
|
// Only scan if we've received a transaction (NewTx message)
|
|
if (this.hasReceivedTransaction && attempts < 20) {
|
|
console.log('🔄 Transaction received, scanning blocks to update wallet state...');
|
|
this.updateUserStatus('🔄 Processing received transaction...');
|
|
try {
|
|
// Force a complete scan to catch the new transaction
|
|
console.log(`🔄 Scanning blocks to catch new transaction...`);
|
|
await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL);
|
|
console.log('✅ Block scan completed after transaction');
|
|
|
|
// Check amount again after scanning
|
|
const newAmount = await this.getAmount();
|
|
console.log(`💰 Amount after transaction scan: ${newAmount}`);
|
|
|
|
if (newAmount > 0n) {
|
|
this.updateUserStatus(`💰 Found ${newAmount} tokens in wallet!`);
|
|
} else {
|
|
this.updateUserStatus('⏳ Transaction processed, waiting for confirmation...');
|
|
}
|
|
} catch (scanError) {
|
|
console.error('❌ Error during transaction scan:', scanError);
|
|
this.updateUserStatus('⚠️ Processing transaction...');
|
|
}
|
|
} else if (!this.hasReceivedTransaction) {
|
|
this.updateUserStatus('⏳ Waiting for faucet transaction...');
|
|
}
|
|
|
|
attempts--;
|
|
if (attempts > 0) {
|
|
console.log(`⏳ Waiting 5 seconds before next attempt (${attempts} attempts left)...`);
|
|
this.updateUserStatus(`⏳ Checking for tokens... (${attempts} attempts remaining)`);
|
|
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait for 5 seconds
|
|
}
|
|
}
|
|
|
|
throw new Error('Amount is still insufficient after 20 attempts - faucet may be down or transaction not confirmed');
|
|
}
|
|
|
|
public async createPairingProcess(userName: string, pairWith: string[]): Promise<ApiReturn> {
|
|
if (this.sdkClient.is_paired()) {
|
|
throw new Error('Device already paired');
|
|
}
|
|
const myAddress: string = this.sdkClient.get_address();
|
|
pairWith.push(myAddress);
|
|
const privateData = {
|
|
description: 'pairing',
|
|
counter: 0,
|
|
};
|
|
const publicData = {
|
|
memberPublicName: userName,
|
|
pairedAddresses: pairWith,
|
|
};
|
|
const validation_fields: string[] = [
|
|
...Object.keys(privateData),
|
|
...Object.keys(publicData),
|
|
'roles',
|
|
];
|
|
const roles: Record<string, RoleDefinition> = {
|
|
pairing: {
|
|
members: [],
|
|
validation_rules: [
|
|
{
|
|
quorum: 1.0,
|
|
fields: validation_fields,
|
|
min_sig_member: 1.0,
|
|
},
|
|
],
|
|
storages: [STORAGEURL],
|
|
},
|
|
};
|
|
try {
|
|
return this.createProcess(privateData, publicData, roles);
|
|
} catch (e) {
|
|
throw new Error(`Creating process failed:, ${e}`);
|
|
}
|
|
}
|
|
|
|
private isFileBlob(value: any): value is { type: string; data: Uint8Array } {
|
|
return (
|
|
typeof value === 'object' &&
|
|
value !== null &&
|
|
typeof value.type === 'string' &&
|
|
value.data instanceof Uint8Array
|
|
);
|
|
}
|
|
|
|
private splitData(obj: Record<string, any>) {
|
|
const jsonCompatibleData: Record<string, any> = {};
|
|
const binaryData: Record<string, { type: string; data: Uint8Array }> = {};
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
if (this.isFileBlob(value)) {
|
|
binaryData[key] = value;
|
|
} else {
|
|
jsonCompatibleData[key] = value;
|
|
}
|
|
}
|
|
|
|
return { jsonCompatibleData, binaryData };
|
|
}
|
|
|
|
public async createProcess(
|
|
privateData: Record<string, any>,
|
|
publicData: Record<string, any>,
|
|
roles: Record<string, RoleDefinition>
|
|
): Promise<ApiReturn> {
|
|
// Vérifier que les clés sont disponibles avant toute opération
|
|
await this.ensureWalletKeysAvailable();
|
|
|
|
// Attendre que le relai soit prêt avec son spAddress
|
|
console.log('⏳ Waiting for relays to be ready...');
|
|
// Update UI status
|
|
const { updateCreatorStatus } = await import('../utils/sp-address.utils');
|
|
updateCreatorStatus('⏳ Waiting for relays to be ready...');
|
|
|
|
await this.getRelayReadyPromise();
|
|
|
|
// Vérifier que nous avons maintenant un spAddress
|
|
const relays = this.getAllRelays();
|
|
const relayAddress = relays.find(relay => relay.spAddress && relay.spAddress.trim() !== '')?.spAddress;
|
|
|
|
if (!relayAddress) {
|
|
console.error('Available relays:', relays);
|
|
throw new Error('❌ No relay address available after waiting');
|
|
}
|
|
|
|
console.log('✅ Relay address found:', relayAddress);
|
|
|
|
const feeRate = 1;
|
|
|
|
// We can't encode files as the rest because Uint8Array is not valid json
|
|
// So we first take them apart and we will encode them separately and put them back in the right object
|
|
// TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking
|
|
const privateSplitData = this.splitData(privateData);
|
|
const publicSplitData = this.splitData(publicData);
|
|
const encodedPrivateData = {
|
|
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
|
|
...this.sdkClient.encode_binary(privateSplitData.binaryData),
|
|
};
|
|
const encodedPublicData = {
|
|
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
|
|
...this.sdkClient.encode_binary(publicSplitData.binaryData),
|
|
};
|
|
|
|
// console.log('encodedPrivateData:', encodedPrivateData);
|
|
// console.log('encodedPublicData:', encodedPublicData);
|
|
// console.log('roles:', roles);
|
|
// console.log('members:', this.getAllMembers());
|
|
// console.log('relayAddress:', relayAddress, 'feeRate:', feeRate);
|
|
|
|
// First, ensure we have a complete initial scan before requesting faucet tokens
|
|
console.log('🔄 Ensuring complete initial scan before faucet request...');
|
|
await this.ensureCompleteInitialScan();
|
|
|
|
// Now request tokens from faucet
|
|
await this.getTokensFromFaucet();
|
|
|
|
const membersObj = this.getAllMembers();
|
|
console.log('🔍 DEBUG: Members for create_new_process:', membersObj);
|
|
console.log('🔍 DEBUG: Members type:', typeof membersObj);
|
|
console.log('🔍 DEBUG: Members keys:', Object.keys(membersObj));
|
|
|
|
// Check if membersList is empty
|
|
if (!membersObj || Object.keys(membersObj).length === 0) {
|
|
console.warn('⚠️ No members available for create_new_process, waiting for handshake...');
|
|
throw new Error('No members available - handshake not completed yet');
|
|
}
|
|
|
|
// Convert membersObj to array format for WebAssembly (it expects a sequence, not a map)
|
|
const members = Object.values(membersObj).map(member => ({
|
|
sp_addresses: member.sp_addresses
|
|
}));
|
|
console.log('🔍 DEBUG: Members array length:', members.length);
|
|
console.log('🔍 DEBUG: Members array sample:', members.slice(0, 3));
|
|
|
|
const result = this.sdkClient.create_new_process(
|
|
encodedPrivateData,
|
|
roles,
|
|
encodedPublicData,
|
|
relayAddress,
|
|
feeRate,
|
|
members
|
|
);
|
|
|
|
if (result.updated_process) {
|
|
console.log('created process:', result.updated_process);
|
|
await this.checkConnections(result.updated_process.current_process);
|
|
return result;
|
|
} else {
|
|
throw new Error('Empty updated_process in createProcessReturn');
|
|
}
|
|
}
|
|
|
|
public async updateProcess(
|
|
process: Process,
|
|
privateData: Record<string, any>,
|
|
publicData: Record<string, any>,
|
|
roles: Record<string, RoleDefinition> | null
|
|
): Promise<ApiReturn> {
|
|
// If roles is null, we just take the last commited state roles
|
|
if (!roles) {
|
|
roles = this.getRoles(process);
|
|
} else {
|
|
// We should check that we have the right to change the roles here, or maybe it's better leave it to the wasm
|
|
console.log('Provided new roles:', JSON.stringify(roles));
|
|
}
|
|
const privateSplitData = this.splitData(privateData);
|
|
const publicSplitData = this.splitData(publicData);
|
|
const encodedPrivateData = {
|
|
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
|
|
...this.sdkClient.encode_binary(privateSplitData.binaryData),
|
|
};
|
|
const encodedPublicData = {
|
|
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
|
|
...this.sdkClient.encode_binary(publicSplitData.binaryData),
|
|
};
|
|
try {
|
|
const members = Object.values(this.getAllMembers()).map(member => ({
|
|
sp_addresses: member.sp_addresses
|
|
}));
|
|
const result = this.sdkClient.update_process(
|
|
process,
|
|
encodedPrivateData,
|
|
roles,
|
|
encodedPublicData,
|
|
members
|
|
);
|
|
if (result.updated_process) {
|
|
await this.checkConnections(result.updated_process.current_process);
|
|
return result;
|
|
} else {
|
|
throw new Error('Empty updated_process in updateProcessReturn');
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to update process: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async createPrdUpdate(processId: string, stateId: string): Promise<ApiReturn> {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) {
|
|
throw new Error('Unknown process');
|
|
} else {
|
|
await this.checkConnections(process);
|
|
}
|
|
try {
|
|
const members = Object.values(this.getAllMembers()).map(member => ({
|
|
sp_addresses: member.sp_addresses
|
|
}));
|
|
return this.sdkClient.create_update_message(process, stateId, members);
|
|
} catch (e) {
|
|
throw new Error(`Failed to create prd update: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async createPrdResponse(processId: string, stateId: string): Promise<ApiReturn> {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) {
|
|
throw new Error('Unknown process');
|
|
}
|
|
try {
|
|
const members = Object.values(this.getAllMembers()).map(member => ({
|
|
sp_addresses: member.sp_addresses
|
|
}));
|
|
return this.sdkClient.create_response_prd(process, stateId, members);
|
|
} catch (e) {
|
|
throw new Error(`Failed to create response prd: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async approveChange(processId: string, stateId: string): Promise<ApiReturn> {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) {
|
|
throw new Error('Failed to get process from db');
|
|
}
|
|
try {
|
|
const members = Object.values(this.getAllMembers()).map(member => ({
|
|
sp_addresses: member.sp_addresses
|
|
}));
|
|
const result = this.sdkClient.validate_state(process, stateId, members);
|
|
if (result.updated_process) {
|
|
await this.checkConnections(result.updated_process.current_process);
|
|
return result;
|
|
} else {
|
|
throw new Error('Empty updated_process in approveChangeReturn');
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to create prd response: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async rejectChange(processId: string, stateId: string): Promise<ApiReturn> {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) {
|
|
throw new Error('Failed to get process from db');
|
|
}
|
|
try {
|
|
return this.sdkClient.refuse_state(process, stateId);
|
|
} catch (e) {
|
|
throw new Error(`Failed to create prd response: ${e}`);
|
|
}
|
|
}
|
|
|
|
async resetDevice() {
|
|
this.sdkClient.reset_device();
|
|
|
|
// Clear all stores
|
|
const db = await Database.getInstance();
|
|
await db.clearStore(DATABASE_CONFIG.stores.wallet.name);
|
|
await db.clearStore(DATABASE_CONFIG.stores.shared_secrets.name);
|
|
await db.clearStore(DATABASE_CONFIG.stores.unconfirmed_secrets.name);
|
|
await db.clearStore(DATABASE_CONFIG.stores.processes.name);
|
|
await db.clearStore(DATABASE_CONFIG.stores.diffs.name);
|
|
}
|
|
|
|
sendNewTxMessage(message: string) {
|
|
sendMessage('NewTx', message);
|
|
}
|
|
|
|
sendCommitMessage(message: string) {
|
|
sendMessage('Commit', message);
|
|
}
|
|
|
|
sendCipherMessages(ciphers: string[]) {
|
|
for (let i = 0; i < ciphers.length; i++) {
|
|
const cipher = ciphers[i];
|
|
sendMessage('Cipher', cipher);
|
|
}
|
|
}
|
|
|
|
sendFaucetMessage(message: string): void {
|
|
sendMessage('Faucet', message);
|
|
}
|
|
|
|
async parseCipher(message: string) {
|
|
const membersList = Object.values(this.getAllMembers()).map(member => ({
|
|
sp_addresses: member.sp_addresses
|
|
}));
|
|
const processes = await this.getProcesses();
|
|
try {
|
|
// console.log('parsing new cipher');
|
|
const apiReturn = this.sdkClient.parse_cipher(message, membersList, processes);
|
|
await this.handleApiReturn(apiReturn);
|
|
|
|
// Device 1 wait Device 2
|
|
const waitingModal = document.getElementById('waiting-modal');
|
|
if (waitingModal) {
|
|
this.device2Ready = true;
|
|
}
|
|
} catch (e) {
|
|
// Log the error but don't treat it as critical during pairing process
|
|
console.warn(`Cipher parsing failed (this may be normal during pairing): ${e}`);
|
|
|
|
// Only log as error if it's not a pairing-related issue
|
|
if (!(e as Error).message?.includes('Failed to handle decrypted message')) {
|
|
console.error(`Parsed cipher with error: ${e}`);
|
|
}
|
|
}
|
|
// await this.saveCipherTxToDb(parsedTx)
|
|
}
|
|
|
|
async parseNewTx(newTxMsg: string | NewTxMessage) {
|
|
const parsedMsg: NewTxMessage = typeof newTxMsg === 'string' ? JSON.parse(newTxMsg) : newTxMsg;
|
|
if (parsedMsg.error !== null) {
|
|
console.error('Received error in new tx message:', parsedMsg.error);
|
|
this.updateUserStatus('❌ Transaction error received');
|
|
return;
|
|
}
|
|
|
|
// Notify user that a transaction was received
|
|
this.updateUserStatus('📨 New transaction received from blockchain...');
|
|
|
|
// Mark that we've received a transaction for waitForAmount
|
|
this.hasReceivedTransaction = true;
|
|
|
|
const membersList = Object.values(this.getAllMembers()).map(member => ({
|
|
sp_addresses: member.sp_addresses
|
|
}));
|
|
try {
|
|
// Does the transaction spend the tip of a process?
|
|
const prevouts = this.sdkClient.get_prevouts(parsedMsg.transaction);
|
|
console.log('prevouts:', prevouts);
|
|
for (const process of Object.values(this.processesCache)) {
|
|
const tip = process.states[process.states.length - 1].commited_in;
|
|
if (prevouts.includes(tip)) {
|
|
const processId = process.states[0].commited_in;
|
|
const newTip = this.sdkClient.get_txid(parsedMsg.transaction);
|
|
console.log('Transaction', newTip, 'spends the tip of process', processId);
|
|
// We take the data out of the output
|
|
const newStateId = this.sdkClient.get_opreturn(parsedMsg.transaction);
|
|
console.log('newStateId:', newStateId);
|
|
// We update the relevant process
|
|
const updatedProcess = this.sdkClient.process_commit_new_state(
|
|
process,
|
|
newStateId,
|
|
newTip
|
|
);
|
|
this.processesCache[processId] = updatedProcess;
|
|
console.log('updatedProcess:', updatedProcess);
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to parse new tx for commitments:', e);
|
|
}
|
|
|
|
try {
|
|
const parsedTx = this.sdkClient.parse_new_tx(newTxMsg, 0, membersList);
|
|
if (parsedTx) {
|
|
try {
|
|
await this.handleApiReturn(parsedTx);
|
|
const newDevice = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(newDevice);
|
|
|
|
// Force SDK to scan blocks to update wallet state after receiving tokens
|
|
console.log('🔄 Forcing SDK to scan blocks to update wallet state...');
|
|
try {
|
|
// Get current device to check birthday
|
|
const device = await this.getDeviceFromDatabase();
|
|
if (device?.sp_wallet) {
|
|
console.log('🔍 Device wallet state:', {
|
|
birthday: device.sp_wallet.birthday,
|
|
last_scan: device.sp_wallet.last_scan,
|
|
current_block: this.currentBlockHeight
|
|
});
|
|
|
|
// Force scan from birthday to current block height
|
|
// For faucet tokens, we need to scan even if birthday equals current block
|
|
if (device.sp_wallet.birthday <= this.currentBlockHeight) {
|
|
// For new wallets, scan from much earlier to catch faucet transactions
|
|
const scanFromHeight = device.sp_wallet.birthday === this.currentBlockHeight
|
|
? Math.max(0, this.currentBlockHeight - 100) // Scan from 100 blocks earlier for new wallets
|
|
: device.sp_wallet.birthday;
|
|
|
|
console.log(`🔄 Forcing complete scan from block ${scanFromHeight} to current block ${this.currentBlockHeight}...`);
|
|
await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL);
|
|
console.log('✅ Complete block scan completed');
|
|
|
|
// Update last_scan to current block height
|
|
device.sp_wallet.last_scan = this.currentBlockHeight;
|
|
await this.saveDeviceInDatabase(device);
|
|
console.log('✅ Wallet last_scan updated to current block height');
|
|
} else {
|
|
console.log('🔄 Using safe scan blocks...');
|
|
await this.safeScanBlocks();
|
|
}
|
|
} else {
|
|
console.log('🔄 Using safe scan blocks (no device found)...');
|
|
await this.safeScanBlocks();
|
|
}
|
|
} catch (scanError) {
|
|
console.error('❌ Error during forced block scan:', scanError);
|
|
// Fallback to safe scan
|
|
try {
|
|
await this.safeScanBlocks();
|
|
} catch (fallbackError) {
|
|
console.error('❌ Fallback scan also failed:', fallbackError);
|
|
}
|
|
}
|
|
|
|
// Check amount after scanning
|
|
const updatedAmount = await this.getAmount();
|
|
console.log(`💰 Amount after block scan: ${updatedAmount}`);
|
|
|
|
// Update user with scan results
|
|
if (updatedAmount > 0n) {
|
|
this.updateUserStatus(`💰 Wallet updated! Found ${updatedAmount} tokens`);
|
|
} else {
|
|
this.updateUserStatus('⏳ Transaction processed, waiting for confirmation...');
|
|
}
|
|
|
|
// Additional debugging: check if SDK is properly initialized
|
|
console.log('🔍 SDK debugging info:');
|
|
console.log('- Current block height:', this.currentBlockHeight);
|
|
console.log('- Blindbit URL:', BLINDBITURL);
|
|
console.log('- SDK client initialized:', !!this.sdkClient);
|
|
|
|
// Check wallet state in SDK
|
|
try {
|
|
const device = await this.getDeviceFromDatabase();
|
|
if (device?.sp_wallet) {
|
|
console.log('🔍 Wallet state:');
|
|
console.log('- Last scan:', device.sp_wallet.last_scan);
|
|
console.log('- Current block:', this.currentBlockHeight);
|
|
console.log('- Scan needed:', device.sp_wallet.last_scan < this.currentBlockHeight);
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error checking wallet state:', error);
|
|
}
|
|
} catch (scanError) {
|
|
console.error('❌ Failed to scan blocks:', scanError);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.debug(e);
|
|
}
|
|
}
|
|
|
|
public async handleApiReturn(apiReturn: ApiReturn) {
|
|
console.log(apiReturn);
|
|
if (apiReturn.partial_tx) {
|
|
try {
|
|
const res = this.sdkClient.sign_transaction(apiReturn.partial_tx);
|
|
apiReturn.new_tx_to_send = res.new_tx_to_send;
|
|
} catch (e) {
|
|
console.error('Failed to sign transaction:', e);
|
|
}
|
|
}
|
|
|
|
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) {
|
|
this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send));
|
|
await new Promise(r => setTimeout(r, 500));
|
|
}
|
|
|
|
if (apiReturn.secrets) {
|
|
const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets;
|
|
const confirmedSecrets = apiReturn.secrets.shared_secrets;
|
|
|
|
const db = await Database.getInstance();
|
|
for (const secret of unconfirmedSecrets) {
|
|
await db.addObject({
|
|
storeName: 'unconfirmed_secrets',
|
|
object: secret,
|
|
key: null,
|
|
});
|
|
}
|
|
const entries = Object.entries(confirmedSecrets).map(([key, value]) => ({ key, value }));
|
|
for (const entry of entries) {
|
|
try {
|
|
await db.addObject({
|
|
storeName: 'shared_secrets',
|
|
object: entry.value,
|
|
key: entry.key,
|
|
});
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
|
|
// We don't want to throw an error, it could simply be that we registered directly the shared secret
|
|
// this.removeUnconfirmedSecret(entry.value);
|
|
}
|
|
}
|
|
|
|
if (apiReturn.updated_process) {
|
|
const updatedProcess = apiReturn.updated_process;
|
|
|
|
const processId: string = updatedProcess.process_id;
|
|
|
|
if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) {
|
|
for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) {
|
|
const blob = this.hexToBlob(cipher);
|
|
try {
|
|
await this.saveBlobToDb(hash, blob);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save process to db
|
|
await this.saveProcessToDb(processId, updatedProcess.current_process);
|
|
|
|
if (updatedProcess.diffs && updatedProcess.diffs.length != 0) {
|
|
try {
|
|
await this.saveDiffsToDb(updatedProcess.diffs);
|
|
} catch (e) {
|
|
console.error('Failed to save diffs to db:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) {
|
|
for (const hash of apiReturn.push_to_storage) {
|
|
const blob = await this.getBlobFromDb(hash);
|
|
if (blob) {
|
|
// Get the storages from the diff data
|
|
const diff = await this.getDiffByValueFromDb(hash);
|
|
if (diff) {
|
|
const storages = diff.storages;
|
|
await this.saveDataToStorage(hash, storages, blob, null);
|
|
} else {
|
|
console.error('Failed to get diff from db for hash', hash);
|
|
}
|
|
} else {
|
|
console.error('Failed to get data from db for hash', hash);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (apiReturn.commit_to_send) {
|
|
const commit = apiReturn.commit_to_send;
|
|
this.sendCommitMessage(JSON.stringify(commit));
|
|
}
|
|
|
|
if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) {
|
|
this.sendCipherMessages(apiReturn.ciphers_to_send);
|
|
}
|
|
}
|
|
|
|
public async openPairingConfirmationModal(processId: string) {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) {
|
|
console.error('Failed to find pairing process');
|
|
return;
|
|
}
|
|
const firstState = process.states[0];
|
|
const roles = firstState.roles;
|
|
const stateId = firstState.state_id;
|
|
try {
|
|
await this.routingInstance.openPairingConfirmationModal(roles, processId, stateId);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
public async waitForPairingCommitment(
|
|
processId: string,
|
|
maxRetries: number = 30,
|
|
retryDelay: number = 2000
|
|
): Promise<void> {
|
|
console.log(`🔍 DEBUG: waitForPairingCommitment called with processId: ${processId}`);
|
|
console.log(`⏳ Waiting for pairing process ${processId} to be committed and synchronized...`);
|
|
console.log(`🔄 This may take some time as we wait for SDK synchronization...`);
|
|
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
// Check device state directly without forcing updateDevice
|
|
const device = this.dumpDeviceFromMemory();
|
|
console.log(
|
|
`🔍 Attempt ${i + 1}/${maxRetries}: pairing_process_commitment =`,
|
|
device.pairing_process_commitment
|
|
);
|
|
|
|
// Additional debugging: Check if we can get the pairing process ID
|
|
let currentPairingId: string | null = null;
|
|
try {
|
|
currentPairingId = this.sdkClient.get_pairing_process_id();
|
|
console.log(`🔍 Current pairing process ID from SDK: ${currentPairingId}`);
|
|
} catch (e) {
|
|
console.log(`⚠️ SDK pairing process ID not available yet: ${(e as Error).message}`);
|
|
}
|
|
|
|
// Try to force synchronization by requesting the process from peers
|
|
if (i % 3 === 0 && i > 0) {
|
|
try {
|
|
console.log(`🔄 Attempting to request process from peers...`);
|
|
await this.requestDataFromPeers(processId, [], []);
|
|
console.log(`✅ Process request sent to peers`);
|
|
} catch (e) {
|
|
console.log(`⚠️ Failed to request process from peers: ${(e as Error).message}`);
|
|
}
|
|
}
|
|
|
|
// For quorum=1, try to force process synchronization
|
|
if (i === 2) {
|
|
try {
|
|
console.log(`🔄 Forcing process synchronization for quorum=1 test...`);
|
|
// Force the SDK to recognize this as a pairing process
|
|
const process = await this.getProcess(processId);
|
|
if (process) {
|
|
console.log(`🔄 Process found, attempting to sync with SDK...`);
|
|
// Try to trigger SDK synchronization
|
|
await this.sdkClient.get_pairing_process_id();
|
|
}
|
|
} catch (e) {
|
|
console.log(`⚠️ Process synchronization attempt failed: ${(e as Error).message}`);
|
|
}
|
|
}
|
|
|
|
// Check if the process exists in our processes list
|
|
try {
|
|
const process = await this.getProcess(processId);
|
|
if (process) {
|
|
console.log(`🔍 Process exists: ${processId}, states: ${process.states?.length || 0}`);
|
|
const lastState = process.states?.[process.states.length - 1];
|
|
if (lastState) {
|
|
console.log(`🔍 Last state ID: ${lastState.state_id}`);
|
|
}
|
|
} else {
|
|
console.log(`⚠️ Process not found in local processes: ${processId}`);
|
|
}
|
|
} catch (e) {
|
|
console.log(`⚠️ Error checking process: ${(e as Error).message}`);
|
|
}
|
|
|
|
// Check WebSocket connection and handshake data
|
|
try {
|
|
console.log(
|
|
`🔍 WebSocket connections: ${Object.keys(this.relayAddresses).length} relays`
|
|
);
|
|
console.log(`🔍 Current block height: ${this.currentBlockHeight}`);
|
|
console.log(`🔍 Members list size: ${Object.keys(this.membersList).length}`);
|
|
} catch (e) {
|
|
console.log(`⚠️ Error checking WebSocket state: ${(e as Error).message}`);
|
|
}
|
|
|
|
// Check if the commitment is set and not null/empty
|
|
if (
|
|
device.pairing_process_commitment &&
|
|
device.pairing_process_commitment !== null &&
|
|
device.pairing_process_commitment !== ''
|
|
) {
|
|
console.log('✅ Pairing process commitment found:', device.pairing_process_commitment);
|
|
return;
|
|
}
|
|
|
|
// For quorum=1.0 processes, the creator must commit themselves
|
|
// Check if the process is ready for the creator to commit
|
|
if (currentPairingId && currentPairingId === processId) {
|
|
console.log(
|
|
'✅ Creator process is synchronized and ready for self-commitment (quorum=1.0)'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// For quorum=1 test, if we have a process but no commitment yet,
|
|
// try to force synchronization by calling updateDevice more frequently
|
|
if (i < 5) {
|
|
try {
|
|
await this.updateDevice();
|
|
console.log(`🔄 Forced device update on attempt ${i + 1}`);
|
|
} catch (e) {
|
|
console.log(`⚠️ Forced device update failed: ${(e as Error).message}`);
|
|
}
|
|
}
|
|
|
|
// If we have the process but SDK doesn't know about it yet, try to force SDK sync
|
|
if (currentPairingId === null && i > 2) {
|
|
try {
|
|
console.log(`🔄 Attempting to force SDK synchronization for process ${processId}...`);
|
|
// Try to manually pair the device with the process
|
|
const process = await this.getProcess(processId);
|
|
if (process && process.states && process.states.length > 0) {
|
|
const lastState = process.states[process.states.length - 1];
|
|
if (lastState.public_data && lastState.public_data['pairedAddresses']) {
|
|
const pairedAddresses = this.decodeValue(lastState.public_data['pairedAddresses']);
|
|
console.log(
|
|
`🔄 Manually pairing device with addresses: ${JSON.stringify(pairedAddresses)}`
|
|
);
|
|
this.sdkClient.pair_device(processId, pairedAddresses);
|
|
console.log(`✅ Manual pairing completed`);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log(`⚠️ Manual pairing failed: ${(e as Error).message}`);
|
|
}
|
|
}
|
|
|
|
console.log(`⏳ Still waiting for SDK synchronization... (${i + 1}/${maxRetries})`);
|
|
|
|
// Only try updateDevice every 5 attempts to avoid spam
|
|
if (i % 5 === 0 && i > 0) {
|
|
try {
|
|
await this.updateDevice();
|
|
console.log(`✅ Device update successful on attempt ${i + 1}`);
|
|
} catch (e) {
|
|
console.log(
|
|
`⚠️ Device update failed on attempt ${i + 1} (process may not be committed yet): ${(e as Error).message}`
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log(
|
|
`❌ Attempt ${i + 1}/${maxRetries}: Error during synchronization - ${(e as Error).message}`
|
|
);
|
|
}
|
|
|
|
if (i < maxRetries - 1) {
|
|
console.log(`⏳ Waiting ${retryDelay}ms before next attempt...`);
|
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`❌ Pairing process ${processId} was not synchronized after ${maxRetries} attempts (${(maxRetries * retryDelay) / 1000}s)`
|
|
);
|
|
}
|
|
|
|
public async confirmPairing(pairingId?: string) {
|
|
try {
|
|
// Is the wasm paired?
|
|
console.log('confirmPairing');
|
|
let processId: string;
|
|
if (pairingId) {
|
|
processId = pairingId;
|
|
console.log('pairingId (provided):', processId);
|
|
} else if (this.processId) {
|
|
processId = this.processId;
|
|
console.log('pairingId (from stored processId):', processId);
|
|
} else {
|
|
// Try to get pairing process ID, with retry if it fails
|
|
let retries = 3;
|
|
while (retries > 0) {
|
|
try {
|
|
processId = this.getPairingProcessId();
|
|
console.log('pairingId (from SDK):', processId);
|
|
break;
|
|
} catch (e) {
|
|
retries--;
|
|
if (retries === 0) {
|
|
throw e;
|
|
}
|
|
console.log(`Failed to get pairing process ID, retrying... (${retries} attempts left)`);
|
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second before retry
|
|
}
|
|
}
|
|
}
|
|
// TODO confirm that the pairing process id is known, commited
|
|
const newDevice = this.dumpDeviceFromMemory();
|
|
console.log('newDevice:', newDevice);
|
|
await this.saveDeviceInDatabase(newDevice);
|
|
console.log('Device saved in database');
|
|
} catch (e) {
|
|
console.error('Failed to confirm pairing');
|
|
return;
|
|
}
|
|
}
|
|
|
|
public async updateDevice(): Promise<void> {
|
|
let myPairingProcessId: string;
|
|
try {
|
|
myPairingProcessId = this.getPairingProcessId();
|
|
} catch (e) {
|
|
console.error('Failed to get pairing process id');
|
|
return;
|
|
}
|
|
|
|
const myPairingProcess = await this.getProcess(myPairingProcessId);
|
|
if (!myPairingProcess) {
|
|
console.error('Unknown pairing process');
|
|
return;
|
|
}
|
|
const myPairingState = this.getLastCommitedState(myPairingProcess);
|
|
if (myPairingState) {
|
|
const encodedSpAddressList = myPairingState.public_data['pairedAddresses'];
|
|
const spAddressList = this.decodeValue(encodedSpAddressList);
|
|
if (spAddressList.length === 0) {
|
|
console.error('Empty pairedAddresses');
|
|
return;
|
|
}
|
|
// We can check if our address is included and simply unpair if it's not
|
|
if (!spAddressList.includes(await this.getDeviceAddress())) {
|
|
await this.unpairDevice();
|
|
return;
|
|
}
|
|
// We can update the device with the new addresses
|
|
this.sdkClient.unpair_device();
|
|
this.sdkClient.pair_device(myPairingProcessId, spAddressList);
|
|
const newDevice = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(newDevice);
|
|
}
|
|
}
|
|
|
|
public pairDevice(processId: string, spAddressList: string[]): void {
|
|
try {
|
|
this.sdkClient.pair_device(processId, spAddressList);
|
|
} catch (e) {
|
|
throw new Error(`Failed to pair device: ${e}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Vérifie que les clés du wallet sont disponibles avant toute opération
|
|
*/
|
|
private async ensureWalletKeysAvailable(): Promise<void> {
|
|
try {
|
|
const device = this.dumpDeviceFromMemory();
|
|
if (!device?.sp_wallet) {
|
|
throw new Error('❌ Wallet not initialized - WebAuthn decryption required');
|
|
}
|
|
|
|
// Vérifier si les clés sont déjà disponibles dans le device
|
|
if (device.sp_wallet.spend_key && device.sp_wallet.scan_key) {
|
|
console.log('✅ Wallet keys available for operation');
|
|
return;
|
|
}
|
|
|
|
// Si les clés ne sont pas disponibles, essayer de les restaurer depuis les credentials
|
|
console.log('🔐 Wallet keys not in memory, attempting to restore from credentials...');
|
|
|
|
try {
|
|
const { SecureCredentialsService } = await import('./secure-credentials.service');
|
|
const secureCredentialsService = SecureCredentialsService.getInstance();
|
|
|
|
// Récupérer les credentials
|
|
const credentials = await secureCredentialsService.retrieveCredentials('');
|
|
if (!credentials) {
|
|
throw new Error('No credentials found');
|
|
}
|
|
|
|
// Restaurer les clés dans le device en mémoire
|
|
if (device.sp_wallet) {
|
|
device.sp_wallet.spend_key = credentials.spendKey;
|
|
device.sp_wallet.scan_key = credentials.scanKey;
|
|
|
|
// Restaurer le device avec les clés
|
|
this.restoreDevice(device);
|
|
|
|
console.log('✅ Wallet keys restored from credentials');
|
|
} else {
|
|
throw new Error('No wallet in device to restore keys to');
|
|
}
|
|
} catch (credentialError) {
|
|
console.error('❌ Failed to restore wallet keys from credentials:', credentialError);
|
|
throw new Error('❌ Wallet keys not available - WebAuthn decryption required');
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Wallet keys not available:', error);
|
|
throw new Error('❌ Wallet keys not available - WebAuthn decryption required');
|
|
}
|
|
}
|
|
|
|
public async getAmount(): Promise<bigint> {
|
|
if (!this.sdkClient) {
|
|
throw new Error('SDK not initialized - cannot get amount');
|
|
}
|
|
|
|
// Vérifier que les clés sont disponibles avant toute opération
|
|
await this.ensureWalletKeysAvailable();
|
|
|
|
try {
|
|
const amount = this.sdkClient.get_available_amount();
|
|
console.log(`💰 SDK get_available_amount() returned: ${amount}`);
|
|
|
|
// Additional debugging: check wallet state
|
|
try {
|
|
const device = this.dumpDeviceFromMemory();
|
|
if (device?.sp_wallet) {
|
|
console.log(`🔍 Wallet debugging info:`, {
|
|
birthday: device.sp_wallet.birthday,
|
|
last_scan: device.sp_wallet.last_scan,
|
|
current_block: this.currentBlockHeight,
|
|
has_spend_key: !!device.sp_wallet.spend_key,
|
|
has_scan_key: !!device.sp_wallet.scan_key
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.warn('⚠️ Error getting wallet debugging info:', error);
|
|
}
|
|
|
|
return amount;
|
|
} catch (error) {
|
|
console.error('❌ Error calling get_available_amount():', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getDeviceAddress(): Promise<string> {
|
|
try {
|
|
if (!this.sdkClient) {
|
|
throw new Error('WebAssembly SDK not initialized - memory too high');
|
|
}
|
|
|
|
// Vérifier que les clés sont disponibles avant toute opération
|
|
await this.ensureWalletKeysAvailable();
|
|
|
|
return this.sdkClient.get_address();
|
|
} catch (e) {
|
|
throw new Error(`Failed to get device address: ${e}`);
|
|
}
|
|
}
|
|
|
|
getCurrentBlockHeight(): number {
|
|
return this.currentBlockHeight;
|
|
}
|
|
|
|
public dumpDeviceFromMemory(): Device {
|
|
try {
|
|
return this.sdkClient.dump_device();
|
|
} catch (e) {
|
|
throw new Error(`Failed to dump device: ${e}`);
|
|
}
|
|
}
|
|
|
|
public dumpNeuteredDevice(): Device | null {
|
|
try {
|
|
return this.sdkClient.dump_neutered_device();
|
|
} catch (e) {
|
|
console.error(`Failed to dump device: ${e}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public getPairingProcessId(): string {
|
|
try {
|
|
return this.sdkClient.get_pairing_process_id();
|
|
} catch (e) {
|
|
throw new Error(`Failed to get pairing process: ${e}`);
|
|
}
|
|
}
|
|
|
|
async saveDeviceInDatabase(device: Device): Promise<void> {
|
|
console.log('🔐 saveDeviceInDatabase called - starting encryption process...');
|
|
try {
|
|
// Récupérer la clé PBKDF2 pour chiffrer le device
|
|
console.log('🔐 Retrieving PBKDF2 key for device encryption...');
|
|
const { SecureCredentialsService } = await import('./secure-credentials.service');
|
|
const secureCredentialsService = SecureCredentialsService.getInstance();
|
|
|
|
// Trouver le mode de sécurité qui fonctionne
|
|
const allSecurityModes = ['none', 'otp', 'password', 'os', 'proton-pass'];
|
|
let pbkdf2Key: string | null = null;
|
|
let workingMode: string | null = null;
|
|
|
|
for (const mode of allSecurityModes) {
|
|
try {
|
|
const hasKey = await secureCredentialsService.hasPBKDF2Key(mode as any);
|
|
if (hasKey) {
|
|
const key = await secureCredentialsService.retrievePBKDF2Key(mode as any);
|
|
if (key) {
|
|
pbkdf2Key = key;
|
|
workingMode = mode;
|
|
console.log(`✅ PBKDF2 key found for mode: ${mode}`);
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Continue to next mode
|
|
console.log(`⚠️ No PBKDF2 key for mode ${mode}`);
|
|
}
|
|
}
|
|
|
|
if (!pbkdf2Key) {
|
|
throw new Error('Failed to retrieve PBKDF2 key - cannot encrypt device');
|
|
}
|
|
|
|
console.log('🔐 Encrypting device with PBKDF2 key...');
|
|
// Chiffrer le device
|
|
const { EncryptionService } = await import('./encryption.service');
|
|
const encryptionService = EncryptionService.getInstance();
|
|
const deviceString = JSON.stringify(device);
|
|
const encryptedDevice = await encryptionService.encrypt(deviceString, pbkdf2Key);
|
|
console.log('✅ Device encrypted successfully');
|
|
|
|
// Récupérer le wallet existant pour préserver encrypted_wallet
|
|
console.log('🔍 Retrieving existing wallet to preserve encrypted_wallet...');
|
|
let encryptedWallet: string | undefined = undefined;
|
|
|
|
// Récupérer le wallet chiffré depuis la base de données directement
|
|
const dbTemp = await new Promise<IDBDatabase>((resolve, reject) => {
|
|
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
|
|
const walletStoreName = DATABASE_CONFIG.stores.wallet.name;
|
|
const walletData = await new Promise<any>((resolve, reject) => {
|
|
const tx = dbTemp.transaction(walletStoreName, 'readonly');
|
|
const store = tx.objectStore(walletStoreName);
|
|
const getRequest = store.get('1');
|
|
getRequest.onsuccess = () => resolve(getRequest.result);
|
|
getRequest.onerror = () => reject(getRequest.error);
|
|
});
|
|
|
|
if (walletData?.encrypted_wallet) {
|
|
encryptedWallet = walletData.encrypted_wallet;
|
|
console.log('✅ encrypted_wallet preserved');
|
|
} else {
|
|
console.log('⚠️ No existing encrypted_wallet to preserve');
|
|
}
|
|
|
|
// Sauvegarder avec le format chiffré
|
|
console.log('💾 Saving encrypted device to database...');
|
|
|
|
// Utiliser directement IndexedDB au lieu du service Database pour éviter les problèmes de service worker
|
|
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
|
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
|
|
const walletStore = DATABASE_CONFIG.stores.wallet.name;
|
|
|
|
// Sauvegarder le wallet chiffré directement avec IndexedDB
|
|
await new Promise<void>((resolve, reject) => {
|
|
const transaction = db.transaction([walletStore], 'readwrite');
|
|
const store = transaction.objectStore(walletStore);
|
|
|
|
// Sauvegarder le nouveau wallet chiffré (put écrase automatiquement si existe)
|
|
const walletObject: any = {
|
|
pre_id: '1',
|
|
encrypted_device: encryptedDevice
|
|
};
|
|
|
|
// Préserver encrypted_wallet s'il existe
|
|
if (encryptedWallet) {
|
|
walletObject.encrypted_wallet = encryptedWallet;
|
|
}
|
|
|
|
// Définir les handlers de transaction avant le put
|
|
transaction.oncomplete = async () => {
|
|
console.log('✅ Transaction completed for wallet save');
|
|
|
|
// Vérifier que le wallet a bien été sauvegardé en le récupérant depuis la base
|
|
try {
|
|
const verificationDb = await new Promise<IDBDatabase>((resolveDb, rejectDb) => {
|
|
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
|
|
request.onsuccess = () => resolveDb(request.result);
|
|
request.onerror = () => rejectDb(request.error);
|
|
});
|
|
|
|
const verificationTx = verificationDb.transaction([walletStore], 'readonly');
|
|
const verificationStore = verificationTx.objectStore(walletStore);
|
|
const verifyRequest = verificationStore.get('1');
|
|
|
|
await new Promise<void>((resolveVerify, rejectVerify) => {
|
|
verifyRequest.onsuccess = () => {
|
|
const savedData = verifyRequest.result;
|
|
if (savedData?.encrypted_device === encryptedDevice) {
|
|
console.log('✅ Verified: Device correctly saved in database');
|
|
resolveVerify();
|
|
} else {
|
|
console.error('❌ Verification failed: Device not found or encrypted data mismatch');
|
|
rejectVerify(new Error('Device save verification failed'));
|
|
}
|
|
};
|
|
|
|
verifyRequest.onerror = () => {
|
|
console.error('❌ Verification failed: Could not retrieve saved device', verifyRequest.error);
|
|
rejectVerify(verifyRequest.error);
|
|
};
|
|
});
|
|
|
|
resolve();
|
|
} catch (verifyError) {
|
|
reject(verifyError);
|
|
}
|
|
};
|
|
|
|
transaction.onerror = () => {
|
|
console.error('❌ Transaction failed:', transaction.error);
|
|
reject(transaction.error);
|
|
};
|
|
|
|
const putRequest = store.put(walletObject);
|
|
putRequest.onsuccess = () => {
|
|
console.log('✅ Device saved to database with encryption');
|
|
// La vérification se fera dans transaction.oncomplete
|
|
};
|
|
putRequest.onerror = () => {
|
|
console.error('❌ Failed to save wallet:', putRequest.error);
|
|
reject(putRequest.error);
|
|
};
|
|
});
|
|
} catch (e) {
|
|
console.error('❌ Error saving device to database:', e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async getDeviceFromDatabase(): Promise<Device | null> {
|
|
console.log('🔍 DEBUG: getDeviceFromDatabase - attempting to get wallet with key "1"');
|
|
|
|
// Utiliser directement IndexedDB au lieu du service Database pour éviter les problèmes de service worker
|
|
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
|
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
|
|
const walletStore = DATABASE_CONFIG.stores.wallet.name;
|
|
console.log('🔍 DEBUG: getDeviceFromDatabase - db opened directly, objectStoreNames:', Array.from(db.objectStoreNames));
|
|
|
|
try {
|
|
const dbRes = await new Promise<any>((resolve, reject) => {
|
|
const tx = db.transaction(walletStore, 'readonly');
|
|
const store = tx.objectStore(walletStore);
|
|
const getRequest = store.get('1');
|
|
getRequest.onsuccess = () => resolve(getRequest.result);
|
|
getRequest.onerror = () => reject(getRequest.error);
|
|
});
|
|
|
|
console.log('🔍 DEBUG: getDeviceFromDatabase - db.getObject result:', dbRes);
|
|
if (!dbRes) {
|
|
console.log('🔍 DEBUG: getDeviceFromDatabase - no data found for key "1"');
|
|
return null;
|
|
}
|
|
|
|
// Check if data is encrypted (new format) or plain (old format)
|
|
if (dbRes['encrypted_device']) {
|
|
// New encrypted format - need to decrypt
|
|
console.log('🔐 Wallet found in encrypted format, decrypting...');
|
|
|
|
// Get the PBKDF2 key based on security mode
|
|
const { SecureCredentialsService } = await import('./secure-credentials.service');
|
|
const secureCredentialsService = SecureCredentialsService.getInstance();
|
|
|
|
// Get all security modes to find which one works
|
|
// Mettre 'none' en premier pour éviter d'ouvrir la fenêtre du navigateur
|
|
const allSecurityModes = ['none', 'otp', 'password', 'os', 'proton-pass'];
|
|
let pbkdf2Key: string | null = null;
|
|
let workingMode: string | null = null;
|
|
|
|
for (const mode of allSecurityModes) {
|
|
try {
|
|
// Vérifier d'abord silencieusement si une clé existe
|
|
const hasKey = await secureCredentialsService.hasPBKDF2Key(mode as any);
|
|
if (hasKey) {
|
|
// Si une clé existe, essayer de la récupérer
|
|
const key = await secureCredentialsService.retrievePBKDF2Key(mode as any);
|
|
if (key) {
|
|
pbkdf2Key = key;
|
|
workingMode = mode;
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Continue to next mode
|
|
}
|
|
}
|
|
|
|
if (!pbkdf2Key) {
|
|
throw new Error('Failed to retrieve PBKDF2 key - cannot decrypt wallet');
|
|
}
|
|
|
|
// Decrypt the device
|
|
const { EncryptionService } = await import('./encryption.service');
|
|
const encryptionService = EncryptionService.getInstance();
|
|
|
|
const decryptedDeviceString = await encryptionService.decrypt(
|
|
dbRes['encrypted_device'],
|
|
pbkdf2Key
|
|
);
|
|
|
|
const decryptedDevice = JSON.parse(decryptedDeviceString);
|
|
console.log('✅ Wallet decrypted successfully');
|
|
return decryptedDevice;
|
|
} else if (dbRes['device']) {
|
|
// Old plain format (backward compatibility)
|
|
console.log('⚠️ Wallet found in old format (not encrypted)');
|
|
return dbRes['device'];
|
|
} else {
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to retrieve device from db: ${e}`);
|
|
}
|
|
}
|
|
|
|
async deleteAccount(): Promise<void> {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
// Clear all stores
|
|
await db.clearStore(DATABASE_CONFIG.stores.wallet.name);
|
|
await db.clearStore(DATABASE_CONFIG.stores.processes.name);
|
|
await db.clearStore(DATABASE_CONFIG.stores.shared_secrets.name);
|
|
await db.clearStore(DATABASE_CONFIG.stores.unconfirmed_secrets.name);
|
|
await db.clearStore(DATABASE_CONFIG.stores.diffs.name);
|
|
await db.clearStore(DATABASE_CONFIG.stores.data.name);
|
|
await db.clearStore(DATABASE_CONFIG.stores.labels.name);
|
|
|
|
// Clear localStorage
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
|
|
// Clear IndexedDB completely
|
|
await this.clearAllIndexedDB();
|
|
|
|
console.log('✅ Account completely deleted');
|
|
} catch (e) {
|
|
console.error('❌ Error deleting account:', e);
|
|
throw new Error(`Failed to delete account: ${e}`);
|
|
}
|
|
}
|
|
|
|
private async clearAllIndexedDB(): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const deleteReq = indexedDB.deleteDatabase('4nk');
|
|
deleteReq.onsuccess = () => {
|
|
console.log('✅ IndexedDB database deleted');
|
|
resolve();
|
|
};
|
|
deleteReq.onerror = () => {
|
|
console.error('❌ Error deleting IndexedDB database');
|
|
reject(deleteReq.error);
|
|
};
|
|
});
|
|
}
|
|
|
|
async getMemberFromDevice(): Promise<string[] | null> {
|
|
try {
|
|
const device = await this.getDeviceFromDatabase();
|
|
if (device) {
|
|
const pairedMember = device['paired_member'];
|
|
return pairedMember.sp_addresses;
|
|
} else {
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to retrieve paired_member from device: ${e}`);
|
|
}
|
|
}
|
|
|
|
isChildRole(parent: any, child: any): boolean {
|
|
try {
|
|
this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child));
|
|
} catch (e) {
|
|
console.error(e);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
|
|
let us;
|
|
try {
|
|
us = this.sdkClient.get_pairing_process_id();
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
|
|
return this.rolesContainsMember(roles, us);
|
|
}
|
|
|
|
rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean {
|
|
for (const roleDef of Object.values(roles)) {
|
|
if (roleDef.members.includes(pairingProcessId)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async dumpWallet() {
|
|
const wallet = await this.sdkClient.dump_wallet();
|
|
return wallet;
|
|
}
|
|
|
|
public createFaucetMessage() {
|
|
const message = this.sdkClient.create_faucet_msg();
|
|
return message;
|
|
}
|
|
|
|
async createNewDevice() {
|
|
let spAddress = '';
|
|
try {
|
|
if (!this.sdkClient) {
|
|
throw new Error('WebAssembly SDK not initialized - cannot create device');
|
|
}
|
|
// We set birthday later when we have the chain tip from relay
|
|
console.log('🔧 Creating new device with birthday 0...');
|
|
spAddress = await this.sdkClient.create_new_device(0, 'signet');
|
|
console.log('✅ Device created with address:', spAddress);
|
|
|
|
// Force wallet generation to ensure keys are created
|
|
console.log('🔧 Forcing wallet generation...');
|
|
try {
|
|
const wallet = await this.sdkClient.dump_wallet();
|
|
console.log('✅ Wallet generated:', wallet);
|
|
} catch (walletError) {
|
|
console.warn('⚠️ Wallet generation failed:', walletError);
|
|
}
|
|
|
|
const device = this.dumpDeviceFromMemory();
|
|
console.log('🔍 Device details after creation:', {
|
|
has_spend_key: !!device.sp_wallet?.spend_key,
|
|
has_scan_key: !!device.sp_wallet?.scan_key,
|
|
birthday: device.sp_wallet?.birthday,
|
|
sp_address: device.sp_wallet?.address
|
|
});
|
|
|
|
await this.saveDeviceInDatabase(device);
|
|
console.log('✅ Device saved to database');
|
|
} catch (e) {
|
|
console.error('Services ~ Error:', e);
|
|
}
|
|
|
|
return spAddress;
|
|
}
|
|
|
|
public restoreDevice(device: Device) {
|
|
try {
|
|
this.sdkClient.restore_device(device);
|
|
|
|
// Force wallet generation to ensure keys are available
|
|
console.log('🔧 Forcing wallet generation after restore...');
|
|
try {
|
|
const wallet = this.sdkClient.dump_wallet();
|
|
console.log('✅ Wallet restored:', wallet);
|
|
} catch (walletError) {
|
|
console.warn('⚠️ Wallet restoration failed:', walletError);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
public async waitForBlockHeight(): Promise<void> {
|
|
console.log('⏳ Waiting for block height to be set...');
|
|
|
|
// Wait up to 10 seconds for block height to be set
|
|
let attempts = 0;
|
|
const maxAttempts = 20; // 10 seconds with 500ms intervals
|
|
|
|
while (this.currentBlockHeight === -1 && attempts < maxAttempts) {
|
|
attempts++;
|
|
console.log(`⏳ Waiting for block height... (attempt ${attempts}/${maxAttempts})`);
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
|
|
if (this.currentBlockHeight === -1) {
|
|
throw new Error('Timeout waiting for block height from relay');
|
|
}
|
|
|
|
console.log(`✅ Block height received: ${this.currentBlockHeight}`);
|
|
}
|
|
|
|
/**
|
|
* Ensures a complete initial scan is performed before requesting faucet tokens
|
|
* This prevents the race condition between scan and faucet transactions
|
|
* Only performs scan if wallet is not already synchronized
|
|
*/
|
|
public async ensureCompleteInitialScan(): Promise<void> {
|
|
console.log('🔄 Ensuring complete initial scan...');
|
|
|
|
try {
|
|
const device = await this.getDeviceFromDatabase();
|
|
if (!device?.sp_wallet) {
|
|
throw new Error('Device not found or wallet not initialized');
|
|
}
|
|
|
|
// Check if wallet is already synchronized
|
|
const lastScan = device.sp_wallet.last_scan || 0;
|
|
const isSynchronized = lastScan >= this.currentBlockHeight;
|
|
|
|
if (isSynchronized) {
|
|
console.log(`✅ Wallet already synchronized (last_scan: ${lastScan}, current: ${this.currentBlockHeight})`);
|
|
return;
|
|
}
|
|
|
|
// Only scan if wallet is not synchronized
|
|
console.log(`🔄 Wallet needs synchronization (last_scan: ${lastScan}, current: ${this.currentBlockHeight})`);
|
|
console.log(`🔄 Performing scan from block ${lastScan} to ${this.currentBlockHeight}...`);
|
|
|
|
await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL);
|
|
console.log('✅ Complete initial scan completed');
|
|
|
|
// Update last_scan to current block height
|
|
device.sp_wallet.last_scan = this.currentBlockHeight;
|
|
await this.saveDeviceInDatabase(device);
|
|
console.log('✅ Wallet scan state updated');
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error during complete initial scan:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
public async updateDeviceBlockHeight(): Promise<void> {
|
|
if (this.currentBlockHeight === -1) {
|
|
throw new Error('Current block height not set');
|
|
}
|
|
|
|
// Update user status
|
|
this.updateUserStatus('🔄 Synchronizing wallet with blockchain...');
|
|
|
|
let device: Device | null = null;
|
|
try {
|
|
device = await this.getDeviceFromDatabase();
|
|
} catch (e) {
|
|
throw new Error(`Failed to get device from database: ${e}`);
|
|
}
|
|
|
|
if (!device) {
|
|
throw new Error('Device not found');
|
|
}
|
|
|
|
const birthday = device.sp_wallet.birthday;
|
|
if (birthday === undefined || birthday === null) {
|
|
throw new Error('Birthday not found');
|
|
}
|
|
|
|
if (birthday === 0) {
|
|
// This is a new device, set birthday to scan from much earlier to catch faucet transactions
|
|
// Scan from 100 blocks earlier to ensure we catch all faucet transactions
|
|
console.log('🔧 Updating birthday for new device:', {
|
|
old_birthday: device.sp_wallet.birthday,
|
|
new_birthday: Math.max(0, this.currentBlockHeight - 100),
|
|
current_block: this.currentBlockHeight
|
|
});
|
|
|
|
device.sp_wallet.birthday = Math.max(0, this.currentBlockHeight - 100);
|
|
// We also set last_scan to the same value initially
|
|
device.sp_wallet.last_scan = device.sp_wallet.birthday;
|
|
try {
|
|
// First set the updated device in memory
|
|
this.sdkClient.restore_device(device);
|
|
|
|
// Vérifier que le device a été restauré en mémoire
|
|
const restoredDevice = this.dumpDeviceFromMemory();
|
|
if (restoredDevice?.sp_wallet?.birthday === device.sp_wallet.birthday) {
|
|
console.log('✅ Device restored in memory with updated birthday:', device.sp_wallet.birthday);
|
|
} else {
|
|
throw new Error(`Device restoration failed: expected birthday ${device.sp_wallet.birthday}, got ${restoredDevice?.sp_wallet?.birthday}`);
|
|
}
|
|
|
|
// Then save it to database
|
|
await this.saveDeviceInDatabase(device);
|
|
|
|
// Vérifier que le device a été sauvegardé en base de données
|
|
const savedDevice = await this.getDeviceFromDatabase();
|
|
if (savedDevice?.sp_wallet?.birthday === device.sp_wallet.birthday) {
|
|
console.log('✅ Device saved to database with updated birthday:', device.sp_wallet.birthday);
|
|
} else {
|
|
throw new Error(`Device save verification failed: expected birthday ${device.sp_wallet.birthday}, got ${savedDevice?.sp_wallet?.birthday}`);
|
|
}
|
|
|
|
// For new wallets, perform initial scan to catch any existing transactions
|
|
console.log(`🔄 Performing initial scan for new wallet from block ${device.sp_wallet.birthday} to ${this.currentBlockHeight}...`);
|
|
await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL);
|
|
|
|
// Vérifier que le scan est terminé en vérifiant last_scan
|
|
const deviceAfterScan = this.dumpDeviceFromMemory();
|
|
if (deviceAfterScan?.sp_wallet?.last_scan === this.currentBlockHeight) {
|
|
console.log('✅ Initial scan completed for new wallet');
|
|
} else {
|
|
console.warn(`⚠️ Initial scan may not be complete: expected last_scan ${this.currentBlockHeight}, got ${deviceAfterScan?.sp_wallet?.last_scan}`);
|
|
}
|
|
|
|
// Update last_scan to current block height
|
|
device.sp_wallet.last_scan = this.currentBlockHeight;
|
|
await this.saveDeviceInDatabase(device);
|
|
|
|
// Vérifier que le device a été sauvegardé avec last_scan mis à jour
|
|
const finalDevice = await this.getDeviceFromDatabase();
|
|
if (finalDevice?.sp_wallet?.last_scan === this.currentBlockHeight) {
|
|
console.log('✅ New wallet initial scan completed and saved');
|
|
} else {
|
|
throw new Error(`Final save verification failed: expected last_scan ${this.currentBlockHeight}, got ${finalDevice?.sp_wallet?.last_scan}`);
|
|
}
|
|
console.log('✅ updateDeviceBlockHeight completed successfully for new device');
|
|
return;
|
|
} catch (e) {
|
|
throw new Error(`Failed to save updated device: ${e}`);
|
|
}
|
|
} else {
|
|
// This is existing device, we need to catch up if last_scan is lagging behind chain_tip
|
|
if (device.sp_wallet.last_scan < this.currentBlockHeight) {
|
|
// We need to catch up - this is the initial synchronization, not a duplicate scan
|
|
console.log('🔄 Initial wallet synchronization with blockchain...');
|
|
try {
|
|
await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL);
|
|
console.log('✅ Initial wallet synchronization completed');
|
|
} catch (e) {
|
|
console.error(`Failed to scan blocks: ${e}`);
|
|
return;
|
|
}
|
|
|
|
// If everything went well, we can update our storage
|
|
try {
|
|
const device = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(device);
|
|
this.updateUserStatus('✅ Wallet synchronized with blockchain');
|
|
} catch (e) {
|
|
console.error(`Failed to save updated device: ${e}`);
|
|
this.updateUserStatus('⚠️ Wallet synchronization completed with warnings');
|
|
}
|
|
} else {
|
|
// Up to date, just returns
|
|
this.updateUserStatus('✅ Wallet already synchronized');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async removeProcess(processId: string): Promise<void> {
|
|
const db = await Database.getInstance();
|
|
const storeName = 'processes';
|
|
|
|
try {
|
|
await db.deleteObject(storeName, processId);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
public async batchSaveProcessesToDb(processes: Record<string, Process>) {
|
|
if (Object.keys(processes).length === 0) {
|
|
return;
|
|
}
|
|
|
|
const db = await Database.getInstance();
|
|
const storeName = 'processes';
|
|
try {
|
|
await db.batchWriting({
|
|
storeName,
|
|
objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })),
|
|
});
|
|
this.processesCache = { ...this.processesCache, ...processes };
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public async saveProcessToDb(processId: string, process: Process) {
|
|
const db = await Database.getInstance();
|
|
const storeName = 'processes';
|
|
try {
|
|
await db.addObject({
|
|
storeName,
|
|
object: process,
|
|
key: processId,
|
|
});
|
|
|
|
// Update the process in the cache
|
|
this.processesCache[processId] = process;
|
|
} catch (e) {
|
|
console.error(`Failed to save process ${processId}: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async saveBlobToDb(hash: string, data: Blob) {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
await db.addObject({
|
|
storeName: 'data',
|
|
object: data,
|
|
key: hash,
|
|
});
|
|
} catch (e) {
|
|
console.error(`Failed to save data to db: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async getBlobFromDb(hash: string): Promise<Blob | null> {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
return await db.getObject('data', hash);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async saveDataToStorage(hash: string, storages: string[], data: Blob, ttl: number | null) {
|
|
try {
|
|
await storeData(storages, hash, data, ttl);
|
|
} catch (e) {
|
|
console.error(`Failed to store data with hash ${hash}: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async fetchValueFromStorage(hash: string): Promise<ArrayBuffer | null> {
|
|
const storages = [STORAGEURL];
|
|
|
|
return await retrieveData(storages, hash);
|
|
}
|
|
|
|
public async getDiffByValueFromDb(hash: string): Promise<UserDiff | null> {
|
|
const db = await Database.getInstance();
|
|
const diff = await db.getObject('diffs', hash);
|
|
return diff;
|
|
}
|
|
|
|
public async saveDiffsToDb(diffs: UserDiff[]) {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
for (const diff of diffs) {
|
|
await db.addObject({
|
|
storeName: 'diffs',
|
|
object: diff,
|
|
key: null,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to save process: ${e}`);
|
|
}
|
|
}
|
|
|
|
public async getProcess(processId: string): Promise<Process | null> {
|
|
if (!processId) {
|
|
return null;
|
|
}
|
|
|
|
if (this.processesCache[processId]) {
|
|
return this.processesCache[processId];
|
|
} else {
|
|
const db = await Database.getInstance();
|
|
const process = await db.getObject('processes', processId);
|
|
return process;
|
|
}
|
|
}
|
|
|
|
public async getProcesses(): Promise<Record<string, Process>> {
|
|
if (Object.keys(this.processesCache).length > 0) {
|
|
return this.processesCache;
|
|
} else {
|
|
try {
|
|
const db = await Database.getInstance();
|
|
this.processesCache = await db.dumpStore('processes');
|
|
return this.processesCache;
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
public async restoreProcessesFromBackUp(processes: Record<string, Process>) {
|
|
const db = await Database.getInstance();
|
|
const storeName = 'processes';
|
|
try {
|
|
await db.batchWriting({
|
|
storeName,
|
|
objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })),
|
|
});
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
|
|
await this.restoreProcessesFromDB();
|
|
}
|
|
|
|
// Restore processes cache from persistent storage
|
|
public async restoreProcessesFromDB() {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
const processes: Record<string, Process> = await db.dumpStore('processes');
|
|
if (processes && Object.keys(processes).length != 0) {
|
|
console.log(`Restoring ${Object.keys(processes).length} processes`);
|
|
this.processesCache = processes;
|
|
} else {
|
|
console.log('No processes to restore!');
|
|
}
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public async clearSecretsFromDB() {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
await db.clearStore('shared_secrets');
|
|
await db.clearStore('unconfirmed_secrets');
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
public async restoreSecretsFromBackUp(secretsStore: SecretsStore) {
|
|
const db = await Database.getInstance();
|
|
|
|
for (const secret of secretsStore.unconfirmed_secrets) {
|
|
await db.addObject({
|
|
storeName: 'unconfirmed_secrets',
|
|
object: secret,
|
|
key: null,
|
|
});
|
|
}
|
|
const entries = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({
|
|
key,
|
|
value,
|
|
}));
|
|
for (const entry of entries) {
|
|
await db.addObject({
|
|
storeName: 'shared_secrets',
|
|
object: entry.value,
|
|
key: entry.key,
|
|
});
|
|
}
|
|
|
|
// Now we can transfer them to memory
|
|
await this.restoreSecretsFromDB();
|
|
}
|
|
|
|
public async restoreSecretsFromDB() {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
if (!this.sdkClient) {
|
|
console.log('WebAssembly SDK not initialized - skipping secrets restoration');
|
|
return;
|
|
}
|
|
const sharedSecrets: Record<string, string> = await db.dumpStore('shared_secrets');
|
|
const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets');
|
|
const secretsStore = {
|
|
shared_secrets: sharedSecrets,
|
|
unconfirmed_secrets: Object.values(unconfirmedSecrets),
|
|
};
|
|
this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore));
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
decodeValue(value: number[]): any | null {
|
|
try {
|
|
return this.sdkClient.decode_value(value);
|
|
} catch (e) {
|
|
console.error(`Failed to decode value: ${e}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async decryptAttribute(
|
|
processId: string,
|
|
state: ProcessState,
|
|
attribute: string
|
|
): Promise<any | null> {
|
|
console.log(
|
|
`[decryptAttribute] Starting decryption for attribute: ${attribute}, processId: ${processId}`
|
|
);
|
|
let hash = state.pcd_commitment[attribute];
|
|
if (!hash) {
|
|
console.log(`[decryptAttribute] No hash found for attribute: ${attribute}`);
|
|
return null;
|
|
}
|
|
let key = state.keys[attribute];
|
|
console.log(
|
|
`[decryptAttribute] Initial key state for ${attribute}:`,
|
|
key ? 'present' : 'missing'
|
|
);
|
|
const pairingProcessId = this.getPairingProcessId();
|
|
|
|
// If key is missing, request an update and then retry
|
|
if (!key) {
|
|
const roles = state.roles;
|
|
let hasAccess = false;
|
|
// If we're not supposed to have access to this attribute, ignore
|
|
for (const role of Object.values(roles)) {
|
|
for (const rule of Object.values(role.validation_rules)) {
|
|
if (rule.fields.includes(attribute)) {
|
|
if (role.members.includes(pairingProcessId)) {
|
|
// We have access to this attribute
|
|
hasAccess = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasAccess) {
|
|
console.log(`[decryptAttribute] No access rights for attribute: ${attribute}`);
|
|
return null;
|
|
}
|
|
|
|
console.log(`[decryptAttribute] Requesting key update for attribute: ${attribute}`);
|
|
await this.checkConnections((await this.getProcess(processId))!);
|
|
// We should have the key, so we're going to ask other members for it
|
|
await this.requestDataFromPeers(processId, [state.state_id], [state.roles]);
|
|
|
|
const maxRetries = 1;
|
|
const retryDelay = 100; // delay in milliseconds
|
|
let retries = 0;
|
|
|
|
while ((!hash || !key) && retries < maxRetries) {
|
|
console.log(
|
|
`[decryptAttribute] Retry ${retries + 1}/${maxRetries} for attribute: ${attribute}`
|
|
);
|
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
// Re-read hash and key after waiting
|
|
hash = state.pcd_commitment[attribute];
|
|
key = state.keys[attribute];
|
|
retries++;
|
|
console.log(`[decryptAttribute] After retry ${retries}: hash=${!!hash}, key=${!!key}`);
|
|
}
|
|
}
|
|
|
|
if (hash && key) {
|
|
console.log(
|
|
`[decryptAttribute] Starting decryption process with hash: ${hash.substring(0, 8)}...`
|
|
);
|
|
const blob = await this.getBlobFromDb(hash);
|
|
if (blob) {
|
|
console.log(`[decryptAttribute] Blob retrieved successfully for ${attribute}`);
|
|
// Decrypt the data
|
|
const buf = await blob.arrayBuffer();
|
|
const cipher = new Uint8Array(buf);
|
|
|
|
const keyUIntArray = this.hexToUInt8Array(key);
|
|
|
|
try {
|
|
const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher);
|
|
if (clear) {
|
|
// deserialize the result to get the actual data
|
|
const decoded = this.sdkClient.decode_value(clear);
|
|
return decoded;
|
|
} else {
|
|
throw new Error('decrypt_data returned null');
|
|
}
|
|
} catch (e) {
|
|
console.error(`[decryptAttribute] Failed to decrypt data for ${attribute}:`, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
getNotifications(): any[] | null {
|
|
// return [
|
|
// {
|
|
// id: 1,
|
|
// title: 'Notif 1',
|
|
// description: 'A normal notification',
|
|
// sendToNotificationPage: false,
|
|
// path: '/notif1',
|
|
// },
|
|
// {
|
|
// id: 2,
|
|
// title: 'Notif 2',
|
|
// description: 'A normal notification',
|
|
// sendToNotificationPage: false,
|
|
// path: '/notif2',
|
|
// },
|
|
// {
|
|
// id: 3,
|
|
// title: 'Notif 3',
|
|
// description: 'A normal notification',
|
|
// sendToNotificationPage: false,
|
|
// path: '/notif3',
|
|
// },
|
|
// ];
|
|
return this.notifications;
|
|
}
|
|
|
|
setNotifications(notifications: any[]) {
|
|
this.notifications = notifications;
|
|
}
|
|
|
|
async importJSON(backup: BackUp): Promise<void> {
|
|
const device = backup.device;
|
|
|
|
// Reset current device
|
|
await this.resetDevice();
|
|
|
|
await this.saveDeviceInDatabase(device);
|
|
|
|
this.restoreDevice(device);
|
|
|
|
// TODO restore secrets and processes from file
|
|
const secretsStore = backup.secrets;
|
|
await this.restoreSecretsFromBackUp(secretsStore);
|
|
|
|
const processes = backup.processes;
|
|
await this.restoreProcessesFromBackUp(processes);
|
|
}
|
|
|
|
public async createBackUp(): Promise<BackUp | null> {
|
|
// Get the device from indexedDB
|
|
const device = await this.getDeviceFromDatabase();
|
|
if (!device) {
|
|
console.error('No device loaded');
|
|
return null;
|
|
}
|
|
|
|
// Get the processes
|
|
const processes = await this.getProcesses();
|
|
|
|
// Get the shared secrets
|
|
const secrets = await this.getAllSecrets();
|
|
|
|
// Create a backup object
|
|
const backUp = {
|
|
device: device,
|
|
secrets: secrets,
|
|
processes: processes,
|
|
};
|
|
|
|
return backUp;
|
|
}
|
|
|
|
// Device 1 wait Device 2
|
|
public device1: boolean = false;
|
|
public device2Ready: boolean = false;
|
|
|
|
public resetState() {
|
|
this.device1 = false;
|
|
this.device2Ready = false;
|
|
}
|
|
|
|
// Handle the handshake message
|
|
public async handleHandshakeMsg(url: string, parsedMsg: any) {
|
|
try {
|
|
// parsedMsg is already parsed by the validator, no need to JSON.parse again
|
|
const handshakeMsg: HandshakeMessage = parsedMsg;
|
|
console.log('🔍 DEBUG: Handshake message received:', {
|
|
url,
|
|
hasSpAddress: !!handshakeMsg.sp_address,
|
|
spAddress: handshakeMsg.sp_address,
|
|
spAddressType: typeof handshakeMsg.sp_address,
|
|
spAddressLength: handshakeMsg.sp_address?.length
|
|
});
|
|
|
|
if (handshakeMsg.sp_address) {
|
|
this.updateRelay(url, handshakeMsg.sp_address);
|
|
this.relayAddresses[url] = handshakeMsg.sp_address;
|
|
this.resolveRelayReady();
|
|
} else {
|
|
console.warn('⚠️ Handshake received but sp_address is empty or undefined');
|
|
}
|
|
|
|
console.log('handshakeMsg:', handshakeMsg);
|
|
this.currentBlockHeight = handshakeMsg.chain_tip;
|
|
console.log('this.currentBlockHeight:', this.currentBlockHeight);
|
|
this.updateDeviceBlockHeight();
|
|
if (this.membersList && Object.keys(this.membersList).length === 0) {
|
|
// We start from an empty list, just copy it over
|
|
this.membersList = handshakeMsg.peers_list;
|
|
} else {
|
|
// We are incrementing our list
|
|
for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) {
|
|
this.membersList[processId] = member as Member;
|
|
}
|
|
}
|
|
|
|
setTimeout(async () => {
|
|
const newProcesses: OutPointProcessMap = handshakeMsg.processes_list;
|
|
if (!newProcesses || Object.keys(newProcesses).length === 0) {
|
|
console.debug('Received empty processes list from', url);
|
|
return;
|
|
}
|
|
|
|
// Add a flag to prevent processing the same handshake multiple times
|
|
const handshakeKey = `${url}_${JSON.stringify(handshakeMsg.processes_list)}`;
|
|
if (this.processedHandshakes && this.processedHandshakes.has(handshakeKey)) {
|
|
console.debug('Handshake already processed for', url);
|
|
return;
|
|
}
|
|
if (!this.processedHandshakes) {
|
|
this.processedHandshakes = new Set();
|
|
}
|
|
this.processedHandshakes.add(handshakeKey);
|
|
|
|
if (this.processesCache && Object.keys(this.processesCache).length === 0) {
|
|
// We restored db but cache is empty, meaning we're starting from scratch
|
|
try {
|
|
await this.batchSaveProcessesToDb(newProcesses);
|
|
} catch (e) {
|
|
console.error('Failed to save processes to db:', e);
|
|
}
|
|
} else {
|
|
// We need to update our processes with what relay provides
|
|
const toSave: Record<string, Process> = {};
|
|
for (const [processId, process] of Object.entries(newProcesses)) {
|
|
const existing = await this.getProcess(processId);
|
|
if (existing) {
|
|
// Look for state id we don't know yet
|
|
const newStates: string[] = [];
|
|
const newRoles: Record<string, RoleDefinition>[] = [];
|
|
if (!Array.isArray(process.states)) {
|
|
console.warn('process.states is not an array:', process.states);
|
|
continue;
|
|
}
|
|
for (const state of process.states) {
|
|
if (!state?.state_id) {
|
|
continue;
|
|
} // shouldn't happen
|
|
if (state.state_id === EMPTY32BYTES) {
|
|
// We check that the tip is the same we have, if not we update
|
|
const existingTip = existing.states[existing.states.length - 1].commited_in;
|
|
if (existingTip !== state.commited_in) {
|
|
console.log('Found new tip for process', processId);
|
|
existing.states.pop(); // We discard the last state
|
|
existing.states.push(state);
|
|
// We know that's the last state, so we just trigger the update
|
|
toSave[processId] = existing;
|
|
}
|
|
} else if (!this.lookForStateId(existing, state.state_id)) {
|
|
// We don't want to overwrite what we already have for existing processes
|
|
// We may end up overwriting the keys for example
|
|
// So the process we're going to save needs to merge new states with what we already have
|
|
const existingLastState = existing.states.pop();
|
|
if (!existingLastState) {
|
|
// This should never happen
|
|
console.error('Failed to get last state for process', processId);
|
|
break;
|
|
}
|
|
existing.states.push(state);
|
|
existing.states.push(existingLastState);
|
|
toSave[processId] = existing; // We mark it for update
|
|
if (this.rolesContainsUs(state.roles)) {
|
|
newStates.push(state.state_id);
|
|
newRoles.push(state.roles);
|
|
}
|
|
} else {
|
|
// We already have the state, but we check if we have the keys
|
|
const existingState = this.getStateFromId(existing, state.state_id);
|
|
if (existingState!.keys && Object.keys(existingState!.keys).length != 0) {
|
|
// We have some keys, so we just assume everything ok and move on for now
|
|
continue;
|
|
} else {
|
|
// We verify we are part of the roles
|
|
const roles = state.roles;
|
|
if (this.rolesContainsUs(roles)) {
|
|
// We don't have keys, but we are part of the roles, so we need to request the keys
|
|
// that may also be because we are part of a role that don't have any fields
|
|
// It's possible but let's request for nothing anyway
|
|
newStates.push(state.state_id);
|
|
newRoles.push(roles);
|
|
} else {
|
|
// We are simply not involved, move on
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newStates.length != 0) {
|
|
await this.checkConnections(existing);
|
|
await this.requestDataFromPeers(processId, newStates, newRoles);
|
|
}
|
|
// Otherwise we're probably just in the initial loading at page initialization
|
|
} else {
|
|
// We add it to db
|
|
toSave[processId] = process;
|
|
}
|
|
}
|
|
|
|
if (toSave && Object.keys(toSave).length > 0) {
|
|
console.log('batch saving processes to db', toSave);
|
|
await this.batchSaveProcessesToDb(toSave);
|
|
}
|
|
}
|
|
}, 500);
|
|
} catch (e) {
|
|
console.error('Failed to parse init message:', e);
|
|
}
|
|
}
|
|
|
|
private lookForStateId(process: Process, stateId: string): boolean {
|
|
for (const state of process.states) {
|
|
if (state.state_id === stateId) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Waits for at least one handshake message to be received from any connected relay.
|
|
* This ensures that the relay addresses are fully populated and the member list is updated.
|
|
* @returns A promise that resolves when at least one handshake message is received.
|
|
*/
|
|
private async waitForHandshakeMessage(timeoutMs: number = 3000): Promise<void> {
|
|
const startTime = Date.now();
|
|
const pollInterval = 100; // Check every 100ms
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
const checkForHandshake = () => {
|
|
// Check if we have any members or any relays (indicating handshake was received)
|
|
if (
|
|
Object.keys(this.membersList).length > 0 ||
|
|
Object.keys(this.relayAddresses).length > 0
|
|
) {
|
|
console.log('Handshake message received (members or relays present)');
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
// Check timeout
|
|
if (Date.now() - startTime >= timeoutMs) {
|
|
reject(new Error(`No handshake message received after ${timeoutMs}ms timeout`));
|
|
return;
|
|
}
|
|
|
|
// Continue polling
|
|
setTimeout(checkForHandshake, pollInterval);
|
|
};
|
|
|
|
checkForHandshake();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retourne la liste de tous les membres ordonnés par leur process id
|
|
* @returns Un tableau contenant tous les membres
|
|
*/
|
|
public getAllMembersSorted(): Record<string, Member> {
|
|
return Object.fromEntries(
|
|
Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
|
|
);
|
|
}
|
|
|
|
public getAllMembers(): Record<string, Member> {
|
|
return this.membersList;
|
|
}
|
|
|
|
public getAddressesForMemberId(memberId: string): string[] | null {
|
|
try {
|
|
return this.membersList[memberId].sp_addresses;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public compareMembers(memberA: string[], memberB: string[]): boolean {
|
|
if (!memberA || !memberB) {
|
|
return false;
|
|
}
|
|
if (memberA.length !== memberB.length) {
|
|
return false;
|
|
}
|
|
|
|
const res =
|
|
memberA.every(item => memberB.includes(item)) &&
|
|
memberB.every(item => memberA.includes(item));
|
|
|
|
return res;
|
|
}
|
|
|
|
public async handleCommitError(response: string) {
|
|
const content = JSON.parse(response);
|
|
const error = content.error;
|
|
const errorMsg = error['GenericError'];
|
|
const dontRetry = [
|
|
'State is identical to the previous state',
|
|
'Not enough valid proofs',
|
|
'Not enough members to validate',
|
|
];
|
|
if (dontRetry.includes(errorMsg)) {
|
|
return;
|
|
}
|
|
// Wait and retry
|
|
setTimeout(async () => {
|
|
this.sendCommitMessage(JSON.stringify(content));
|
|
}, 1000);
|
|
}
|
|
|
|
public getRoles(process: Process): Record<string, RoleDefinition> | null {
|
|
const lastCommitedState = this.getLastCommitedState(process);
|
|
if (
|
|
lastCommitedState &&
|
|
lastCommitedState.roles &&
|
|
Object.keys(lastCommitedState.roles).length != 0
|
|
) {
|
|
return lastCommitedState!.roles;
|
|
} else if (process.states.length === 2) {
|
|
const firstState = process.states[0];
|
|
if (firstState && firstState.roles && Object.keys(firstState.roles).length != 0) {
|
|
return firstState!.roles;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public getPublicData(process: Process): Record<string, any> | null {
|
|
const lastCommitedState = this.getLastCommitedState(process);
|
|
if (
|
|
lastCommitedState &&
|
|
lastCommitedState.public_data &&
|
|
Object.keys(lastCommitedState.public_data).length != 0
|
|
) {
|
|
return lastCommitedState!.public_data;
|
|
} else if (process.states.length === 2) {
|
|
const firstState = process.states[0];
|
|
if (firstState && firstState.public_data && Object.keys(firstState.public_data).length != 0) {
|
|
return firstState!.public_data;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public getProcessName(process: Process): string | null {
|
|
const lastCommitedState = this.getLastCommitedState(process);
|
|
if (lastCommitedState && lastCommitedState.public_data) {
|
|
const processName = lastCommitedState!.public_data['processName'];
|
|
if (processName) {
|
|
return this.decodeValue(processName);
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async getMyProcesses(): Promise<string[] | null> {
|
|
// If we're not paired yet, just skip it
|
|
let pairingProcessId = null;
|
|
try {
|
|
pairingProcessId = this.getPairingProcessId();
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
if (!pairingProcessId) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const processes = await this.getProcesses();
|
|
|
|
const newMyProcesses = new Set<string>(this.myProcesses || []);
|
|
// MyProcesses automatically contains pairing process
|
|
newMyProcesses.add(pairingProcessId);
|
|
for (const [processId, process] of Object.entries(processes)) {
|
|
// We use myProcesses attribute to not reevaluate all processes everytime
|
|
if (newMyProcesses.has(processId)) {
|
|
continue;
|
|
}
|
|
try {
|
|
const roles = this.getRoles(process);
|
|
|
|
if (roles && this.rolesContainsUs(roles)) {
|
|
newMyProcesses.add(processId);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
this.myProcesses = newMyProcesses; // atomic update
|
|
return Array.from(this.myProcesses);
|
|
} catch (e) {
|
|
console.error('Failed to get processes:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async requestDataFromPeers(
|
|
processId: string,
|
|
stateIds: string[],
|
|
roles: Record<string, RoleDefinition>[]
|
|
) {
|
|
console.log('Requesting data from peers');
|
|
const membersList = Object.values(this.getAllMembers()).map(member => ({
|
|
sp_addresses: member.sp_addresses
|
|
}));
|
|
try {
|
|
// Convert objects to strings for WASM compatibility
|
|
const rolesString = JSON.stringify(roles);
|
|
const membersString = JSON.stringify(membersList);
|
|
const stateIdsString = JSON.stringify(stateIds);
|
|
|
|
const res = this.sdkClient.request_data(processId, stateIdsString, rolesString, membersString);
|
|
await this.handleApiReturn(res);
|
|
} catch (e) {
|
|
console.error('Error requesting data from peers:', e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public hexToBlob(hexString: string): Blob {
|
|
const uint8Array = this.hexToUInt8Array(hexString);
|
|
|
|
return new Blob([new Uint8Array(uint8Array)], { type: 'application/octet-stream' });
|
|
}
|
|
|
|
public hexToUInt8Array(hexString: string): Uint8Array {
|
|
if (hexString.length % 2 !== 0) {
|
|
throw new Error('Invalid hex string: length must be even');
|
|
}
|
|
const uint8Array = new Uint8Array(hexString.length / 2);
|
|
for (let i = 0; i < hexString.length; i += 2) {
|
|
uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16);
|
|
}
|
|
|
|
return uint8Array;
|
|
}
|
|
|
|
public async blobToHex(blob: Blob): Promise<string> {
|
|
const buffer = await blob.arrayBuffer();
|
|
const bytes = new Uint8Array(buffer);
|
|
return Array.from(bytes)
|
|
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
}
|
|
|
|
public getHashForFile(
|
|
commitedIn: string,
|
|
label: string,
|
|
fileBlob: { type: string; data: Uint8Array }
|
|
): string {
|
|
return this.sdkClient.hash_value(fileBlob, commitedIn, label);
|
|
}
|
|
|
|
public getMerkleProofForFile(
|
|
processState: ProcessState,
|
|
attributeName: string
|
|
): MerkleProofResult {
|
|
return this.sdkClient.get_merkle_proof(processState, attributeName);
|
|
}
|
|
|
|
public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean {
|
|
try {
|
|
return this.sdkClient.validate_merkle_proof(proof, hash);
|
|
} catch (e) {
|
|
throw new Error(`Failed to validate merkle proof: ${e}`);
|
|
}
|
|
}
|
|
|
|
public getLastCommitedState(process: Process): ProcessState | null {
|
|
if (process.states.length === 0) {return null;}
|
|
const processTip = process.states[process.states.length - 1].commited_in;
|
|
const lastCommitedState = process.states.findLast(state => state.commited_in !== processTip);
|
|
if (lastCommitedState) {
|
|
return lastCommitedState;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public getLastCommitedStateIndex(process: Process): number | null {
|
|
if (process.states.length === 0) {return null;}
|
|
const processTip = process.states[process.states.length - 1].commited_in;
|
|
for (let i = process.states.length - 1; i >= 0; i--) {
|
|
if (process.states[i].commited_in !== processTip) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public getUncommitedStates(process: Process): ProcessState[] {
|
|
if (process.states.length === 0) {return [];}
|
|
const processTip = process.states[process.states.length - 1].commited_in;
|
|
const res = process.states.filter(state => state.commited_in === processTip);
|
|
return res.filter(state => state.state_id !== EMPTY32BYTES);
|
|
}
|
|
|
|
public getStateFromId(process: Process, stateId: string): ProcessState | null {
|
|
if (process.states.length === 0) {return null;}
|
|
const state = process.states.find(state => state.state_id === stateId);
|
|
if (state) {
|
|
return state;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public getNextStateAfterId(process: Process, stateId: string): ProcessState | null {
|
|
if (process.states.length === 0) {return null;}
|
|
|
|
const index = process.states.findIndex(state => state.state_id === stateId);
|
|
|
|
if (index !== -1 && index < process.states.length - 1) {
|
|
return process.states[index + 1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public isPairingProcess(roles: Record<string, RoleDefinition>): boolean {
|
|
if (Object.keys(roles).length != 1) {
|
|
return false;
|
|
}
|
|
const pairingRole = roles['pairing'];
|
|
if (pairingRole) {
|
|
// For now that's enough, we should probably test more things
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async updateMemberPublicName(process: Process, newName: string): Promise<ApiReturn> {
|
|
const publicData = {
|
|
memberPublicName: newName,
|
|
};
|
|
|
|
return await this.updateProcess(process, {}, publicData, null);
|
|
}
|
|
}
|