@eztra.services/engine 1.0.0-dev.20260202085509 → 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 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;