@eztra.services/engine 1.0.0-dev.20260202084348 → 1.0.0-dev.20260202093252
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1373 -8
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
package/dist/index.js
CHANGED
|
@@ -240,15 +240,1380 @@ var ComposableController = class {
|
|
|
240
240
|
}
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
+
// ../controller/dist/index.js
|
|
244
|
+
var BaseController = class {
|
|
245
|
+
/**
|
|
246
|
+
* Controller version for state migrations
|
|
247
|
+
* Increment this when making breaking changes to state structure
|
|
248
|
+
*/
|
|
249
|
+
static VERSION = 1;
|
|
250
|
+
/**
|
|
251
|
+
* Internal state storage
|
|
252
|
+
*/
|
|
253
|
+
internalState;
|
|
254
|
+
/**
|
|
255
|
+
* Controller messenger for inter-controller communication
|
|
256
|
+
*/
|
|
257
|
+
messenger;
|
|
258
|
+
/**
|
|
259
|
+
* State metadata (persistence, access control, validation)
|
|
260
|
+
* @deprecated Use StatePropertyMetadata instead
|
|
261
|
+
*/
|
|
262
|
+
metadata;
|
|
263
|
+
/**
|
|
264
|
+
* Enhanced metadata with validators and migrators
|
|
265
|
+
* Override this in subclasses to define persistence rules
|
|
266
|
+
*/
|
|
267
|
+
propertyMetadata = {};
|
|
268
|
+
/**
|
|
269
|
+
* Optional persistence service for state save/restore
|
|
270
|
+
*/
|
|
271
|
+
persistenceService;
|
|
272
|
+
/**
|
|
273
|
+
* Create a new controller instance
|
|
274
|
+
*
|
|
275
|
+
* @param config - Controller configuration
|
|
276
|
+
* @param defaultState - Default state (provided by subclass)
|
|
277
|
+
*/
|
|
278
|
+
constructor(config, defaultState) {
|
|
279
|
+
this.messenger = config.messenger;
|
|
280
|
+
this.metadata = config.metadata || {};
|
|
281
|
+
this.persistenceService = config.persistenceService;
|
|
282
|
+
this.internalState = { ...defaultState, ...config.state };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Get current state (returns a copy to prevent direct mutation)
|
|
286
|
+
*/
|
|
287
|
+
get state() {
|
|
288
|
+
return { ...this.internalState };
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Update controller state
|
|
292
|
+
*
|
|
293
|
+
* @param stateUpdate - Partial state update or updater function
|
|
294
|
+
*/
|
|
295
|
+
update(stateUpdate) {
|
|
296
|
+
const prevState = this.internalState;
|
|
297
|
+
if (typeof stateUpdate === "function") {
|
|
298
|
+
const draft = { ...this.internalState };
|
|
299
|
+
const result = stateUpdate(draft);
|
|
300
|
+
this.internalState = result ? { ...draft, ...result } : draft;
|
|
301
|
+
} else {
|
|
302
|
+
this.internalState = { ...this.internalState, ...stateUpdate };
|
|
303
|
+
}
|
|
304
|
+
const event = {
|
|
305
|
+
prevState,
|
|
306
|
+
newState: this.internalState
|
|
307
|
+
};
|
|
308
|
+
this.messenger.publish(`${this.name}:stateChange`, event);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Destroy controller (cleanup)
|
|
312
|
+
* Override this to clean up subscriptions, timers, etc.
|
|
313
|
+
*/
|
|
314
|
+
destroy() {
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get metadata for a state property
|
|
318
|
+
*/
|
|
319
|
+
getMetadata(key) {
|
|
320
|
+
return this.metadata[key];
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Check if a state property should be persisted
|
|
324
|
+
*/
|
|
325
|
+
shouldPersist(key) {
|
|
326
|
+
return this.getMetadata(key)?.persist ?? false;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get persistable state (only properties with persist: true)
|
|
330
|
+
*/
|
|
331
|
+
getPersistableState() {
|
|
332
|
+
const persistable = {};
|
|
333
|
+
for (const key in this.internalState) {
|
|
334
|
+
if (this.shouldPersist(key)) {
|
|
335
|
+
persistable[key] = this.internalState[key];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return persistable;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Get anonymous state (only properties with anonymous: true)
|
|
342
|
+
* Useful for public data that can be accessed without authentication
|
|
343
|
+
*/
|
|
344
|
+
getAnonymousState() {
|
|
345
|
+
const anonymous = {};
|
|
346
|
+
const metadata = this.propertyMetadata || this.metadata;
|
|
347
|
+
for (const key in this.internalState) {
|
|
348
|
+
const meta = metadata[key];
|
|
349
|
+
if (meta?.anonymous === true) {
|
|
350
|
+
anonymous[key] = this.internalState[key];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return anonymous;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Get persistent state (only persisted properties)
|
|
357
|
+
* Uses propertyMetadata if available, falls back to legacy metadata
|
|
358
|
+
*/
|
|
359
|
+
getPersistentState() {
|
|
360
|
+
const persistent = {};
|
|
361
|
+
const metadata = this.propertyMetadata || this.metadata;
|
|
362
|
+
for (const key in this.internalState) {
|
|
363
|
+
const meta = metadata[key];
|
|
364
|
+
if (meta?.persist === true) {
|
|
365
|
+
persistent[key] = this.internalState[key];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return persistent;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Validate state using metadata validators
|
|
372
|
+
* Override this method to add custom validation logic
|
|
373
|
+
*
|
|
374
|
+
* @param state - State to validate
|
|
375
|
+
* @returns Validated state or throws error
|
|
376
|
+
*/
|
|
377
|
+
async validateState(state) {
|
|
378
|
+
const validatedState = { ...this.internalState, ...state };
|
|
379
|
+
const metadata = this.propertyMetadata;
|
|
380
|
+
for (const key in metadata) {
|
|
381
|
+
const meta = metadata[key];
|
|
382
|
+
if (meta?.validator && key in validatedState) {
|
|
383
|
+
const value = validatedState[key];
|
|
384
|
+
const isValid = meta.validator(value);
|
|
385
|
+
if (!isValid) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Validation failed for property "${String(key)}" in controller "${this.name}"`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (meta?.required && !(key in validatedState)) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`Required property "${String(key)}" is missing in controller "${this.name}"`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return validatedState;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Migrate state from an old version to the current version
|
|
401
|
+
* Override this method to handle version-specific migrations
|
|
402
|
+
*
|
|
403
|
+
* @param oldState - State from previous version
|
|
404
|
+
* @param fromVersion - Version number of old state
|
|
405
|
+
* @returns Migrated state
|
|
406
|
+
*/
|
|
407
|
+
async migrateState(oldState, fromVersion) {
|
|
408
|
+
let migratedState = { ...oldState };
|
|
409
|
+
const metadata = this.propertyMetadata;
|
|
410
|
+
for (const key in metadata) {
|
|
411
|
+
const meta = metadata[key];
|
|
412
|
+
if (meta?.migrator && key in migratedState) {
|
|
413
|
+
migratedState[key] = meta.migrator(migratedState[key], fromVersion);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return this.validateState(migratedState);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Restore state from persisted data
|
|
420
|
+
* Handles validation and migration automatically
|
|
421
|
+
*
|
|
422
|
+
* @param persistedState - Persisted state data
|
|
423
|
+
* @param version - Version of persisted state
|
|
424
|
+
* @returns Restored and validated state
|
|
425
|
+
*/
|
|
426
|
+
async restoreState(persistedState, version) {
|
|
427
|
+
const currentVersion = this.constructor.VERSION;
|
|
428
|
+
let stateToRestore = persistedState;
|
|
429
|
+
if (version !== void 0 && version < currentVersion) {
|
|
430
|
+
stateToRestore = await this.migrateState(persistedState, version);
|
|
431
|
+
}
|
|
432
|
+
const validatedState = await this.validateState(stateToRestore);
|
|
433
|
+
const prevState = this.internalState;
|
|
434
|
+
this.internalState = validatedState;
|
|
435
|
+
this.messenger.publish(`${this.name}:stateChange`, {
|
|
436
|
+
prevState,
|
|
437
|
+
newState: validatedState
|
|
438
|
+
});
|
|
439
|
+
return validatedState;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Check if state should be migrated
|
|
443
|
+
*/
|
|
444
|
+
needsMigration(persistedVersion) {
|
|
445
|
+
const currentVersion = this.constructor.VERSION;
|
|
446
|
+
return persistedVersion !== void 0 && persistedVersion < currentVersion;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get controller version
|
|
450
|
+
*/
|
|
451
|
+
getVersion() {
|
|
452
|
+
return this.constructor.VERSION;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Persist current state to storage (if persistence service is available)
|
|
456
|
+
*
|
|
457
|
+
* @returns True if persisted successfully, false otherwise
|
|
458
|
+
*/
|
|
459
|
+
async persist() {
|
|
460
|
+
if (!this.persistenceService) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const metadata = this.propertyMetadata || this.metadata;
|
|
465
|
+
const version = this.getVersion();
|
|
466
|
+
await this.persistenceService.persistController(
|
|
467
|
+
this.name,
|
|
468
|
+
this.internalState,
|
|
469
|
+
metadata,
|
|
470
|
+
version
|
|
471
|
+
);
|
|
472
|
+
return true;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.error(`[${this.name}] Failed to persist state:`, error);
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Load persisted state from storage (if persistence service is available)
|
|
480
|
+
*
|
|
481
|
+
* @returns True if loaded successfully, false otherwise
|
|
482
|
+
*/
|
|
483
|
+
async loadPersistedState() {
|
|
484
|
+
if (!this.persistenceService) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
const metadata = this.propertyMetadata || this.metadata;
|
|
489
|
+
const currentVersion = this.getVersion();
|
|
490
|
+
const persistedState = await this.persistenceService.restoreController(
|
|
491
|
+
this.name,
|
|
492
|
+
metadata,
|
|
493
|
+
currentVersion
|
|
494
|
+
);
|
|
495
|
+
if (persistedState) {
|
|
496
|
+
const persistedVersion = this.persistenceService.getPersistedVersion?.(this.name) ?? currentVersion;
|
|
497
|
+
await this.restoreState(persistedState, persistedVersion);
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
501
|
+
} catch (error) {
|
|
502
|
+
console.error(`[${this.name}] Failed to load persisted state:`, error);
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
var ControllerMessenger = class {
|
|
508
|
+
/**
|
|
509
|
+
* Registered action handlers
|
|
510
|
+
* Map: action name -> handler function
|
|
511
|
+
*/
|
|
512
|
+
actionHandlers = /* @__PURE__ */ new Map();
|
|
513
|
+
/**
|
|
514
|
+
* Event subscriptions
|
|
515
|
+
* Map: event name -> Set of listener functions
|
|
516
|
+
*/
|
|
517
|
+
eventSubscriptions = /* @__PURE__ */ new Map();
|
|
518
|
+
/**
|
|
519
|
+
* Register an action handler
|
|
520
|
+
*
|
|
521
|
+
* @param action - Action name (must be unique)
|
|
522
|
+
* @param handler - Handler function
|
|
523
|
+
* @throws Error if action is already registered
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```typescript
|
|
527
|
+
* messenger.registerActionHandler(
|
|
528
|
+
* 'WalletController:createWallet',
|
|
529
|
+
* async (mnemonic: string) => {
|
|
530
|
+
* // Create wallet logic
|
|
531
|
+
* return { id: '123', address: '0xabc...' };
|
|
532
|
+
* }
|
|
533
|
+
* );
|
|
534
|
+
* ```
|
|
535
|
+
*/
|
|
536
|
+
registerActionHandler(action, handler) {
|
|
537
|
+
if (this.actionHandlers.has(action)) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`Action handler for "${String(action)}" is already registered`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
this.actionHandlers.set(action, handler);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Unregister an action handler
|
|
546
|
+
*
|
|
547
|
+
* @param action - Action name to unregister
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* ```typescript
|
|
551
|
+
* messenger.unregisterActionHandler('WalletController:createWallet');
|
|
552
|
+
* ```
|
|
553
|
+
*/
|
|
554
|
+
unregisterActionHandler(action) {
|
|
555
|
+
this.actionHandlers.delete(action);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Call an action (RPC-style)
|
|
559
|
+
*
|
|
560
|
+
* @param action - Action name to call
|
|
561
|
+
* @param params - Action parameters
|
|
562
|
+
* @returns Action result (typed based on action definition)
|
|
563
|
+
* @throws Error if action is not registered
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* ```typescript
|
|
567
|
+
* // Synchronous action
|
|
568
|
+
* const wallet = messenger.call('WalletController:getActiveWallet');
|
|
569
|
+
*
|
|
570
|
+
* // Asynchronous action
|
|
571
|
+
* const balance = await messenger.call(
|
|
572
|
+
* 'WalletController:getBalance',
|
|
573
|
+
* '0x123...'
|
|
574
|
+
* );
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
call(action, ...params) {
|
|
578
|
+
const handler = this.actionHandlers.get(action);
|
|
579
|
+
if (!handler) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
`Action handler for "${String(action)}" is not registered`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
return handler(...params);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.error(`Error executing action "${String(action)}":`, error);
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Subscribe to an event
|
|
593
|
+
*
|
|
594
|
+
* @param event - Event name to subscribe to
|
|
595
|
+
* @param listener - Listener function
|
|
596
|
+
* @returns Unsubscribe function
|
|
597
|
+
*
|
|
598
|
+
* @example
|
|
599
|
+
* ```typescript
|
|
600
|
+
* const unsubscribe = messenger.subscribe(
|
|
601
|
+
* 'WalletController:balanceUpdated',
|
|
602
|
+
* (payload) => {
|
|
603
|
+
* console.log(`New balance: ${payload.balance}`);
|
|
604
|
+
* }
|
|
605
|
+
* );
|
|
606
|
+
*
|
|
607
|
+
* // Later, unsubscribe
|
|
608
|
+
* unsubscribe();
|
|
609
|
+
* ```
|
|
610
|
+
*/
|
|
611
|
+
subscribe(event, listener) {
|
|
612
|
+
if (!this.eventSubscriptions.has(event)) {
|
|
613
|
+
this.eventSubscriptions.set(event, /* @__PURE__ */ new Set());
|
|
614
|
+
}
|
|
615
|
+
const listeners = this.eventSubscriptions.get(event);
|
|
616
|
+
listeners.add(listener);
|
|
617
|
+
return () => {
|
|
618
|
+
this.unsubscribe(event, listener);
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Unsubscribe from an event
|
|
623
|
+
*
|
|
624
|
+
* @param event - Event name
|
|
625
|
+
* @param listener - Listener function to remove
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* ```typescript
|
|
629
|
+
* const listener = (payload) => console.log(payload);
|
|
630
|
+
* messenger.subscribe('WalletController:balanceUpdated', listener);
|
|
631
|
+
*
|
|
632
|
+
* // Later...
|
|
633
|
+
* messenger.unsubscribe('WalletController:balanceUpdated', listener);
|
|
634
|
+
* ```
|
|
635
|
+
*/
|
|
636
|
+
unsubscribe(event, listener) {
|
|
637
|
+
const listeners = this.eventSubscriptions.get(event);
|
|
638
|
+
if (listeners) {
|
|
639
|
+
listeners.delete(listener);
|
|
640
|
+
if (listeners.size === 0) {
|
|
641
|
+
this.eventSubscriptions.delete(event);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Publish an event (pub/sub)
|
|
647
|
+
*
|
|
648
|
+
* All subscribed listeners are called asynchronously.
|
|
649
|
+
* Errors in listeners are caught and logged to prevent one listener from affecting others.
|
|
650
|
+
*
|
|
651
|
+
* @param event - Event name to publish
|
|
652
|
+
* @param payload - Event payload
|
|
653
|
+
*
|
|
654
|
+
* @example
|
|
655
|
+
* ```typescript
|
|
656
|
+
* messenger.publish('WalletController:balanceUpdated', {
|
|
657
|
+
* address: '0x123...',
|
|
658
|
+
* balance: '1000000000000000000'
|
|
659
|
+
* });
|
|
660
|
+
* ```
|
|
661
|
+
*/
|
|
662
|
+
publish(event, payload) {
|
|
663
|
+
const listeners = this.eventSubscriptions.get(event);
|
|
664
|
+
if (listeners && listeners.size > 0) {
|
|
665
|
+
Promise.resolve().then(() => {
|
|
666
|
+
listeners.forEach((listener) => {
|
|
667
|
+
try {
|
|
668
|
+
listener(payload);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
console.error(
|
|
671
|
+
`Error in event listener for "${String(event)}":`,
|
|
672
|
+
error
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Clear all subscriptions for an event
|
|
681
|
+
*
|
|
682
|
+
* @param event - Event name
|
|
683
|
+
*
|
|
684
|
+
* @example
|
|
685
|
+
* ```typescript
|
|
686
|
+
* messenger.clearEventSubscriptions('WalletController:balanceUpdated');
|
|
687
|
+
* ```
|
|
688
|
+
*/
|
|
689
|
+
clearEventSubscriptions(event) {
|
|
690
|
+
this.eventSubscriptions.delete(event);
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Get a restricted messenger for a controller
|
|
694
|
+
*
|
|
695
|
+
* Restricted messengers enforce the principle of least privilege:
|
|
696
|
+
* - Controllers can only call actions they're explicitly allowed to call
|
|
697
|
+
* - Controllers can only subscribe to events they're explicitly allowed to subscribe to
|
|
698
|
+
* - Controllers can only publish events in their own namespace
|
|
699
|
+
* - Controllers can only register actions in their own namespace
|
|
700
|
+
*
|
|
701
|
+
* @param options - Restriction configuration
|
|
702
|
+
* @returns Restricted messenger instance
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```typescript
|
|
706
|
+
* const walletMessenger = messenger.getRestricted({
|
|
707
|
+
* name: 'WalletController',
|
|
708
|
+
* allowedActions: ['WalletController:createWallet', 'WalletController:getBalance'],
|
|
709
|
+
* allowedEvents: ['WalletController:balanceUpdated'],
|
|
710
|
+
* externalActions: ['NetworkController:getActiveNetwork'],
|
|
711
|
+
* externalEvents: ['NetworkController:networkChanged']
|
|
712
|
+
* });
|
|
713
|
+
*
|
|
714
|
+
* // WalletController can now:
|
|
715
|
+
* // - Register WalletController actions
|
|
716
|
+
* // - Call WalletController and NetworkController actions
|
|
717
|
+
* // - Publish WalletController events
|
|
718
|
+
* // - Subscribe to WalletController and NetworkController events
|
|
719
|
+
* ```
|
|
720
|
+
*/
|
|
721
|
+
getRestricted(options) {
|
|
722
|
+
const {
|
|
723
|
+
name,
|
|
724
|
+
allowedActions = [],
|
|
725
|
+
allowedEvents = [],
|
|
726
|
+
externalActions = [],
|
|
727
|
+
externalEvents = []
|
|
728
|
+
} = options;
|
|
729
|
+
const allAllowedActions = /* @__PURE__ */ new Set([
|
|
730
|
+
...allowedActions,
|
|
731
|
+
...externalActions
|
|
732
|
+
]);
|
|
733
|
+
const allAllowedEvents = /* @__PURE__ */ new Set([
|
|
734
|
+
...allowedEvents,
|
|
735
|
+
...externalEvents
|
|
736
|
+
]);
|
|
737
|
+
return {
|
|
738
|
+
call: ((action, ...params) => {
|
|
739
|
+
if (!allAllowedActions.has(action)) {
|
|
740
|
+
throw new Error(
|
|
741
|
+
`Controller "${name}" is not allowed to call action "${action}"`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
return this.call(action, ...params);
|
|
745
|
+
}),
|
|
746
|
+
registerActionHandler: ((action, handler) => {
|
|
747
|
+
const actionNamespace = String(action).split(":")[0];
|
|
748
|
+
if (actionNamespace !== name) {
|
|
749
|
+
throw new Error(
|
|
750
|
+
`Controller "${name}" can only register actions in its own namespace (${name}:*), attempted to register "${action}"`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
if (!allowedActions.includes(action)) {
|
|
754
|
+
throw new Error(
|
|
755
|
+
`Controller "${name}" is not allowed to register action "${action}"`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
return this.registerActionHandler(action, handler);
|
|
759
|
+
}),
|
|
760
|
+
unregisterActionHandler: ((action) => {
|
|
761
|
+
const actionNamespace = String(action).split(":")[0];
|
|
762
|
+
if (actionNamespace !== name) {
|
|
763
|
+
throw new Error(
|
|
764
|
+
`Controller "${name}" can only unregister actions in its own namespace`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return this.unregisterActionHandler(action);
|
|
768
|
+
}),
|
|
769
|
+
publish: ((event, payload) => {
|
|
770
|
+
const eventNamespace = String(event).split(":")[0];
|
|
771
|
+
if (eventNamespace !== name) {
|
|
772
|
+
throw new Error(
|
|
773
|
+
`Controller "${name}" can only publish events in its own namespace (${name}:*), attempted to publish "${event}"`
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
if (!allowedEvents.includes(event)) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
`Controller "${name}" is not allowed to publish event "${event}"`
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
return this.publish(event, payload);
|
|
782
|
+
}),
|
|
783
|
+
subscribe: ((event, listener) => {
|
|
784
|
+
if (!allAllowedEvents.has(event)) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`Controller "${name}" is not allowed to subscribe to event "${event}"`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return this.subscribe(event, listener);
|
|
790
|
+
}),
|
|
791
|
+
unsubscribe: ((event, listener) => {
|
|
792
|
+
if (!allAllowedEvents.has(event)) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`Controller "${name}" is not allowed to unsubscribe from event "${event}"`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
return this.unsubscribe(event, listener);
|
|
798
|
+
}),
|
|
799
|
+
clearEventSubscriptions: ((event) => {
|
|
800
|
+
if (!allAllowedEvents.has(event)) {
|
|
801
|
+
throw new Error(
|
|
802
|
+
`Controller "${name}" is not allowed to clear event subscriptions for "${event}"`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
return this.clearEventSubscriptions(event);
|
|
806
|
+
})
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Get all registered action names
|
|
811
|
+
* Useful for debugging and inspection
|
|
812
|
+
*/
|
|
813
|
+
getRegisteredActions() {
|
|
814
|
+
return Array.from(this.actionHandlers.keys()).map(String);
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Get all events that have active subscriptions
|
|
818
|
+
* Useful for debugging and inspection
|
|
819
|
+
*/
|
|
820
|
+
getActiveEvents() {
|
|
821
|
+
return Array.from(this.eventSubscriptions.keys()).map(String);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Get subscriber count for an event
|
|
825
|
+
* Useful for debugging
|
|
826
|
+
*/
|
|
827
|
+
getSubscriberCount(event) {
|
|
828
|
+
return this.eventSubscriptions.get(event)?.size ?? 0;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Clear all action handlers and event subscriptions
|
|
832
|
+
* Useful for cleanup and testing
|
|
833
|
+
*/
|
|
834
|
+
clear() {
|
|
835
|
+
this.actionHandlers.clear();
|
|
836
|
+
this.eventSubscriptions.clear();
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// ../infrastructure/storage/dist/index.js
|
|
841
|
+
var StatePersistenceService = class {
|
|
842
|
+
constructor(storage) {
|
|
843
|
+
this.storage = storage;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Persist controller state to storage
|
|
847
|
+
*
|
|
848
|
+
* Only properties with `persist: true` in metadata are saved.
|
|
849
|
+
*
|
|
850
|
+
* @param controllerName - Unique identifier for the controller
|
|
851
|
+
* @param state - Full controller state
|
|
852
|
+
* @param metadata - State property metadata (defines what to persist)
|
|
853
|
+
* @param version - State version number
|
|
854
|
+
*/
|
|
855
|
+
async persistController(controllerName, state, metadata, version) {
|
|
856
|
+
try {
|
|
857
|
+
const persistableState = this.extractPersistableState(state, metadata);
|
|
858
|
+
const wrapper = {
|
|
859
|
+
data: persistableState,
|
|
860
|
+
version,
|
|
861
|
+
timestamp: Date.now()
|
|
862
|
+
};
|
|
863
|
+
const serialized = JSON.stringify(wrapper);
|
|
864
|
+
const key = this.getStorageKey(controllerName);
|
|
865
|
+
this.storage.set(key, serialized);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
console.error(
|
|
868
|
+
`[StatePersistenceService] Failed to persist state for ${controllerName}:`,
|
|
869
|
+
error
|
|
870
|
+
);
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Restore controller state from storage
|
|
876
|
+
*
|
|
877
|
+
* Handles version mismatches and runs migrations if needed.
|
|
878
|
+
*
|
|
879
|
+
* @param controllerName - Unique identifier for the controller
|
|
880
|
+
* @param metadata - State property metadata (for migration)
|
|
881
|
+
* @param currentVersion - Current version of the controller
|
|
882
|
+
* @returns Restored state or null if not found
|
|
883
|
+
*/
|
|
884
|
+
async restoreController(controllerName, metadata, currentVersion) {
|
|
885
|
+
try {
|
|
886
|
+
const key = this.getStorageKey(controllerName);
|
|
887
|
+
const serialized = this.storage.getString(key);
|
|
888
|
+
if (!serialized) {
|
|
889
|
+
console.log(
|
|
890
|
+
`[StatePersistenceService] No persisted state found for ${controllerName}`
|
|
891
|
+
);
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
const wrapper = JSON.parse(serialized);
|
|
895
|
+
if (wrapper.version < currentVersion) {
|
|
896
|
+
console.log(
|
|
897
|
+
`[StatePersistenceService] Migrating ${controllerName} state from v${wrapper.version} to v${currentVersion}`
|
|
898
|
+
);
|
|
899
|
+
return this.migrateState(wrapper.data, wrapper.version, currentVersion, metadata);
|
|
900
|
+
}
|
|
901
|
+
return wrapper.data;
|
|
902
|
+
} catch (error) {
|
|
903
|
+
console.error(
|
|
904
|
+
`[StatePersistenceService] Failed to restore state for ${controllerName}:`,
|
|
905
|
+
error
|
|
906
|
+
);
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Delete persisted state for a controller
|
|
912
|
+
*
|
|
913
|
+
* @param controllerName - Unique identifier for the controller
|
|
914
|
+
*/
|
|
915
|
+
async deleteController(controllerName) {
|
|
916
|
+
const key = this.getStorageKey(controllerName);
|
|
917
|
+
this.storage.delete(key);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Check if persisted state exists for a controller
|
|
921
|
+
*
|
|
922
|
+
* @param controllerName - Unique identifier for the controller
|
|
923
|
+
* @returns True if state exists
|
|
924
|
+
*/
|
|
925
|
+
hasPersistedState(controllerName) {
|
|
926
|
+
const key = this.getStorageKey(controllerName);
|
|
927
|
+
return this.storage.contains(key);
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Get the version of persisted state
|
|
931
|
+
*
|
|
932
|
+
* @param controllerName - Unique identifier for the controller
|
|
933
|
+
* @returns Version number or null if not found
|
|
934
|
+
*/
|
|
935
|
+
getPersistedVersion(controllerName) {
|
|
936
|
+
try {
|
|
937
|
+
const key = this.getStorageKey(controllerName);
|
|
938
|
+
const serialized = this.storage.getString(key);
|
|
939
|
+
if (!serialized) {
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
const wrapper = JSON.parse(serialized);
|
|
943
|
+
return wrapper.version;
|
|
944
|
+
} catch (error) {
|
|
945
|
+
console.error(
|
|
946
|
+
`[StatePersistenceService] Failed to get version for ${controllerName}:`,
|
|
947
|
+
error
|
|
948
|
+
);
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Clear all persisted controller states
|
|
954
|
+
*/
|
|
955
|
+
async clearAll() {
|
|
956
|
+
const prefix = "controller:";
|
|
957
|
+
const allKeys = this.storage.getAllKeys();
|
|
958
|
+
const controllerKeys = allKeys.filter((key) => key.startsWith(prefix));
|
|
959
|
+
for (const key of controllerKeys) {
|
|
960
|
+
this.storage.delete(key);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Extract persistable state based on metadata
|
|
965
|
+
*
|
|
966
|
+
* @private
|
|
967
|
+
*/
|
|
968
|
+
extractPersistableState(state, metadata) {
|
|
969
|
+
const persistable = {};
|
|
970
|
+
for (const key in metadata) {
|
|
971
|
+
const meta = metadata[key];
|
|
972
|
+
if (meta?.persist === true && key in state) {
|
|
973
|
+
persistable[key] = state[key];
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return persistable;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Migrate state from old version to new version
|
|
980
|
+
*
|
|
981
|
+
* @private
|
|
982
|
+
*/
|
|
983
|
+
migrateState(oldState, fromVersion, _toVersion, metadata) {
|
|
984
|
+
let migratedState = { ...oldState };
|
|
985
|
+
for (const key in metadata) {
|
|
986
|
+
const meta = metadata[key];
|
|
987
|
+
if (meta?.migrator && key in migratedState) {
|
|
988
|
+
try {
|
|
989
|
+
const oldValue = migratedState[key];
|
|
990
|
+
const newValue = meta.migrator(oldValue, fromVersion);
|
|
991
|
+
migratedState[key] = newValue;
|
|
992
|
+
} catch (error) {
|
|
993
|
+
console.error(
|
|
994
|
+
`[StatePersistenceService] Migration failed for property ${String(key)}:`,
|
|
995
|
+
error
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return migratedState;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Get storage key for a controller
|
|
1004
|
+
*
|
|
1005
|
+
* @private
|
|
1006
|
+
*/
|
|
1007
|
+
getStorageKey(controllerName) {
|
|
1008
|
+
return `controller:${controllerName}`;
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
// ../networkController/dist/index.js
|
|
1013
|
+
var GET_ALL_RPC_ENDPOINTS_QUERY = `
|
|
1014
|
+
query GetAllRpcEndpoints($q: QueryGetListInput) {
|
|
1015
|
+
getAllRpcEndpoints(q: $q) {
|
|
1016
|
+
data {
|
|
1017
|
+
id
|
|
1018
|
+
createdAt
|
|
1019
|
+
updatedAt
|
|
1020
|
+
name
|
|
1021
|
+
rpcUrl
|
|
1022
|
+
chainId
|
|
1023
|
+
provider
|
|
1024
|
+
status
|
|
1025
|
+
}
|
|
1026
|
+
total
|
|
1027
|
+
pagination {
|
|
1028
|
+
limit
|
|
1029
|
+
offset
|
|
1030
|
+
page
|
|
1031
|
+
total
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
`;
|
|
1036
|
+
var GET_ONE_RPC_ENDPOINT_QUERY = `
|
|
1037
|
+
query GetOneRpcEndpoint($id: NonEmptyString!) {
|
|
1038
|
+
getOneRpcEndpoint(id: $id) {
|
|
1039
|
+
id
|
|
1040
|
+
createdAt
|
|
1041
|
+
updatedAt
|
|
1042
|
+
name
|
|
1043
|
+
rpcUrl
|
|
1044
|
+
chainId
|
|
1045
|
+
provider
|
|
1046
|
+
status
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
`;
|
|
1050
|
+
var RpcEndpointService = class _RpcEndpointService {
|
|
1051
|
+
static apiUri = "https://dev-api.eztra.io";
|
|
1052
|
+
/**
|
|
1053
|
+
* Set the API URI for GraphQL requests
|
|
1054
|
+
*/
|
|
1055
|
+
static setApiUri(uri) {
|
|
1056
|
+
_RpcEndpointService.apiUri = uri;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Get the current API URI
|
|
1060
|
+
*/
|
|
1061
|
+
static getApiUri() {
|
|
1062
|
+
return _RpcEndpointService.apiUri;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Execute a GraphQL query
|
|
1066
|
+
*/
|
|
1067
|
+
static async executeQuery(query, variables) {
|
|
1068
|
+
try {
|
|
1069
|
+
const response = await fetch(`${_RpcEndpointService.apiUri}/graphql`, {
|
|
1070
|
+
method: "POST",
|
|
1071
|
+
headers: {
|
|
1072
|
+
"Content-Type": "application/json"
|
|
1073
|
+
},
|
|
1074
|
+
body: JSON.stringify({
|
|
1075
|
+
query,
|
|
1076
|
+
variables
|
|
1077
|
+
})
|
|
1078
|
+
});
|
|
1079
|
+
if (!response.ok) {
|
|
1080
|
+
console.error("[RpcEndpointService] HTTP error:", response.status);
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
const result = await response.json();
|
|
1084
|
+
if (result.errors && result.errors.length > 0) {
|
|
1085
|
+
console.error("[RpcEndpointService] GraphQL errors:", result.errors);
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
return result.data ?? null;
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
console.error("[RpcEndpointService] Request failed:", error);
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Get all RPC endpoints with optional filtering
|
|
1096
|
+
*/
|
|
1097
|
+
static async getAll(query) {
|
|
1098
|
+
const result = await _RpcEndpointService.executeQuery(GET_ALL_RPC_ENDPOINTS_QUERY, { q: query });
|
|
1099
|
+
return result?.getAllRpcEndpoints?.data ?? null;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Get all RPC endpoints with pagination info
|
|
1103
|
+
*/
|
|
1104
|
+
static async getAllWithPagination(query) {
|
|
1105
|
+
const result = await _RpcEndpointService.executeQuery(GET_ALL_RPC_ENDPOINTS_QUERY, { q: query });
|
|
1106
|
+
return result?.getAllRpcEndpoints ?? null;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Get a single RPC endpoint by ID
|
|
1110
|
+
*/
|
|
1111
|
+
static async getOne(id) {
|
|
1112
|
+
const result = await _RpcEndpointService.executeQuery(GET_ONE_RPC_ENDPOINT_QUERY, { id });
|
|
1113
|
+
return result?.getOneRpcEndpoint ?? null;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Get RPC endpoints by chain ID
|
|
1117
|
+
*/
|
|
1118
|
+
static async getByChainId(chainId) {
|
|
1119
|
+
return _RpcEndpointService.getAll({
|
|
1120
|
+
filter: { chainId }
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Get active RPC endpoints
|
|
1125
|
+
*/
|
|
1126
|
+
static async getActive() {
|
|
1127
|
+
return _RpcEndpointService.getAll({
|
|
1128
|
+
filter: { status: "ACTIVE" }
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Get active RPC endpoints by chain ID
|
|
1133
|
+
*/
|
|
1134
|
+
static async getActiveByChainId(chainId) {
|
|
1135
|
+
return _RpcEndpointService.getAll({
|
|
1136
|
+
filter: { chainId, status: "ACTIVE" }
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
var RpcManager = class {
|
|
1141
|
+
healthMap = /* @__PURE__ */ new Map();
|
|
1142
|
+
healthCheckInterval = 3e4;
|
|
1143
|
+
// 30 seconds
|
|
1144
|
+
maxFailures = 3;
|
|
1145
|
+
/**
|
|
1146
|
+
* Get RPC URL for a specific wallet index (round-robin distribution)
|
|
1147
|
+
* @param chainId - Chain ID to get RPC for
|
|
1148
|
+
* @param endpoints - Available RPC endpoints
|
|
1149
|
+
* @param walletIndex - Wallet index for round-robin selection
|
|
1150
|
+
* @returns Selected RPC URL
|
|
1151
|
+
*/
|
|
1152
|
+
getRpcForWalletIndex(chainId, endpoints, walletIndex) {
|
|
1153
|
+
const chainEndpoints = endpoints.filter(
|
|
1154
|
+
(ep) => ep.chainId === chainId && ep.status === "ACTIVE" && ep.rpcUrl
|
|
1155
|
+
);
|
|
1156
|
+
if (chainEndpoints.length === 0) {
|
|
1157
|
+
console.warn(
|
|
1158
|
+
`[RpcManager] No active RPC endpoints found for chain ${chainId}`
|
|
1159
|
+
);
|
|
1160
|
+
return "";
|
|
1161
|
+
}
|
|
1162
|
+
const healthyEndpoints = chainEndpoints.filter(
|
|
1163
|
+
(ep) => this.isEndpointHealthy(ep.rpcUrl)
|
|
1164
|
+
);
|
|
1165
|
+
const availableEndpoints = healthyEndpoints.length > 0 ? healthyEndpoints : chainEndpoints;
|
|
1166
|
+
const selectedIndex = walletIndex % availableEndpoints.length;
|
|
1167
|
+
const selectedEndpoint = availableEndpoints[selectedIndex];
|
|
1168
|
+
return selectedEndpoint.rpcUrl;
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Get the best RPC URL for a chain (prioritizes healthy, low-latency endpoints)
|
|
1172
|
+
* @param chainId - Chain ID to get RPC for
|
|
1173
|
+
* @param endpoints - Available RPC endpoints
|
|
1174
|
+
* @returns Best RPC URL
|
|
1175
|
+
*/
|
|
1176
|
+
getBestRpc(chainId, endpoints) {
|
|
1177
|
+
const chainEndpoints = endpoints.filter(
|
|
1178
|
+
(ep) => ep.chainId === chainId && ep.status === "ACTIVE" && ep.rpcUrl
|
|
1179
|
+
);
|
|
1180
|
+
if (chainEndpoints.length === 0) {
|
|
1181
|
+
return "";
|
|
1182
|
+
}
|
|
1183
|
+
const sorted = [...chainEndpoints].sort((a, b) => {
|
|
1184
|
+
const healthA = this.healthMap.get(a.rpcUrl);
|
|
1185
|
+
const healthB = this.healthMap.get(b.rpcUrl);
|
|
1186
|
+
if (healthA?.isHealthy && !healthB?.isHealthy) return -1;
|
|
1187
|
+
if (!healthA?.isHealthy && healthB?.isHealthy) return 1;
|
|
1188
|
+
const timeA = healthA?.responseTime ?? Infinity;
|
|
1189
|
+
const timeB = healthB?.responseTime ?? Infinity;
|
|
1190
|
+
return timeA - timeB;
|
|
1191
|
+
});
|
|
1192
|
+
return sorted[0]?.rpcUrl ?? "";
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Check if an endpoint is healthy
|
|
1196
|
+
*/
|
|
1197
|
+
isEndpointHealthy(url) {
|
|
1198
|
+
const health = this.healthMap.get(url);
|
|
1199
|
+
if (!health) {
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
const now = Date.now();
|
|
1203
|
+
if (now - health.lastChecked > this.healthCheckInterval) {
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
return health.isHealthy;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Mark an RPC endpoint as failed
|
|
1210
|
+
*/
|
|
1211
|
+
markFailed(url) {
|
|
1212
|
+
const existing = this.healthMap.get(url);
|
|
1213
|
+
const failureCount = (existing?.failureCount ?? 0) + 1;
|
|
1214
|
+
this.healthMap.set(url, {
|
|
1215
|
+
url,
|
|
1216
|
+
isHealthy: failureCount < this.maxFailures,
|
|
1217
|
+
lastChecked: Date.now(),
|
|
1218
|
+
responseTime: existing?.responseTime ?? Infinity,
|
|
1219
|
+
failureCount
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Mark an RPC endpoint as successful
|
|
1224
|
+
*/
|
|
1225
|
+
markSuccess(url, responseTime) {
|
|
1226
|
+
this.healthMap.set(url, {
|
|
1227
|
+
url,
|
|
1228
|
+
isHealthy: true,
|
|
1229
|
+
lastChecked: Date.now(),
|
|
1230
|
+
responseTime,
|
|
1231
|
+
failureCount: 0
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Reset health data for all endpoints
|
|
1236
|
+
*/
|
|
1237
|
+
resetHealth() {
|
|
1238
|
+
this.healthMap.clear();
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Get health status for an endpoint
|
|
1242
|
+
*/
|
|
1243
|
+
getHealth(url) {
|
|
1244
|
+
return this.healthMap.get(url);
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Get all health statuses
|
|
1248
|
+
*/
|
|
1249
|
+
getAllHealth() {
|
|
1250
|
+
return new Map(this.healthMap);
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
var NetworkController = class extends BaseController {
|
|
1254
|
+
static VERSION = 1;
|
|
1255
|
+
name = "NetworkController";
|
|
1256
|
+
defaultState = {
|
|
1257
|
+
selectedChainId: 1,
|
|
1258
|
+
// Ethereum mainnet default
|
|
1259
|
+
rpcEndpoints: [],
|
|
1260
|
+
currentRpcUrl: null,
|
|
1261
|
+
isLoading: false,
|
|
1262
|
+
error: null,
|
|
1263
|
+
walletIndex: 0
|
|
1264
|
+
};
|
|
1265
|
+
propertyMetadata = {
|
|
1266
|
+
selectedChainId: { persist: true, anonymous: false },
|
|
1267
|
+
rpcEndpoints: { persist: false, anonymous: true },
|
|
1268
|
+
// Public data, fetched fresh
|
|
1269
|
+
currentRpcUrl: { persist: false, anonymous: true },
|
|
1270
|
+
isLoading: { persist: false, anonymous: true },
|
|
1271
|
+
error: { persist: false, anonymous: true },
|
|
1272
|
+
walletIndex: { persist: true, anonymous: false }
|
|
1273
|
+
};
|
|
1274
|
+
rpcManager;
|
|
1275
|
+
constructor(config) {
|
|
1276
|
+
super(
|
|
1277
|
+
{
|
|
1278
|
+
messenger: config.messenger,
|
|
1279
|
+
persistenceService: config.persistenceService,
|
|
1280
|
+
state: config.state
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
selectedChainId: config.defaultChainId ?? 1,
|
|
1284
|
+
rpcEndpoints: [],
|
|
1285
|
+
currentRpcUrl: null,
|
|
1286
|
+
isLoading: false,
|
|
1287
|
+
error: null,
|
|
1288
|
+
walletIndex: 0
|
|
1289
|
+
}
|
|
1290
|
+
);
|
|
1291
|
+
this.rpcManager = new RpcManager();
|
|
1292
|
+
this.registerActions();
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Register messenger actions
|
|
1296
|
+
*/
|
|
1297
|
+
registerActions() {
|
|
1298
|
+
this.messenger.registerActionHandler(
|
|
1299
|
+
"NetworkController:setChainId",
|
|
1300
|
+
this.setChainId.bind(this)
|
|
1301
|
+
);
|
|
1302
|
+
this.messenger.registerActionHandler(
|
|
1303
|
+
"NetworkController:getActiveChainId",
|
|
1304
|
+
this.getActiveChainId.bind(this)
|
|
1305
|
+
);
|
|
1306
|
+
this.messenger.registerActionHandler(
|
|
1307
|
+
"NetworkController:getRpcUrl",
|
|
1308
|
+
this.getRpcUrl.bind(this)
|
|
1309
|
+
);
|
|
1310
|
+
this.messenger.registerActionHandler(
|
|
1311
|
+
"NetworkController:refreshEndpoints",
|
|
1312
|
+
this.refreshEndpoints.bind(this)
|
|
1313
|
+
);
|
|
1314
|
+
this.messenger.registerActionHandler(
|
|
1315
|
+
"NetworkController:setWalletIndex",
|
|
1316
|
+
this.setWalletIndex.bind(this)
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Initialize controller - load RPC endpoints
|
|
1321
|
+
*/
|
|
1322
|
+
async initialize() {
|
|
1323
|
+
console.log("[NetworkController] Initializing...");
|
|
1324
|
+
if (this.persistenceService) {
|
|
1325
|
+
await this.loadPersistedState();
|
|
1326
|
+
}
|
|
1327
|
+
await this.refreshEndpoints();
|
|
1328
|
+
console.log("[NetworkController] Initialized");
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Destroy controller - cleanup
|
|
1332
|
+
*/
|
|
1333
|
+
destroy() {
|
|
1334
|
+
this.messenger.unregisterActionHandler("NetworkController:setChainId");
|
|
1335
|
+
this.messenger.unregisterActionHandler("NetworkController:getActiveChainId");
|
|
1336
|
+
this.messenger.unregisterActionHandler("NetworkController:getRpcUrl");
|
|
1337
|
+
this.messenger.unregisterActionHandler("NetworkController:refreshEndpoints");
|
|
1338
|
+
this.messenger.unregisterActionHandler("NetworkController:setWalletIndex");
|
|
1339
|
+
super.destroy();
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Action: Set active chain ID
|
|
1343
|
+
*/
|
|
1344
|
+
async setChainId(chainId) {
|
|
1345
|
+
if (this.state.selectedChainId === chainId) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
console.log(`[NetworkController] Switching to chain ${chainId}`);
|
|
1349
|
+
this.update({ isLoading: true, error: null });
|
|
1350
|
+
try {
|
|
1351
|
+
const rpcUrl = this.getRpcUrl(chainId);
|
|
1352
|
+
if (!rpcUrl) {
|
|
1353
|
+
throw new Error(`No RPC endpoint available for chain ${chainId}`);
|
|
1354
|
+
}
|
|
1355
|
+
this.update({
|
|
1356
|
+
selectedChainId: chainId,
|
|
1357
|
+
currentRpcUrl: rpcUrl,
|
|
1358
|
+
isLoading: false
|
|
1359
|
+
});
|
|
1360
|
+
this.messenger.publish("NetworkController:chainChanged", {
|
|
1361
|
+
chainId,
|
|
1362
|
+
rpcUrl
|
|
1363
|
+
});
|
|
1364
|
+
if (this.persistenceService) {
|
|
1365
|
+
await this.persist();
|
|
1366
|
+
}
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1369
|
+
this.update({
|
|
1370
|
+
isLoading: false,
|
|
1371
|
+
error: errorMessage
|
|
1372
|
+
});
|
|
1373
|
+
throw error;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Action: Get active chain ID
|
|
1378
|
+
*/
|
|
1379
|
+
getActiveChainId() {
|
|
1380
|
+
return this.state.selectedChainId;
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Action: Get RPC URL for a chain
|
|
1384
|
+
*/
|
|
1385
|
+
getRpcUrl(chainId) {
|
|
1386
|
+
const { rpcEndpoints, walletIndex } = this.state;
|
|
1387
|
+
const rpcUrl = this.rpcManager.getRpcForWalletIndex(
|
|
1388
|
+
chainId,
|
|
1389
|
+
rpcEndpoints,
|
|
1390
|
+
walletIndex
|
|
1391
|
+
);
|
|
1392
|
+
return rpcUrl;
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Action: Refresh RPC endpoints from service
|
|
1396
|
+
*/
|
|
1397
|
+
async refreshEndpoints() {
|
|
1398
|
+
console.log("[NetworkController] Refreshing RPC endpoints...");
|
|
1399
|
+
this.update({ isLoading: true });
|
|
1400
|
+
try {
|
|
1401
|
+
const endpoints = await RpcEndpointService.getAll({});
|
|
1402
|
+
this.update({
|
|
1403
|
+
rpcEndpoints: endpoints || [],
|
|
1404
|
+
isLoading: false,
|
|
1405
|
+
error: null
|
|
1406
|
+
});
|
|
1407
|
+
this.messenger.publish("NetworkController:endpointsUpdated", {
|
|
1408
|
+
count: endpoints?.length || 0
|
|
1409
|
+
});
|
|
1410
|
+
console.log(
|
|
1411
|
+
`[NetworkController] Loaded ${endpoints?.length || 0} RPC endpoints`
|
|
1412
|
+
);
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to fetch endpoints";
|
|
1415
|
+
this.update({
|
|
1416
|
+
isLoading: false,
|
|
1417
|
+
error: errorMessage
|
|
1418
|
+
});
|
|
1419
|
+
console.error("[NetworkController] Failed to refresh endpoints:", error);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Action: Set wallet index (affects RPC selection)
|
|
1424
|
+
*/
|
|
1425
|
+
setWalletIndex(index) {
|
|
1426
|
+
if (this.state.walletIndex === index) {
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
this.update({ walletIndex: index });
|
|
1430
|
+
const rpcUrl = this.getRpcUrl(this.state.selectedChainId);
|
|
1431
|
+
if (rpcUrl && rpcUrl !== this.state.currentRpcUrl) {
|
|
1432
|
+
this.update({ currentRpcUrl: rpcUrl });
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
// ../walletController/dist/index.js
|
|
1438
|
+
var WalletController = class extends BaseController {
|
|
1439
|
+
static VERSION = 1;
|
|
1440
|
+
name = "WalletController";
|
|
1441
|
+
defaultState = {
|
|
1442
|
+
wallets: [],
|
|
1443
|
+
selectedWalletIndex: 0,
|
|
1444
|
+
isLoading: false,
|
|
1445
|
+
error: null,
|
|
1446
|
+
isLocked: true
|
|
1447
|
+
};
|
|
1448
|
+
propertyMetadata = {
|
|
1449
|
+
wallets: {
|
|
1450
|
+
persist: true,
|
|
1451
|
+
anonymous: false,
|
|
1452
|
+
required: false
|
|
1453
|
+
},
|
|
1454
|
+
selectedWalletIndex: {
|
|
1455
|
+
persist: true,
|
|
1456
|
+
anonymous: false
|
|
1457
|
+
},
|
|
1458
|
+
isLoading: {
|
|
1459
|
+
persist: false,
|
|
1460
|
+
anonymous: true
|
|
1461
|
+
},
|
|
1462
|
+
error: {
|
|
1463
|
+
persist: false,
|
|
1464
|
+
anonymous: true
|
|
1465
|
+
},
|
|
1466
|
+
isLocked: {
|
|
1467
|
+
persist: true,
|
|
1468
|
+
anonymous: false
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
constructor(config) {
|
|
1472
|
+
super(
|
|
1473
|
+
{
|
|
1474
|
+
messenger: config.messenger,
|
|
1475
|
+
persistenceService: config.persistenceService,
|
|
1476
|
+
state: config.state
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
wallets: [],
|
|
1480
|
+
selectedWalletIndex: 0,
|
|
1481
|
+
isLoading: false,
|
|
1482
|
+
error: null,
|
|
1483
|
+
isLocked: true
|
|
1484
|
+
}
|
|
1485
|
+
);
|
|
1486
|
+
this.registerActions();
|
|
1487
|
+
this.subscribeToEvents();
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Register messenger actions
|
|
1491
|
+
*/
|
|
1492
|
+
registerActions() {
|
|
1493
|
+
this.messenger.registerActionHandler(
|
|
1494
|
+
"WalletController:addWallet",
|
|
1495
|
+
this.addWallet.bind(this)
|
|
1496
|
+
);
|
|
1497
|
+
this.messenger.registerActionHandler(
|
|
1498
|
+
"WalletController:selectWallet",
|
|
1499
|
+
this.selectWallet.bind(this)
|
|
1500
|
+
);
|
|
1501
|
+
this.messenger.registerActionHandler(
|
|
1502
|
+
"WalletController:getActiveWallet",
|
|
1503
|
+
this.getActiveWallet.bind(this)
|
|
1504
|
+
);
|
|
1505
|
+
this.messenger.registerActionHandler(
|
|
1506
|
+
"WalletController:lockWallet",
|
|
1507
|
+
this.lockWallet.bind(this)
|
|
1508
|
+
);
|
|
1509
|
+
this.messenger.registerActionHandler(
|
|
1510
|
+
"WalletController:unlockWallet",
|
|
1511
|
+
this.unlockWallet.bind(this)
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Subscribe to events from other controllers
|
|
1516
|
+
*/
|
|
1517
|
+
subscribeToEvents() {
|
|
1518
|
+
this.messenger.subscribe(
|
|
1519
|
+
"NetworkController:chainChanged",
|
|
1520
|
+
this.onNetworkChanged.bind(this)
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Initialize controller
|
|
1525
|
+
*/
|
|
1526
|
+
async initialize() {
|
|
1527
|
+
console.log("[WalletController] Initializing...");
|
|
1528
|
+
if (this.persistenceService) {
|
|
1529
|
+
await this.loadPersistedState();
|
|
1530
|
+
}
|
|
1531
|
+
const chainId = this.messenger.call("NetworkController:getActiveChainId");
|
|
1532
|
+
console.log(`[WalletController] Active chain: ${chainId}`);
|
|
1533
|
+
console.log("[WalletController] Initialized");
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Destroy controller
|
|
1537
|
+
*/
|
|
1538
|
+
destroy() {
|
|
1539
|
+
this.messenger.unregisterActionHandler("WalletController:addWallet");
|
|
1540
|
+
this.messenger.unregisterActionHandler("WalletController:selectWallet");
|
|
1541
|
+
this.messenger.unregisterActionHandler("WalletController:getActiveWallet");
|
|
1542
|
+
this.messenger.unregisterActionHandler("WalletController:lockWallet");
|
|
1543
|
+
this.messenger.unregisterActionHandler("WalletController:unlockWallet");
|
|
1544
|
+
super.destroy();
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Event handler: Network changed
|
|
1548
|
+
*/
|
|
1549
|
+
onNetworkChanged(event) {
|
|
1550
|
+
console.log(
|
|
1551
|
+
`[WalletController] Network changed to chain ${event.chainId}, may need to refresh balances`
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Action: Add wallet
|
|
1556
|
+
*/
|
|
1557
|
+
async addWallet(wallet) {
|
|
1558
|
+
console.log(`[WalletController] Adding wallet: ${wallet.name}`);
|
|
1559
|
+
const wallets = [...this.state.wallets, wallet];
|
|
1560
|
+
this.update({ wallets });
|
|
1561
|
+
if (this.persistenceService) {
|
|
1562
|
+
await this.persist();
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Action: Select wallet
|
|
1567
|
+
*/
|
|
1568
|
+
async selectWallet(index) {
|
|
1569
|
+
if (index < 0 || index >= this.state.wallets.length) {
|
|
1570
|
+
throw new Error(`Invalid wallet index: ${index}`);
|
|
1571
|
+
}
|
|
1572
|
+
if (this.state.selectedWalletIndex === index) {
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const wallet = this.state.wallets[index];
|
|
1576
|
+
this.update({ selectedWalletIndex: index });
|
|
1577
|
+
this.messenger.publish("WalletController:walletSelected", {
|
|
1578
|
+
index,
|
|
1579
|
+
wallet
|
|
1580
|
+
});
|
|
1581
|
+
this.messenger.call("NetworkController:setWalletIndex", index);
|
|
1582
|
+
if (this.persistenceService) {
|
|
1583
|
+
await this.persist();
|
|
1584
|
+
}
|
|
1585
|
+
console.log(`[WalletController] Selected wallet: ${wallet.name}`);
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Action: Get active wallet
|
|
1589
|
+
*/
|
|
1590
|
+
getActiveWallet() {
|
|
1591
|
+
const { wallets, selectedWalletIndex } = this.state;
|
|
1592
|
+
if (wallets.length === 0) {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
return wallets[selectedWalletIndex] || null;
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Action: Lock wallet
|
|
1599
|
+
*/
|
|
1600
|
+
lockWallet() {
|
|
1601
|
+
this.update({ isLocked: true });
|
|
1602
|
+
this.messenger.publish("WalletController:walletLocked", {});
|
|
1603
|
+
console.log("[WalletController] Wallet locked");
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Action: Unlock wallet
|
|
1607
|
+
*/
|
|
1608
|
+
async unlockWallet(_password) {
|
|
1609
|
+
this.update({ isLocked: false });
|
|
1610
|
+
this.messenger.publish("WalletController:walletUnlocked", {});
|
|
1611
|
+
console.log("[WalletController] Wallet unlocked");
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
|
|
243
1616
|
// src/EztraEngine.ts
|
|
244
|
-
import { ControllerMessenger } from "@eztra/controller";
|
|
245
|
-
import { StatePersistenceService } from "@eztra/storage";
|
|
246
|
-
import {
|
|
247
|
-
NetworkController
|
|
248
|
-
} from "@eztra/network-controller";
|
|
249
|
-
import {
|
|
250
|
-
WalletController
|
|
251
|
-
} from "@eztra/wallet-controller";
|
|
252
1617
|
var EztraEngine = class {
|
|
253
1618
|
messenger;
|
|
254
1619
|
composableController;
|