@monygroupcorp/micro-web3 0.1.1 → 1.2.0

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.
@@ -0,0 +1,213 @@
1
+ import { FloatingWalletButton } from '../FloatingWalletButton/FloatingWalletButton.js';
2
+ import WalletModal from '../Wallet/WalletModal.js';
3
+
4
+ class WalletButtonModal extends WalletModal {
5
+ events() {
6
+ const baseEvents = typeof super.events === 'function' ? super.events() : {};
7
+ return {
8
+ ...baseEvents,
9
+ 'click .wallet-option': async (e) => {
10
+ const button = e?.target?.closest?.('.wallet-option');
11
+ const walletType = button?.dataset?.wallet;
12
+ if (!walletType) {
13
+ return;
14
+ }
15
+ if (this.onWalletSelected) {
16
+ await this.onWalletSelected(walletType);
17
+ }
18
+ this.hide();
19
+ },
20
+ };
21
+ }
22
+ }
23
+
24
+ export class WalletButton extends FloatingWalletButton {
25
+ constructor(props) {
26
+ super(props);
27
+ this.modalRoot = null;
28
+ }
29
+
30
+ async showWalletModal() {
31
+ const providerMap =
32
+ this.walletService?.providerMap ||
33
+ {
34
+ rabby: () => (window.ethereum?.isRabby ? window.ethereum : null),
35
+ rainbow: () => (window.ethereum?.isRainbow ? window.ethereum : null),
36
+ phantom: () => window.phantom?.ethereum || null,
37
+ metamask: () => window.ethereum || null,
38
+ };
39
+
40
+ const walletIcons =
41
+ this.walletService?.walletIcons ||
42
+ {
43
+ rabby: '/public/wallets/rabby.webp',
44
+ rainbow: '/public/wallets/rainbow.webp',
45
+ phantom: '/public/wallets/phantom.webp',
46
+ metamask: '/public/wallets/MetaMask.webp',
47
+ };
48
+
49
+ if (!this.walletModal) {
50
+ this.walletModal = new WalletButtonModal({
51
+ providerMap,
52
+ walletIcons,
53
+ onWalletSelected: async (walletType) => {
54
+ await this.handleWalletSelection(walletType);
55
+ },
56
+ });
57
+
58
+ if (!this.modalRoot) {
59
+ this.modalRoot = document.createElement('div');
60
+ document.body.appendChild(this.modalRoot);
61
+ }
62
+
63
+ this.walletModal.mount(this.modalRoot);
64
+ }
65
+
66
+ this.walletModal.show();
67
+ }
68
+
69
+ onUnmount() {
70
+ if (this.modalRoot?.parentNode) {
71
+ this.modalRoot.parentNode.removeChild(this.modalRoot);
72
+ this.modalRoot = null;
73
+ }
74
+ super.onUnmount();
75
+ }
76
+
77
+ render() {
78
+ if (this.state.loading) {
79
+ return `
80
+ <div class="floating-wallet-button loading">
81
+ <div class="wallet-spinner"></div>
82
+ </div>
83
+ `;
84
+ }
85
+
86
+ const { walletConnected, address, balance, menuOpen } = this.state;
87
+
88
+ if (!walletConnected) {
89
+ return `
90
+ <div class="floating-wallet-button disconnected" data-ref="wallet-button">
91
+ <button class="wallet-btn" data-ref="connect-btn">
92
+ <span class="wallet-mark"></span>
93
+ <span class="wallet-text">Connect Wallet</span>
94
+ </button>
95
+ </div>
96
+ `;
97
+ }
98
+
99
+ const truncatedAddress = `${address.slice(0, 6)}...${address.slice(-4)}`;
100
+ const classNames = ['floating-wallet-button', 'connected'];
101
+ if (menuOpen) {
102
+ classNames.push('menu-open');
103
+ }
104
+
105
+ const markup = `
106
+ <div class="${classNames.join(' ')}" data-ref="wallet-button">
107
+ <button class="wallet-btn" data-ref="wallet-btn" title="${this.escapeHtml(address)}\nBalance: ${balance} ETH">
108
+ <span class="wallet-mark wallet-mark--connected"></span>
109
+ <span class="wallet-address">${this.escapeHtml(truncatedAddress)}</span>
110
+ <span class="wallet-disconnect" data-ref="inline-disconnect" title="Disconnect">⏏︎</span>
111
+ </button>
112
+ ${this.renderWalletPanel(address, balance, menuOpen)}
113
+ </div>
114
+ `;
115
+
116
+ return this.minifyHtml(markup);
117
+ }
118
+
119
+ events() {
120
+ const baseEvents = super.events?.() || {};
121
+ return {
122
+ ...baseEvents,
123
+ 'click [data-ref="inline-disconnect"]': (e) => this.handleInlineDisconnect(e),
124
+ 'click [data-ref="wallet-btn"]': (e) => this.handleWalletToggle(e),
125
+ 'click [data-ref="connect-btn"]': (e) => this.handleWalletToggle(e),
126
+ };
127
+ }
128
+
129
+ renderWalletPanel(address, balance, menuOpen) {
130
+ const truncatedAddress = `${address.slice(0, 6)}...${address.slice(-4)}`;
131
+
132
+ const markup = `
133
+ <div class="wallet-info-panel" data-ref="wallet-panel" aria-hidden="${menuOpen ? 'false' : 'true'}">
134
+ <div class="wallet-info-row">
135
+ <span class="wallet-info-label">Address</span>
136
+ <span class="wallet-info-value">${this.escapeHtml(truncatedAddress)}</span>
137
+ </div>
138
+ <div class="wallet-info-row">
139
+ <span class="wallet-info-label">ETH Balance</span>
140
+ <span class="wallet-info-value">${balance} ETH</span>
141
+ </div>
142
+ </div>
143
+ `;
144
+
145
+ return this.minifyHtml(markup);
146
+ }
147
+
148
+ onStateUpdate(oldState, newState) {
149
+ if (typeof super.onStateUpdate === 'function') {
150
+ super.onStateUpdate(oldState, newState);
151
+ }
152
+ if (!oldState || oldState.menuOpen !== newState.menuOpen) {
153
+ if (newState.menuOpen) {
154
+ this.attachOutsideClickListener();
155
+ } else {
156
+ this.detachOutsideClickListener();
157
+ }
158
+ }
159
+ }
160
+
161
+ attachOutsideClickListener() {
162
+ if (this.outsideClickHandler || this.outsideClickTimeout) return;
163
+ this.outsideClickHandler = (event) => {
164
+ if (!this.element) return;
165
+ const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
166
+ const clickedInside =
167
+ (path.length && path.includes(this.element)) ||
168
+ (event.target && this.element.contains(event.target));
169
+ if (!clickedInside) {
170
+ this.setState({ menuOpen: false });
171
+ }
172
+ };
173
+ this.outsideClickTimeout = window.setTimeout(() => {
174
+ document.addEventListener('click', this.outsideClickHandler);
175
+ this.outsideClickTimeout = null;
176
+ }, 0);
177
+ }
178
+
179
+ detachOutsideClickListener() {
180
+ if (this.outsideClickTimeout) {
181
+ window.clearTimeout(this.outsideClickTimeout);
182
+ this.outsideClickTimeout = null;
183
+ }
184
+ if (this.outsideClickHandler) {
185
+ document.removeEventListener('click', this.outsideClickHandler);
186
+ this.outsideClickHandler = null;
187
+ }
188
+ }
189
+
190
+ setupDOMEventListeners() {
191
+ // Delegated events handle wiring in this component.
192
+ }
193
+
194
+ handleWalletToggle(e) {
195
+ this.handleButtonClick(e);
196
+ }
197
+
198
+ handleInlineDisconnect(e) {
199
+ if (e) {
200
+ e.preventDefault();
201
+ e.stopPropagation();
202
+ }
203
+ super.handleDisconnect(e);
204
+ }
205
+
206
+ minifyHtml(markup) {
207
+ return typeof markup === 'string'
208
+ ? markup.replace(/>\s+</g, '><').trim()
209
+ : markup;
210
+ }
211
+ }
212
+
213
+ export default WalletButton;
package/src/index.js CHANGED
@@ -4,9 +4,14 @@ import WalletService from './services/WalletService.js';
4
4
  import ContractCache from './services/ContractCache.js';
5
5
  import PriceService from './services/PriceService.js';
6
6
  import * as IpfsService from './services/IpfsService.js';
7
+ import EventIndexer from './services/EventIndexer.js';
8
+
9
+ // Storage adapters and settings
10
+ import { IndexedDBAdapter, MemoryAdapter, IndexerSettings } from './storage/index.js';
7
11
 
8
12
  // UI Components
9
13
  import { FloatingWalletButton } from './components/FloatingWalletButton/FloatingWalletButton.js';
14
+ import { WalletButton } from './components/WalletButton/WalletButton.js';
10
15
  import { WalletModal } from './components/Wallet/WalletModal.js';
11
16
  import { IpfsImage } from './components/Ipfs/IpfsImage.js';
12
17
  import { SwapInterface } from './components/Swap/SwapInterface.js';
@@ -18,6 +23,8 @@ import { BondingCurve } from './components/BondingCurve/BondingCurve.js';
18
23
  import { PriceDisplay } from './components/Display/PriceDisplay.js';
19
24
  import { BalanceDisplay } from './components/Display/BalanceDisplay.js';
20
25
  import { MessagePopup } from './components/Util/MessagePopup.js';
26
+ import { SyncProgressBar } from './components/SyncProgressBar/SyncProgressBar.js';
27
+ import { SettingsModal } from './components/SettingsModal/SettingsModal.js';
21
28
 
22
29
  export {
23
30
  // Services
@@ -26,9 +33,16 @@ export {
26
33
  ContractCache,
27
34
  PriceService,
28
35
  IpfsService,
36
+ EventIndexer,
37
+
38
+ // Storage adapters and settings
39
+ IndexedDBAdapter,
40
+ MemoryAdapter,
41
+ IndexerSettings,
29
42
 
30
43
  // UI Components
31
44
  FloatingWalletButton,
45
+ WalletButton,
32
46
  WalletModal,
33
47
  IpfsImage,
34
48
  SwapInterface,
@@ -39,5 +53,7 @@ export {
39
53
  BondingCurve,
40
54
  PriceDisplay,
41
55
  BalanceDisplay,
42
- MessagePopup
43
- };
56
+ MessagePopup,
57
+ SyncProgressBar,
58
+ SettingsModal
59
+ };
@@ -0,0 +1,218 @@
1
+ // src/indexer/EntityResolver.js
2
+
3
+ /**
4
+ * Entity resolver for EventIndexer.
5
+ * Provides the Entities API for domain-level queries.
6
+ */
7
+ class EntityResolver {
8
+ constructor(queryEngine, entityDefinitions = {}) {
9
+ this.queryEngine = queryEngine;
10
+ this.definitions = entityDefinitions;
11
+ this.entityAPIs = {};
12
+
13
+ // Create API for each entity
14
+ for (const [name, definition] of Object.entries(entityDefinitions)) {
15
+ this.entityAPIs[name] = this._createEntityAPI(name, definition);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Get entity API by name.
21
+ * @param {string} name - Entity name
22
+ * @returns {EntityQueryable}
23
+ */
24
+ getEntity(name) {
25
+ return this.entityAPIs[name];
26
+ }
27
+
28
+ /**
29
+ * Get all entity APIs (for proxy access).
30
+ * @returns {Object}
31
+ */
32
+ getAllEntities() {
33
+ return this.entityAPIs;
34
+ }
35
+
36
+ _createEntityAPI(name, definition) {
37
+ const self = this;
38
+
39
+ return {
40
+ /**
41
+ * Query entities.
42
+ * @param {EntityWhereClause} where - Filter conditions
43
+ * @returns {Promise<Entity[]>}
44
+ */
45
+ async query(where = {}) {
46
+ return self._queryEntities(name, definition, where);
47
+ },
48
+
49
+ /**
50
+ * Get single entity by key.
51
+ * @param {string|number} key - Entity key value
52
+ * @returns {Promise<Entity|null>}
53
+ */
54
+ async get(key) {
55
+ const entities = await self._queryEntities(name, definition, {
56
+ [definition.key]: key
57
+ });
58
+ return entities[0] || null;
59
+ },
60
+
61
+ /**
62
+ * Subscribe to entity changes.
63
+ * @param {EntityWhereClause} where - Filter conditions
64
+ * @param {Function} callback - Called when entities change
65
+ * @returns {Function} Unsubscribe function
66
+ */
67
+ subscribe(where, callback) {
68
+ // Subscribe to source event and related events
69
+ const eventTypes = self._getRelevantEventTypes(definition);
70
+
71
+ return self.queryEngine.subscribe(eventTypes, async () => {
72
+ const entities = await self._queryEntities(name, definition, where);
73
+ callback(entities);
74
+ });
75
+ },
76
+
77
+ /**
78
+ * Count entities.
79
+ * @param {EntityWhereClause} where - Filter conditions
80
+ * @returns {Promise<number>}
81
+ */
82
+ async count(where = {}) {
83
+ const entities = await self._queryEntities(name, definition, where);
84
+ return entities.length;
85
+ }
86
+ };
87
+ }
88
+
89
+ async _queryEntities(name, definition, where = {}) {
90
+ // Separate status filter from direct filters
91
+ const statusFilter = where.status;
92
+ const directFilters = { ...where };
93
+ delete directFilters.status;
94
+
95
+ // Query source events
96
+ const sourceResult = await this.queryEngine.query(definition.source, {
97
+ where: directFilters,
98
+ limit: 10000 // Get all matching source events
99
+ });
100
+
101
+ // Get event checker for status/computed evaluation
102
+ const eventChecker = await this.queryEngine.getEventChecker();
103
+
104
+ // Prefetch events needed for status evaluation
105
+ const relevantEventTypes = this._getRelevantEventTypes(definition);
106
+ await eventChecker.prefetch(relevantEventTypes);
107
+
108
+ // Build entities from source events
109
+ const entities = [];
110
+ for (const sourceEvent of sourceResult.events) {
111
+ const entity = await this._buildEntity(name, definition, sourceEvent, eventChecker);
112
+
113
+ // Apply status filter
114
+ if (statusFilter && entity.status !== statusFilter) {
115
+ continue;
116
+ }
117
+
118
+ entities.push(entity);
119
+ }
120
+
121
+ return entities;
122
+ }
123
+
124
+ async _buildEntity(name, definition, sourceEvent, eventChecker) {
125
+ const entity = {
126
+ _type: name,
127
+ _sourceEvent: sourceEvent,
128
+ ...sourceEvent.data
129
+ };
130
+
131
+ // Add key field explicitly
132
+ entity[definition.key] = sourceEvent.data[definition.key] || sourceEvent.indexed[definition.key];
133
+
134
+ // Evaluate status
135
+ if (definition.status) {
136
+ entity.status = this._evaluateStatus(entity, definition.status, eventChecker);
137
+ }
138
+
139
+ // Evaluate computed fields
140
+ if (definition.computed) {
141
+ for (const [fieldName, computeFn] of Object.entries(definition.computed)) {
142
+ try {
143
+ entity[fieldName] = computeFn(entity, eventChecker);
144
+ } catch (error) {
145
+ console.error(`[EntityResolver] Error computing ${fieldName}:`, error);
146
+ entity[fieldName] = null;
147
+ }
148
+ }
149
+ }
150
+
151
+ // Resolve relations (lazy - only resolve when accessed)
152
+ if (definition.relations) {
153
+ for (const [relationName, relationDef] of Object.entries(definition.relations)) {
154
+ Object.defineProperty(entity, relationName, {
155
+ get: () => this._resolveRelation(entity, relationDef),
156
+ enumerable: true
157
+ });
158
+ }
159
+ }
160
+
161
+ return entity;
162
+ }
163
+
164
+ _evaluateStatus(entity, statusDef, eventChecker) {
165
+ // Check each status condition in order
166
+ for (const [statusName, condition] of Object.entries(statusDef)) {
167
+ if (statusName === 'default') continue;
168
+
169
+ try {
170
+ if (condition(entity, eventChecker)) {
171
+ return statusName;
172
+ }
173
+ } catch (error) {
174
+ console.error(`[EntityResolver] Error evaluating status ${statusName}:`, error);
175
+ }
176
+ }
177
+
178
+ return statusDef.default || 'unknown';
179
+ }
180
+
181
+ async _resolveRelation(entity, relationDef) {
182
+ const relatedEntity = this.entityAPIs[relationDef.entity];
183
+ if (!relatedEntity) return null;
184
+
185
+ const foreignKeyValue = entity[relationDef.foreignKey];
186
+ if (foreignKeyValue === undefined) return null;
187
+
188
+ if (relationDef.type === 'many') {
189
+ return relatedEntity.query({ [relationDef.foreignKey]: foreignKeyValue });
190
+ } else {
191
+ return relatedEntity.get(foreignKeyValue);
192
+ }
193
+ }
194
+
195
+ _getRelevantEventTypes(definition) {
196
+ const types = new Set([definition.source]);
197
+
198
+ // Add events referenced in status conditions
199
+ if (definition.status) {
200
+ for (const condition of Object.values(definition.status)) {
201
+ if (typeof condition === 'function') {
202
+ // Try to extract event names from function source
203
+ // This is a best-effort heuristic
204
+ const fnStr = condition.toString();
205
+ const matches = fnStr.match(/events\.has\(['"](\w+)['"]/g) || [];
206
+ for (const match of matches) {
207
+ const eventName = match.match(/['"](\w+)['"]/)?.[1];
208
+ if (eventName) types.add(eventName);
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ return Array.from(types);
215
+ }
216
+ }
217
+
218
+ export default EntityResolver;