@monygroupcorp/micro-web3 0.1.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,265 @@
1
+ import { Component } from '@monygroupcorp/microact';
2
+ import { isIpfsUri, resolveIpfsToHttp, getAvailableGateways } from '../../services/IpfsService.js';
3
+
4
+ /**
5
+ * IpfsImage Component
6
+ *
7
+ * Renders images with IPFS support, including gateway rotation on failure.
8
+ * Falls back gracefully if all gateways fail.
9
+ *
10
+ * Props:
11
+ * - src: Image URL (HTTP or IPFS)
12
+ * - alt: Alt text for image
13
+ * - className: CSS classes
14
+ * - style: Inline styles
15
+ * - loading: Loading attribute ('lazy', 'eager', etc.)
16
+ * - onLoad: Callback when image loads
17
+ * - onError: Callback when image fails
18
+ * - placeholder: Placeholder content while loading (optional)
19
+ * - errorPlaceholder: Error placeholder content (optional)
20
+ */
21
+ export class IpfsImage extends Component {
22
+ constructor(props = {}) {
23
+ super();
24
+ this.props = props;
25
+
26
+ // State for gateway rotation and loading
27
+ this.setState({
28
+ gatewayIndex: 0,
29
+ isLoading: true,
30
+ hasError: false,
31
+ currentSrc: null
32
+ });
33
+
34
+ // Bind methods
35
+ this.handleImageLoad = this.handleImageLoad.bind(this);
36
+ this.handleImageError = this.handleImageError.bind(this);
37
+ }
38
+
39
+ /**
40
+ * Get current HTTP URL based on src and gateway index
41
+ */
42
+ getCurrentUrl() {
43
+ const { src } = this.props;
44
+
45
+ if (!src) {
46
+ return null;
47
+ }
48
+
49
+ // If not IPFS, return as-is
50
+ if (!isIpfsUri(src)) {
51
+ return src;
52
+ }
53
+
54
+ // Resolve IPFS to HTTP using current gateway
55
+ return resolveIpfsToHttp(src, this.state.gatewayIndex);
56
+ }
57
+
58
+ /**
59
+ * Handle successful image load
60
+ */
61
+ handleImageLoad(event) {
62
+ this.setState({
63
+ isLoading: false,
64
+ hasError: false
65
+ });
66
+
67
+ // Call user's onLoad callback if provided
68
+ if (this.props.onLoad) {
69
+ this.props.onLoad(event);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Handle image load error - try next gateway
75
+ */
76
+ handleImageError(event) {
77
+ const gateways = getAvailableGateways();
78
+ const { src } = this.props;
79
+
80
+ // If not IPFS or out of gateways, show error
81
+ if (!isIpfsUri(src) || this.state.gatewayIndex >= gateways.length - 1) {
82
+ this.setState({
83
+ isLoading: false,
84
+ hasError: true
85
+ });
86
+
87
+ // Log error for debugging
88
+ console.error('[IpfsImage] Failed to load image:', src, {
89
+ triedGateways: this.state.gatewayIndex + 1,
90
+ totalGateways: gateways.length
91
+ });
92
+
93
+ // Call user's onError callback if provided
94
+ if (this.props.onError) {
95
+ this.props.onError(event);
96
+ }
97
+
98
+ return;
99
+ }
100
+
101
+ // Try next gateway
102
+ const nextIndex = this.state.gatewayIndex + 1;
103
+ this.setState({
104
+ gatewayIndex: nextIndex
105
+ });
106
+
107
+ // Update image src to trigger new load attempt
108
+ const img = this.element?.querySelector('img');
109
+ if (img) {
110
+ const nextUrl = resolveIpfsToHttp(src, nextIndex);
111
+ if (nextUrl) {
112
+ img.src = nextUrl;
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Render component
119
+ */
120
+ render() {
121
+ const {
122
+ src,
123
+ alt = '',
124
+ className = '',
125
+ style = {},
126
+ loading = 'lazy',
127
+ placeholder,
128
+ errorPlaceholder
129
+ } = this.props;
130
+
131
+ const { isLoading, hasError } = this.state;
132
+
133
+ if (!src) {
134
+ return '<div class="ipfs-image-empty"></div>';
135
+ }
136
+
137
+ const currentUrl = this.getCurrentUrl();
138
+
139
+ // Error state - show placeholder
140
+ if (hasError) {
141
+ if (errorPlaceholder) {
142
+ return errorPlaceholder;
143
+ }
144
+
145
+ // Default error placeholder
146
+ return `
147
+ <div class="ipfs-image-error ${className}" style="${this.styleToString(style)}">
148
+ <div class="ipfs-image-error-icon">⚠️</div>
149
+ <div class="ipfs-image-error-text">IPFS image unavailable</div>
150
+ </div>
151
+ `;
152
+ }
153
+
154
+ // Loading or loaded state - show image
155
+ return `
156
+ <div class="ipfs-image-container ${className}" style="${this.styleToString(style)}">
157
+ ${isLoading && placeholder ? placeholder : ''}
158
+ <img
159
+ src="${this.escapeHtml(currentUrl || '')}"
160
+ alt="${this.escapeHtml(alt)}"
161
+ loading="${loading}"
162
+ class="ipfs-image ${isLoading ? 'ipfs-image-loading' : 'ipfs-image-loaded'}"
163
+ style="${isLoading ? 'opacity: 0;' : 'opacity: 1; transition: opacity 0.3s;'}"
164
+ />
165
+ </div>
166
+ `;
167
+ }
168
+
169
+ /**
170
+ * Mount component and attach event listeners
171
+ */
172
+ mount(element) {
173
+ super.mount(element);
174
+
175
+ // Attach load/error handlers to img element
176
+ const img = this.element?.querySelector('img');
177
+ if (img) {
178
+ img.addEventListener('load', this.handleImageLoad);
179
+ img.addEventListener('error', this.handleImageError);
180
+
181
+ // Register cleanup
182
+ this.registerCleanup(() => {
183
+ img.removeEventListener('load', this.handleImageLoad);
184
+ img.removeEventListener('error', this.handleImageError);
185
+ });
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Convert style object to string
191
+ */
192
+ styleToString(style) {
193
+ if (!style || typeof style !== 'object') {
194
+ return '';
195
+ }
196
+
197
+ return Object.entries(style)
198
+ .map(([key, value]) => {
199
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
200
+ return `${cssKey}: ${value};`;
201
+ })
202
+ .join(' ');
203
+ }
204
+ }
205
+
206
+ // Static styles for the component
207
+ IpfsImage.styles = `
208
+ .ipfs-image-container {
209
+ position: relative;
210
+ display: inline-block;
211
+ width: 100%;
212
+ height: 100%;
213
+ }
214
+
215
+ .ipfs-image {
216
+ display: block;
217
+ width: 100%;
218
+ height: 100%;
219
+ object-fit: cover;
220
+ }
221
+
222
+ .ipfs-image-loading {
223
+ opacity: 0;
224
+ }
225
+
226
+ .ipfs-image-loaded {
227
+ opacity: 1;
228
+ transition: opacity 0.3s ease-in;
229
+ }
230
+
231
+ .ipfs-image-error {
232
+ display: flex;
233
+ flex-direction: column;
234
+ align-items: center;
235
+ justify-content: center;
236
+ min-height: 100px;
237
+ background-color: #f5f5f5;
238
+ color: #666;
239
+ padding: 1rem;
240
+ text-align: center;
241
+ }
242
+
243
+ .ipfs-image-error-icon {
244
+ font-size: 2rem;
245
+ margin-bottom: 0.5rem;
246
+ }
247
+
248
+ .ipfs-image-error-text {
249
+ font-size: 0.875rem;
250
+ }
251
+
252
+ .ipfs-image-empty {
253
+ display: block;
254
+ width: 100%;
255
+ height: 100%;
256
+ background-color: transparent;
257
+ }
258
+
259
+ /* Dark mode support */
260
+ html[data-theme='dark'] .ipfs-image-error {
261
+ background-color: #2a2a2a;
262
+ color: #aaa;
263
+ }
264
+ `;
265
+
@@ -0,0 +1,398 @@
1
+ import { Component } from '@monygroupcorp/microact';
2
+ import { MessagePopup } from '../Util/MessagePopup.js';
3
+
4
+ export class ApprovalModal extends Component {
5
+ constructor(props) {
6
+ super(props);
7
+ console.log('[DEBUG] ApproveModal constructor called with amount:', props.amount, 'and address:', props.userAddress);
8
+
9
+ this.blockchainService = props.blockchainService;
10
+ this.eventBus = props.eventBus;
11
+ this.amount = props.amount;
12
+ this.userAddress = props.userAddress;
13
+
14
+ this.messagePopup = new MessagePopup('approve-status');
15
+ this.handleApprove = this.handleApprove.bind(this);
16
+ this.handleClose = this.handleClose.bind(this);
17
+ this.isClosing = false;
18
+ this.modalId = Math.random().toString(36).substring(2, 9);
19
+ console.log(`[DEBUG] ApproveModal instance created with ID: ${this.modalId}`);
20
+ }
21
+
22
+ onMount() {
23
+ console.log(`[DEBUG-${this.modalId}] ApproveModal mounted to DOM`);
24
+ super.onMount();
25
+ this.setupEventListeners();
26
+ }
27
+
28
+ setupEventListeners() {
29
+ const approveButton = this.element.querySelector('.approve-button');
30
+ const closeButton = this.element.querySelector('.approve-modal-close');
31
+ const overlay = this.element.querySelector('.approve-modal-overlay');
32
+
33
+ approveButton?.addEventListener('click', this.handleApprove);
34
+ closeButton?.addEventListener('click', this.handleClose);
35
+ overlay?.addEventListener('click', (e) => {
36
+ if (e.target === overlay) this.handleClose();
37
+ });
38
+ }
39
+
40
+ async handleApprove() {
41
+ console.log(`[DEBUG-${this.modalId}] Approve button clicked`);
42
+ try {
43
+ // Disable the button immediately to prevent multiple clicks
44
+ const approveButton = this.element.querySelector('.approve-button');
45
+ if (!approveButton) {
46
+ console.error(`[DEBUG-${this.modalId}] Approve button not found in the modal`);
47
+ return;
48
+ }
49
+
50
+ console.log(`[DEBUG-${this.modalId}] Approve button found, disabling and updating text`);
51
+ const statusMessage = this.element.querySelector('.status-message') ||
52
+ document.createElement('div');
53
+
54
+ if (!statusMessage.classList.contains('status-message')) {
55
+ statusMessage.className = 'status-message';
56
+ const modalContent = this.element.querySelector('.approve-modal-content');
57
+ if (modalContent) {
58
+ console.log(`[DEBUG-${this.modalId}] Adding status message to modal content`);
59
+ modalContent.appendChild(statusMessage);
60
+ } else {
61
+ console.error(`[DEBUG-${this.modalId}] Modal content not found`);
62
+ }
63
+ }
64
+
65
+ statusMessage.textContent = 'Waiting for wallet confirmation...';
66
+ statusMessage.className = 'status-message pending';
67
+
68
+ approveButton.disabled = true;
69
+ approveButton.textContent = 'Approving...';
70
+
71
+ // Get user address if not provided
72
+ if (!this.userAddress) {
73
+ console.log(`[DEBUG-${this.modalId}] No user address provided, attempting to get from signer`);
74
+ if (this.blockchainService && this.blockchainService.signer) {
75
+ try {
76
+ this.userAddress = await this.blockchainService.signer.getAddress();
77
+ console.log(`[DEBUG-${this.modalId}] Retrieved user address for approval: ${this.userAddress}`);
78
+ } catch (addressError) {
79
+ console.error(`[DEBUG-${this.modalId}] Failed to get user address for approval:`, addressError);
80
+ throw new Error('Could not get wallet address for approval. Please reconnect your wallet.');
81
+ }
82
+ } else {
83
+ console.error(`[DEBUG-${this.modalId}] No blockchain service or signer available`);
84
+ throw new Error('No wallet connected. Please connect your wallet first.');
85
+ }
86
+ } else {
87
+ console.log(`[DEBUG-${this.modalId}] Using provided user address: ${this.userAddress}`);
88
+ }
89
+
90
+ // Format token amount with 18 decimals
91
+ console.log(`[DEBUG-${this.modalId}] Parsing amount: ${this.amount}`);
92
+ const parsedAmount = this.blockchainService.parseExec(this.amount);
93
+ console.log(`[DEBUG-${this.modalId}] Parsed amount: ${parsedAmount}`);
94
+
95
+ // Get router address
96
+ const routerAddress = this.blockchainService.swapRouter?.address || this.blockchainService.swapRouter;
97
+ console.log(`[DEBUG-${this.modalId}] Router address for approval: ${routerAddress}`);
98
+
99
+ // Call the standard setApproval method
100
+ console.log(`[DEBUG-${this.modalId}] Approving ${this.amount} EXEC tokens from ${this.userAddress} to ${routerAddress}`);
101
+
102
+ statusMessage.textContent = 'Transaction submitted, waiting for confirmation...';
103
+
104
+ // Send the approval transaction
105
+ console.log(`[DEBUG-${this.modalId}] Calling blockchainService.setApproval`);
106
+ try {
107
+ const approvalResult = await this.blockchainService.setApproval(routerAddress, parsedAmount);
108
+ console.log(`[DEBUG-${this.modalId}] Approval transaction result:`, approvalResult);
109
+ } catch (txError) {
110
+ console.error(`[DEBUG-${this.modalId}] Transaction error:`, txError);
111
+ throw txError;
112
+ }
113
+
114
+ // Update status message
115
+ statusMessage.textContent = 'Approval successful!';
116
+ statusMessage.className = 'status-message success';
117
+
118
+ // Wait briefly to show success message
119
+ console.log(`[DEBUG-${this.modalId}] Approval successful, waiting before emitting event`);
120
+ await new Promise(resolve => setTimeout(resolve, 1500));
121
+
122
+ // Emit success event
123
+ console.log(`[DEBUG-${this.modalId}] Emitting approve:complete event`);
124
+ this.eventBus.emit('approve:complete');
125
+
126
+ // Close modal
127
+ console.log(`[DEBUG-${this.modalId}] Calling handleClose after successful approval`);
128
+ this.handleClose();
129
+
130
+ } catch (error) {
131
+ console.error(`[DEBUG-${this.modalId}] Approval failed:`, error);
132
+
133
+ let errorMessage = error.message;
134
+ if (errorMessage.includes('Contract call')) {
135
+ const parts = errorMessage.split(': ');
136
+ errorMessage = parts[parts.length - 1];
137
+ }
138
+
139
+ // Update status message in the modal
140
+ const statusMessage = this.element.querySelector('.status-message') ||
141
+ document.createElement('div');
142
+
143
+ if (!statusMessage.classList.contains('status-message')) {
144
+ statusMessage.className = 'status-message';
145
+ const modalContent = this.element.querySelector('.approve-modal-content');
146
+ if (modalContent) {
147
+ modalContent.appendChild(statusMessage);
148
+ }
149
+ }
150
+
151
+ statusMessage.textContent = `Error: ${errorMessage}`;
152
+ statusMessage.className = 'status-message error';
153
+
154
+ this.messagePopup.error(
155
+ `Approval Failed: ${errorMessage}`,
156
+ 'Transaction Failed'
157
+ );
158
+
159
+ // Re-enable button
160
+ const approveButton = this.element.querySelector('.approve-button');
161
+ if (approveButton) {
162
+ approveButton.disabled = false;
163
+ approveButton.textContent = 'Approve';
164
+ }
165
+ }
166
+ }
167
+
168
+ handleClose() {
169
+ console.log(`[DEBUG-${this.modalId}] handleClose called`);
170
+ // Prevent multiple close operations
171
+ if (this.isClosing) {
172
+ console.log(`[DEBUG-${this.modalId}] Already closing, skipping`);
173
+ return;
174
+ }
175
+ this.isClosing = true;
176
+
177
+ console.log(`[DEBUG-${this.modalId}] Closing approval modal`);
178
+
179
+ try {
180
+ // Remove the modal from the DOM
181
+ if (this.element && this.element.parentNode) {
182
+ console.log(`[DEBUG-${this.modalId}] Removing modal from DOM`);
183
+ this.element.parentNode.removeChild(this.element);
184
+ } else {
185
+ console.warn(`[DEBUG-${this.modalId}] Modal element or parent not found during close`);
186
+ }
187
+
188
+ // Emit a closed event
189
+ console.log(`[DEBUG-${this.modalId}] Emitting approveModal:closed event`);
190
+ this.eventBus.emit('approveModal:closed');
191
+
192
+ // Clean up any resources
193
+ console.log(`[DEBUG-${this.modalId}] Calling dispose method`);
194
+ this.dispose();
195
+ } catch (error) {
196
+ console.error(`[DEBUG-${this.modalId}] Error closing approval modal:`, error);
197
+ }
198
+ }
199
+
200
+ // Properly dispose of the component
201
+ dispose() {
202
+ console.log(`[DEBUG-${this.modalId}] Disposing component resources`);
203
+
204
+ // Remove event listeners
205
+ const approveButton = this.element?.querySelector('.approve-button');
206
+ const closeButton = this.element?.querySelector('.approve-modal-close');
207
+ const overlay = this.element?.querySelector('.approve-modal-overlay');
208
+
209
+ approveButton?.removeEventListener('click', this.handleApprove);
210
+ closeButton?.removeEventListener('click', this.handleClose);
211
+
212
+ // Clear references
213
+ this.isClosing = true;
214
+ this.blockchainService = null;
215
+ this.userAddress = null;
216
+ console.log(`[DEBUG-${this.modalId}] Component disposed`);
217
+ }
218
+
219
+ show() {
220
+ console.log(`[DEBUG-${this.modalId}] Show method called`);
221
+ this.isClosing = false;
222
+ this.element.style.display = 'block';
223
+ console.log(`[DEBUG-${this.modalId}] Modal set to display:block`);
224
+ }
225
+
226
+ hide() {
227
+ console.log(`[DEBUG-${this.modalId}] Hide method called`);
228
+ this.element.style.display = 'none';
229
+ }
230
+
231
+ events() {
232
+ console.log(`[DEBUG-${this.modalId}] Setting up event handlers`);
233
+ return {
234
+ 'click .approve-button': (e) => {
235
+ console.log(`[DEBUG-${this.modalId}] Approve button clicked, calling handleApprove`);
236
+ this.handleApprove();
237
+ },
238
+ 'click .approve-modal-close': (e) => {
239
+ console.log(`[DEBUG-${this.modalId}] Close button clicked, calling handleClose`);
240
+ this.handleClose();
241
+ },
242
+ 'click .approve-modal-overlay': (e) => {
243
+ console.log(`[DEBUG-${this.modalId}] Overlay clicked`, e.target, e.currentTarget);
244
+ if (e.target === e.currentTarget) {
245
+ console.log(`[DEBUG-${this.modalId}] Overlay direct click detected, calling handleClose`);
246
+ this.handleClose();
247
+ }
248
+ }
249
+ };
250
+ }
251
+
252
+ render() {
253
+ // Get router address directly from the blockchain service instead of the store
254
+ const routerAddress = this.blockchainService.swapRouter?.address || this.blockchainService.swapRouter;
255
+ const formattedAmount = parseInt(this.amount).toLocaleString();
256
+
257
+ return `
258
+ <div class="approve-modal-overlay">
259
+ <div class="approve-modal">
260
+ <button class="approve-modal-close">&times;</button>
261
+ <div class="approve-modal-content">
262
+ <h2>Approve Router</h2>
263
+ <p>Before selling your $EXEC tokens, you need to approve the router contract to spend them.</p>
264
+
265
+ <div class="approve-details">
266
+ <div class="approve-info">
267
+ <span class="label">Amount to Approve:</span>
268
+ <span class="value">${formattedAmount} $EXEC</span>
269
+ </div>
270
+ <div class="approve-info">
271
+ <span class="label">Router Address:</span>
272
+ <span class="value">${routerAddress}</span>
273
+ </div>
274
+ </div>
275
+
276
+ <button class="approve-button">
277
+ Approve
278
+ </button>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ `;
283
+ }
284
+
285
+ static get styles() {
286
+ return `
287
+ .approve-modal-overlay {
288
+ position: fixed;
289
+ top: 0;
290
+ left: 0;
291
+ right: 0;
292
+ bottom: 0;
293
+ background-color: rgba(0, 0, 0, 0.75);
294
+ display: flex;
295
+ justify-content: center;
296
+ align-items: center;
297
+ z-index: 1000;
298
+ }
299
+
300
+ .approve-modal {
301
+ background-color: #111;
302
+ border-radius: 8px;
303
+ padding: 24px;
304
+ position: relative;
305
+ width: 90%;
306
+ max-width: 500px;
307
+ }
308
+
309
+ .approve-modal-close {
310
+ position: absolute;
311
+ top: 16px;
312
+ right: 16px;
313
+ background: none;
314
+ border: none;
315
+ font-size: 24px;
316
+ cursor: pointer;
317
+ color: #fff;
318
+ }
319
+
320
+ .approve-modal h2 {
321
+ margin: 0 0 16px 0;
322
+ color: #fff;
323
+ }
324
+
325
+ .approve-details {
326
+ background-color: #1a1a1a;
327
+ border-radius: 8px;
328
+ padding: 16px;
329
+ margin: 16px 0;
330
+ }
331
+
332
+ .approve-info {
333
+ display: flex;
334
+ justify-content: space-between;
335
+ margin-bottom: 8px;
336
+ word-break: break-all;
337
+ }
338
+
339
+ .approve-info:last-child {
340
+ margin-bottom: 0;
341
+ }
342
+
343
+ .approve-info .label {
344
+ color: #888;
345
+ margin-right: 16px;
346
+ }
347
+
348
+ .approve-info .value {
349
+ color: #fff;
350
+ text-align: right;
351
+ }
352
+
353
+ .approve-button {
354
+ width: 100%;
355
+ padding: 12px;
356
+ background-color: #007bff;
357
+ color: white;
358
+ border: none;
359
+ border-radius: 4px;
360
+ cursor: pointer;
361
+ font-size: 16px;
362
+ margin-top: 16px;
363
+ }
364
+
365
+ .approve-button:disabled {
366
+ background-color: #555;
367
+ cursor: not-allowed;
368
+ }
369
+
370
+ .approve-button:hover:not(:disabled) {
371
+ background-color: #0056b3;
372
+ }
373
+
374
+ .status-message {
375
+ margin-top: 12px;
376
+ padding: 10px;
377
+ border-radius: 4px;
378
+ text-align: center;
379
+ font-size: 14px;
380
+ }
381
+
382
+ .status-message.pending {
383
+ background-color: #2c3e50;
384
+ color: #f1c40f;
385
+ }
386
+
387
+ .status-message.success {
388
+ background-color: #27ae60;
389
+ color: white;
390
+ }
391
+
392
+ .status-message.error {
393
+ background-color: #c0392b;
394
+ color: white;
395
+ }
396
+ `;
397
+ }
398
+ }export default ApprovalModal;