@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,972 @@
1
+ import { Component } from '@monygroupcorp/microact';
2
+ import { TransactionOptions } from './TransactionOptions.js';
3
+ import { MessagePopup } from '../Util/MessagePopup.js';
4
+ import { PriceDisplay } from '../Display/PriceDisplay.js';
5
+ import { ApprovalModal } from '../Modal/ApprovalModal.js';
6
+ import { SwapInputs } from './SwapInputs.js';
7
+ import SwapButton from './SwapButton.js';
8
+
9
+ export class SwapInterface extends Component {
10
+ constructor(props) {
11
+ super(props);
12
+
13
+ this.blockchainService = props.blockchainService;
14
+ this.eventBus = props.eventBus;
15
+
16
+ this.state = {
17
+ direction: props.direction || 'buy',
18
+ ethAmount: '',
19
+ execAmount: '',
20
+ activeInput: null,
21
+ freeMint: props.freeMint || false,
22
+ freeSupply: props.freeSupply || 0,
23
+ calculatingAmount: false,
24
+ isPhase2: props.isPhase2 === null ? null : props.isPhase2, // null means phase is not yet determined
25
+ dataReady: props.dataReady || false,
26
+ balances: props.balances || { eth: '0', exec: '0' },
27
+ price: props.price || { current: 0 },
28
+ contractData: props.contractData || {},
29
+ };
30
+
31
+ // Store the address - could be a promise or a direct value
32
+ this._address = props.address || null;
33
+
34
+ // Initialize child components
35
+ this.transactionOptions = new TransactionOptions();
36
+ this.messagePopup = new MessagePopup('status-message');
37
+ this.priceDisplay = new PriceDisplay({ price: this.state.price, contractData: this.state.contractData });
38
+ this.messagePopup.initialize();
39
+
40
+ this.swapInputs = null; // Initialized in initializeChildComponents
41
+
42
+ this.swapButton = new SwapButton({
43
+ direction: this.state.direction,
44
+ disabled: false,
45
+ onClick: this.handleSwap.bind(this)
46
+ });
47
+
48
+ this.calculateTimer = null;
49
+
50
+ // Bind event handlers that are passed in props
51
+ this.onDirectionChange = props.onDirectionChange;
52
+
53
+ this.handleTransactionEvents = this.handleTransactionEvents.bind(this);
54
+ this.handleBalanceUpdate = this.handleBalanceUpdate.bind(this);
55
+ this.handleTransactionOptionsUpdate = this.handleTransactionOptionsUpdate.bind(this);
56
+
57
+ this.transactionOptionsState = {
58
+ message: '',
59
+ nftMintingEnabled: false
60
+ };
61
+
62
+ this.approveModal = null;
63
+ this.eventListeners = [];
64
+ this.instanceId = Math.random().toString(36).substring(2, 9);
65
+ console.log(`🔵 SwapInterface instance created: ${this.instanceId}`);
66
+ }
67
+
68
+ // Add new method to handle balance updates
69
+ handleBalanceUpdate(update) {
70
+ // Update state from the event detail or props
71
+ const newBalances = update.balances || this.props.balances;
72
+ const newFreeMint = update.freeMint !== undefined ? update.freeMint : this.props.freeMint;
73
+ const newFreeSupply = update.freeSupply !== undefined ? update.freeSupply : this.props.freeSupply;
74
+
75
+ this.setState({
76
+ balances: newBalances,
77
+ freeMint: newFreeMint,
78
+ freeSupply: newFreeSupply
79
+ });
80
+
81
+ // Update swap inputs component with new balance info directly
82
+ if (this.swapInputs) {
83
+ this.swapInputs.updateProps({
84
+ freeMint: newFreeMint
85
+ });
86
+ }
87
+ }
88
+
89
+ updateElements() {
90
+ // Update child components with new props
91
+ // Use requestAnimationFrame to batch updates and prevent multiple renders
92
+ requestAnimationFrame(() => {
93
+ if (this.swapInputs) {
94
+ this.swapInputs.updateProps({
95
+ direction: this.state.direction,
96
+ ethAmount: this.state.ethAmount,
97
+ execAmount: this.state.execAmount,
98
+ calculatingAmount: this.state.calculatingAmount,
99
+ freeMint: this.state.freeMint,
100
+ isPhase2: this.state.isPhase2
101
+ });
102
+ }
103
+
104
+ if (this.swapButton) {
105
+ this.swapButton.updateProps({
106
+ direction: this.state.direction
107
+ });
108
+ }
109
+ });
110
+ }
111
+
112
+ async calculateSwapAmount(amount, inputType) {
113
+ // Handle empty or invalid input
114
+ if (!amount || isNaN(parseFloat(amount))) {
115
+ return '';
116
+ }
117
+
118
+ try {
119
+ if (this.isLiquidityDeployed()) {
120
+ // Phase 2: Use Uniswap-style calculations
121
+ const price = this.state.price.current;
122
+ console.log('calculateSwapAmount price', price);
123
+
124
+ if (inputType === 'eth') {
125
+ // Calculate EXEC amount based on ETH input
126
+ const ethAmount = parseFloat(amount);
127
+ // Apply a 5% reduction to account for 4% tax + slippage
128
+ const execAmount = (ethAmount / price * 1000000) * 0.95;
129
+ console.log('calculateSwapAmount execAmount', execAmount);
130
+ return execAmount.toFixed(0); // Use integer amounts for EXEC
131
+ } else {
132
+ // Calculate ETH amount based on EXEC input
133
+ const execAmount = parseFloat(amount);
134
+ // Add a 5.5% buffer for 4% tax + slippage + price impact
135
+ const ethAmount = (execAmount / 1000000) * price * 1.055;
136
+ console.log('calculateSwapAmount ethAmount', ethAmount);
137
+ return ethAmount.toFixed(6);
138
+ }
139
+ } else {
140
+ // Phase 1: Use bonding curve logic
141
+ if (inputType === 'eth') {
142
+ // Calculate how much EXEC user will receive for their ETH
143
+ const execAmount = await this.blockchainService.getExecForEth(amount);
144
+
145
+ // Check if user is eligible for free mint
146
+ const { freeSupply, freeMint } = this.state;
147
+ console.log('calculateSwapAmount freeMint', freeMint);
148
+ // If free supply is available and user hasn't claimed their free mint
149
+ const freeMintBonus = (freeSupply > 0 && !freeMint) ? 1000000 : 0;
150
+
151
+ // Round down to ensure we don't exceed maxCost
152
+ return Math.floor(execAmount + freeMintBonus).toString();
153
+ } else {
154
+ // Calculate how much ETH user will receive for their EXEC
155
+ const ethAmount = await this.blockchainService.getEthForExec(amount);
156
+ return ethAmount.toString(); // Use more decimals for precision
157
+ }
158
+ }
159
+ } catch (error) {
160
+ console.error('Error calculating swap amount:', error);
161
+ return '';
162
+ }
163
+ }
164
+
165
+ onMount() {
166
+ console.log(`[${this.instanceId}] SwapInterface onMount called`);
167
+ this.bindEvents();
168
+
169
+ const eventSubscriptions = [
170
+ ['contractData:updated', this.handleContractDataUpdate.bind(this), 'high'],
171
+ ['transaction:pending', this.handleTransactionEvents, 'normal'],
172
+ ['transaction:confirmed', this.handleTransactionEvents, 'normal'],
173
+ ['transaction:success', this.handleTransactionEvents, 'normal'],
174
+ ['transaction:error', this.handleTransactionEvents, 'normal'],
175
+ ['balances:updated', this.handleBalanceUpdate, 'high'],
176
+ ['transactionOptions:update', this.handleTransactionOptionsUpdate, 'low']
177
+ ];
178
+
179
+ this.eventListeners = eventSubscriptions.map(([event, handler, priority]) => {
180
+ console.log(`[${this.instanceId}] Subscribing to ${event} with priority ${priority}`);
181
+ return this.eventBus.on(event, handler);
182
+ });
183
+
184
+ // Check if we already have data and can determine phase immediately
185
+ const { contractData } = this.state;
186
+ if (contractData && contractData.liquidityPool !== undefined) {
187
+ const isPhase2 = this.isLiquidityDeployed();
188
+ this.setState({
189
+ isPhase2: isPhase2,
190
+ dataReady: true
191
+ });
192
+ this.initializeChildComponents();
193
+ }
194
+
195
+ this.update();
196
+
197
+ requestAnimationFrame(() => {
198
+ this.mountChildComponents();
199
+ });
200
+ }
201
+
202
+ updateProps(newProps) {
203
+ // Update state based on new props from parent
204
+ const oldState = { ...this.state };
205
+ const newState = {
206
+ ...this.state,
207
+ ...newProps
208
+ };
209
+
210
+ if (this.shouldUpdate(oldState, newState)) {
211
+ this.setState(newState);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Initialize child components that depend on phase
217
+ * This is called once phase is determined (either in onMount or handleContractDataUpdate)
218
+ */
219
+ initializeChildComponents() {
220
+ // Only initialize if phase is known and SwapInputs hasn't been created yet
221
+ if (this.state.isPhase2 === null) {
222
+ console.log(`[${this.instanceId}] Phase not yet determined, skipping SwapInputs initialization`);
223
+ return;
224
+ }
225
+
226
+ if (!this.swapInputs) {
227
+ console.log(`[${this.instanceId}] Initializing SwapInputs with phase ${this.state.isPhase2 ? '2' : '1'}`);
228
+ this.swapInputs = new SwapInputs({
229
+ direction: this.state.direction,
230
+ ethAmount: this.state.ethAmount,
231
+ execAmount: this.state.execAmount,
232
+ calculatingAmount: this.state.calculatingAmount,
233
+ freeMint: this.state.freeMint,
234
+ isPhase2: this.state.isPhase2,
235
+ onInput: this.handleInput.bind(this)
236
+ });
237
+ }
238
+ }
239
+
240
+ // Add method to explicitly mount child components
241
+ mountChildComponents() {
242
+ // Mount price display
243
+ const priceContainer = this.element.querySelector('.price-display-container');
244
+ if (priceContainer && (!this.priceDisplay.element || !priceContainer.contains(this.priceDisplay.element))) {
245
+ console.log(`[${this.instanceId}] Mounting PriceDisplay component`);
246
+ this.priceDisplay.mount(priceContainer);
247
+ }
248
+
249
+ // Mount transaction options
250
+ const optionsContainer = this.element.querySelector('.transaction-options-container');
251
+ if (optionsContainer && this.transactionOptions &&
252
+ (!this.transactionOptions.element || !optionsContainer.contains(this.transactionOptions.element))) {
253
+ console.log(`[${this.instanceId}] Mounting TransactionOptions component`);
254
+ this.transactionOptions.mount(optionsContainer);
255
+ }
256
+
257
+ // Mount swap inputs
258
+ const inputsContainer = this.element.querySelector('.swap-inputs-container');
259
+ if (inputsContainer && this.swapInputs) {
260
+ this.swapInputs.mount(inputsContainer);
261
+ }
262
+
263
+ // Mount quick fill buttons
264
+ const quickFillContainer = this.element.querySelector('.quick-fill-buttons-container');
265
+ if (quickFillContainer) {
266
+ // Create a wrapper for just the quick fill buttons
267
+ quickFillContainer.innerHTML = '<div class="quick-fill-buttons"></div>';
268
+ const quickFillButtons = quickFillContainer.querySelector('.quick-fill-buttons');
269
+ if (quickFillButtons) {
270
+ // Render quick fill buttons directly
271
+ const { direction } = this.state;
272
+ quickFillButtons.innerHTML = direction === 'buy' ?
273
+ `<button data-amount="0.0025">0.0025</button>
274
+ <button data-amount="0.01">0.01</button>
275
+ <button data-amount="0.05">0.05</button>
276
+ <button data-amount="0.1">0.1</button>`
277
+ :
278
+ `<button data-percentage="25">25%</button>
279
+ <button data-percentage="50">50%</button>
280
+ <button data-percentage="75">75%</button>
281
+ <button data-percentage="100">100%</button>`;
282
+
283
+ // Attach event listeners
284
+ quickFillButtons.addEventListener('click', (e) => {
285
+ if (e.target.dataset.amount || e.target.dataset.percentage) {
286
+ this.handleQuickFill(e);
287
+ }
288
+ });
289
+ }
290
+ }
291
+
292
+ // Mount direction switch in the slot within swap-inputs
293
+ const directionSwitchSlot = this.element.querySelector('.direction-switch-slot');
294
+ if (directionSwitchSlot) {
295
+ directionSwitchSlot.innerHTML = '<button class="direction-switch">↑↓</button>';
296
+ directionSwitchSlot.querySelector('.direction-switch').addEventListener('click', (e) => {
297
+ this.handleDirectionSwitch(e);
298
+ });
299
+ }
300
+
301
+ // Mount swap button
302
+ const buttonContainer = this.element.querySelector('.swap-button-container');
303
+ if (buttonContainer && this.swapButton) {
304
+ this.swapButton.mount(buttonContainer);
305
+ }
306
+ }
307
+
308
+ onUnmount() {
309
+ console.log(`[${this.instanceId}] SwapInterface onUnmount called`);
310
+ // Unsubscribe from all events
311
+ this.eventListeners.forEach(unsubscribe => {
312
+ if (typeof unsubscribe === 'function') {
313
+ unsubscribe();
314
+ }
315
+ });
316
+
317
+ // Clear the list
318
+ this.eventListeners = [];
319
+
320
+ // Remove child components
321
+ if (this.transactionOptions && this.transactionOptions.element) {
322
+ this.transactionOptions.element.remove();
323
+ }
324
+
325
+ // Ensure price display is cleaned up
326
+ if (this.priceDisplay) {
327
+ try {
328
+ this.priceDisplay.unmount();
329
+ } catch (e) {
330
+ console.warn('Error unmounting price display:', e);
331
+ }
332
+ this.priceDisplay = null;
333
+ }
334
+
335
+ // Unmount sub-components
336
+ if (this.swapInputs) {
337
+ try {
338
+ this.swapInputs.unmount();
339
+ } catch (e) {
340
+ console.warn('Error unmounting swap inputs:', e);
341
+ }
342
+ this.swapInputs = null;
343
+ }
344
+
345
+ // Quick fill and direction switch are handled inline, no unmount needed
346
+
347
+ if (this.swapButton) {
348
+ try {
349
+ this.swapButton.unmount();
350
+ } catch (e) {
351
+ console.warn('Error unmounting swap button:', e);
352
+ }
353
+ this.swapButton = null;
354
+ }
355
+
356
+ // Make sure to close and clean up any open approval modal
357
+ if (this.approveModal) {
358
+ try {
359
+ // Close the modal - this will trigger the approveModal:closed event
360
+ // which will clean up related event listeners
361
+ this.approveModal.handleClose();
362
+ } catch (e) {
363
+ console.warn('Error closing approval modal during unmount:', e);
364
+ }
365
+ this.approveModal = null;
366
+ }
367
+
368
+ // Clear any pending timers
369
+ if (this.calculateTimer) {
370
+ clearTimeout(this.calculateTimer);
371
+ this.calculateTimer = null;
372
+ }
373
+ }
374
+
375
+ handleTransactionEvents(event) {
376
+ console.log(`[${this.instanceId}] handleTransactionEvents called with:`, {
377
+ type: event?.type,
378
+ hasError: !!event?.error,
379
+ hasHash: !!event?.hash,
380
+ eventId: event?.id || 'none',
381
+ handled: event?.handled || false
382
+ });
383
+
384
+ // Skip if this event has already been handled by this instance
385
+ if (event?.handledBy?.includes(this.instanceId)) {
386
+ console.log(`[${this.instanceId}] Event already handled by this instance, skipping`);
387
+ return;
388
+ }
389
+
390
+ // Mark this event as handled by this instance
391
+ if (!event.handledBy) {
392
+ event.handledBy = [];
393
+ }
394
+ event.handledBy.push(this.instanceId);
395
+
396
+ const direction = this.state.direction === 'buy' ? 'Buy' : 'Sell';
397
+
398
+ // Check if this is a transaction event
399
+ if (!event || !event.type) {
400
+ console.warn('Invalid transaction event:', event);
401
+ return;
402
+ }
403
+
404
+ // For transaction events - only show if it's not an error
405
+ if ((event.type === 'buy' || event.type === 'sell' || event.type === 'swap') && !event.error) {
406
+ console.log(`[${this.instanceId}] Showing transaction pending message for type:`, event.type);
407
+ this.messagePopup.info(
408
+ `${direction} transaction. Simulating...`,
409
+ 'Transaction Pending'
410
+ );
411
+ }
412
+
413
+ // For confirmed transactions
414
+ if (event.hash) {
415
+ this.messagePopup.info(
416
+ `Transaction confirmed, waiting for completion...`,
417
+ 'Transaction Confirmed'
418
+ );
419
+ }
420
+
421
+ // For successful transactions
422
+ if (event.receipt && (event.type === 'buy' || event.type === 'sell' || event.type === 'swap')) {
423
+ const amount = this.state.direction === 'buy'
424
+ ? this.state.execAmount + ' EXEC'
425
+ : this.state.ethAmount + ' ETH';
426
+
427
+ this.messagePopup.success(
428
+ `Successfully ${direction.toLowerCase() === 'buy' ? 'bought' : 'sold'} ${amount}`,
429
+ 'Transaction Complete'
430
+ );
431
+
432
+ // Clear inputs after successful transaction
433
+ this.state.ethAmount = '';
434
+ this.state.execAmount = '';
435
+ this.state.calculatingAmount = false;
436
+
437
+ // Update child components directly
438
+ if (this.swapInputs) {
439
+ this.swapInputs.updateProps({
440
+ ethAmount: '',
441
+ execAmount: '',
442
+ calculatingAmount: false
443
+ });
444
+ }
445
+
446
+ // Re-mount child components after state update
447
+ this.mountChildComponents();
448
+ }
449
+
450
+ // For error transactions
451
+ if (event.error && !event.handled) {
452
+ console.log(`[${this.instanceId}] Handling error in handleTransactionEvents:`, event.error);
453
+
454
+ let errorMessage = event.error?.message || 'Transaction failed';
455
+
456
+ if (errorMessage.includes('Contract call')) {
457
+ const parts = errorMessage.split(': ');
458
+ errorMessage = parts[parts.length - 1];
459
+ }
460
+
461
+ const context = this.state.direction === 'buy' ?
462
+ 'Buy Failed' :
463
+ 'Sell Failed';
464
+
465
+ this.messagePopup.error(
466
+ `${context}: ${errorMessage}`,
467
+ 'Transaction Failed'
468
+ );
469
+
470
+ event.handled = true;
471
+ }
472
+ }
473
+
474
+ handleTransactionOptionsUpdate(options) {
475
+ this.transactionOptionsState = {
476
+ message: options.message,
477
+ nftMintingEnabled: options.nftMintingEnabled
478
+ };
479
+ }
480
+
481
+ handleInput(inputType, value) {
482
+ // Clear any existing timer
483
+ if (this.calculateTimer) {
484
+ clearTimeout(this.calculateTimer);
485
+ }
486
+
487
+ // Update state directly without triggering re-render
488
+ // We'll update the child components directly instead
489
+ this.state.activeInput = inputType;
490
+ this.state.calculatingAmount = true;
491
+
492
+ if (this.state.direction === 'buy') {
493
+ if (inputType === 'top') {
494
+ this.state.ethAmount = value;
495
+ } else {
496
+ this.state.execAmount = value;
497
+ }
498
+ } else {
499
+ if (inputType === 'top') {
500
+ this.state.execAmount = value;
501
+ } else {
502
+ this.state.ethAmount = value;
503
+ }
504
+ }
505
+
506
+ // Update child components directly without triggering parent re-render
507
+ if (this.swapInputs) {
508
+ this.swapInputs.updateProps({
509
+ direction: this.state.direction,
510
+ ethAmount: this.state.ethAmount,
511
+ execAmount: this.state.execAmount,
512
+ calculatingAmount: this.state.calculatingAmount,
513
+ freeMint: this.state.freeMint,
514
+ isPhase2: this.state.isPhase2
515
+ });
516
+ }
517
+
518
+ // Set debounced calculation
519
+ this.calculateTimer = setTimeout(async () => {
520
+ try {
521
+ const isEthInput = (this.state.direction === 'buy') === (inputType === 'top');
522
+ const calculatedAmount = await this.calculateSwapAmount(value, isEthInput ? 'eth' : 'exec');
523
+
524
+ // Update the opposite input after calculation
525
+ if (isEthInput) {
526
+ this.state.execAmount = calculatedAmount;
527
+ this.state.calculatingAmount = false;
528
+
529
+ // Update child component directly
530
+ if (this.swapInputs) {
531
+ this.swapInputs.updateProps({
532
+ execAmount: calculatedAmount,
533
+ calculatingAmount: false
534
+ });
535
+ }
536
+ } else {
537
+ this.state.ethAmount = calculatedAmount;
538
+ this.state.calculatingAmount = false;
539
+
540
+ // Update child component directly
541
+ if (this.swapInputs) {
542
+ this.swapInputs.updateProps({
543
+ ethAmount: calculatedAmount,
544
+ calculatingAmount: false
545
+ });
546
+ }
547
+ }
548
+ } catch (error) {
549
+ console.error('Error calculating swap amount:', error);
550
+ this.state.calculatingAmount = false;
551
+
552
+ // Update child component directly
553
+ if (this.swapInputs) {
554
+ this.swapInputs.updateProps({
555
+ calculatingAmount: false
556
+ });
557
+ }
558
+ }
559
+ }, 750);
560
+ }
561
+
562
+ events() {
563
+ // Events are now handled by child components
564
+ return {};
565
+ }
566
+
567
+ /**
568
+ * Override shouldUpdate to prevent re-renders on input changes
569
+ * Input changes are handled by child components directly
570
+ */
571
+ shouldUpdate(oldState, newState) {
572
+ if (!oldState || !newState) return true;
573
+ if (oldState === newState) return false;
574
+
575
+ // Don't update if only input values changed (ethAmount, execAmount, activeInput, calculatingAmount)
576
+ // These are handled by child components directly
577
+ const inputOnlyChanges =
578
+ oldState.ethAmount !== newState.ethAmount ||
579
+ oldState.execAmount !== newState.execAmount ||
580
+ oldState.activeInput !== newState.activeInput ||
581
+ oldState.calculatingAmount !== newState.calculatingAmount;
582
+
583
+ // If only input values changed, don't re-render (child components handle it)
584
+ if (inputOnlyChanges &&
585
+ oldState.direction === newState.direction &&
586
+ oldState.freeMint === newState.freeMint &&
587
+ oldState.freeSupply === newState.freeSupply &&
588
+ oldState.isPhase2 === newState.isPhase2 &&
589
+ oldState.dataReady === newState.dataReady) {
590
+ return false;
591
+ }
592
+
593
+ // Update for other state changes (direction, phase, dataReady, etc.)
594
+ return true;
595
+ }
596
+
597
+ handleDirectionSwitch(e) {
598
+ // Prevent default button behavior and stop propagation
599
+ if (e) {
600
+ e.preventDefault();
601
+ e.stopPropagation();
602
+ }
603
+
604
+ // Clear any pending calculations
605
+ if (this.calculateTimer) {
606
+ clearTimeout(this.calculateTimer);
607
+ }
608
+
609
+ const newDirection = this.state.direction === 'buy' ? 'sell' : 'buy';
610
+
611
+ console.log('Direction Switch - Current State:', {
612
+ direction: this.state.direction,
613
+ newDirection,
614
+ freeMint: this.state.freeMint,
615
+ freeSupply: this.state.freeSupply
616
+ });
617
+
618
+ // Use setState instead of directly modifying state
619
+ this.setState({
620
+ direction: newDirection,
621
+ calculatingAmount: false,
622
+ activeInput: null
623
+ });
624
+
625
+ // Notify parent component of the change
626
+ if (this.onDirectionChange) {
627
+ this.onDirectionChange(newDirection);
628
+ }
629
+
630
+ // Update child components with new direction
631
+ requestAnimationFrame(() => {
632
+ this.mountChildComponents();
633
+ });
634
+ }
635
+
636
+ isLiquidityDeployed() {
637
+ const { contractData } = this.state;
638
+ if (!contractData || !contractData.liquidityPool) {
639
+ return false;
640
+ }
641
+ const result = contractData.liquidityPool !== '0x0000000000000000000000000000000000000000';
642
+ console.log('isLiquidityDeployed check:', {
643
+ liquidityPool: contractData.liquidityPool,
644
+ result: result
645
+ });
646
+ return result;
647
+ }
648
+
649
+ async handleSwap() {
650
+ try {
651
+ // Validate inputs
652
+ if (this.state.calculatingAmount) {
653
+ this.messagePopup.info('Please wait for the calculation to complete', 'Loading');
654
+ return;
655
+ }
656
+
657
+ const { ethAmount, execAmount, direction, balances, freeMint } = this.state;
658
+
659
+ if (!ethAmount || !execAmount || parseFloat(ethAmount) <= 0 || parseFloat(execAmount) <= 0) {
660
+ this.messagePopup.info('Please enter valid amounts', 'Invalid Input');
661
+ return;
662
+ }
663
+
664
+ // Check if user has enough balance
665
+ if (direction === 'buy') {
666
+ let ethBalance = parseFloat(this.blockchainService.formatEther(balances.eth || '0'));
667
+ const ethNeeded = parseFloat(ethAmount);
668
+
669
+ if (isNaN(ethNeeded) || isNaN(ethBalance) || ethNeeded > ethBalance) {
670
+ this.messagePopup.info(`Not enough ETH balance. You have ${ethBalance.toFixed(6)} ETH, need ${ethNeeded} ETH`, 'Insufficient Balance');
671
+ return;
672
+ }
673
+ } else {
674
+ const execAmountClean = execAmount.replace(/,/g, '');
675
+ const execBalance = BigInt(balances.exec || 0);
676
+ const execNeeded = BigInt(parseInt(execAmountClean) || 0);
677
+
678
+ if (execNeeded > execBalance) {
679
+ const execBalanceFormatted = parseInt(balances.exec || 0).toLocaleString();
680
+ const execNeededFormatted = parseInt(execAmountClean).toLocaleString();
681
+ this.messagePopup.info(`Not enough EXEC balance. You have ${execBalanceFormatted} EXEC, need ${execNeededFormatted} EXEC`, 'Insufficient Balance');
682
+ return;
683
+ }
684
+ }
685
+
686
+ // Check if a free mint token is being sold
687
+ if (direction === 'sell' && parseInt(execAmount.replace(/,/g, '')) <= 1000000 && freeMint) {
688
+ this.messagePopup.info('Free minted tokens cannot be sold directly.', 'Free Mint Restriction');
689
+ return;
690
+ }
691
+
692
+ const isLiquidityDeployed = this.isLiquidityDeployed();
693
+ const cleanExecAmount = this.state.execAmount.replace(/,/g, '');
694
+
695
+ if (isLiquidityDeployed) {
696
+ const ethValue = this.blockchainService.parseEther(this.state.ethAmount);
697
+ const execAmountBI = this.blockchainService.parseExec(cleanExecAmount);
698
+ const address = await this.getAddress();
699
+
700
+ if (!address) {
701
+ this.messagePopup.error('No wallet address available. Please reconnect your wallet.', 'Wallet Error');
702
+ return;
703
+ }
704
+
705
+ if (this.state.direction === 'buy') {
706
+ await this.blockchainService.swapExactEthForTokenSupportingFeeOnTransfer(address, { amount: execAmountBI }, ethValue);
707
+ } else {
708
+ const routerAddress = this.blockchainService.swapRouter?.address || this.blockchainService.swapRouterAddress;
709
+ const routerAllowance = await this.blockchainService.getApproval(address, routerAddress);
710
+
711
+ if (BigInt(routerAllowance) < BigInt(execAmountBI)) {
712
+ if (this.approveModal) {
713
+ try {
714
+ this.eventBus.off('approve:complete');
715
+ this.approveModal.handleClose();
716
+ } catch (e) { console.warn('Error closing existing approval modal:', e); }
717
+ this.approveModal = null;
718
+ }
719
+
720
+ this.approveModal = new ApproveModal(cleanExecAmount, this.blockchainService, address);
721
+ this.approveModal.mount(document.body);
722
+
723
+ this.eventBus.once('approve:complete', async () => {
724
+ try {
725
+ await this.blockchainService.swapExactTokenForEthSupportingFeeOnTransferV2(address, { amount: execAmountBI });
726
+ } catch (error) {
727
+ this.messagePopup.error(`Swap Failed: ${error.message}`, 'Transaction Failed');
728
+ }
729
+ });
730
+
731
+ this.eventBus.once('approveModal:closed', () => {
732
+ this.approveModal = null;
733
+ });
734
+
735
+ this.approveModal.show();
736
+ return;
737
+ }
738
+
739
+ await this.blockchainService.swapExactTokenForEthSupportingFeeOnTransferV2(address, { amount: execAmountBI });
740
+ }
741
+ } else {
742
+ // Bonding curve logic
743
+ let proof;
744
+ try {
745
+ const currentTier = await this.blockchainService.getCurrentTier();
746
+ proof = await this.blockchainService.getMerkleProof(this.address, currentTier);
747
+ if (!proof) {
748
+ this.messagePopup.error(`You are not whitelisted for Tier ${currentTier + 1}.`, 'Not Whitelisted');
749
+ return;
750
+ }
751
+ } catch (error) {
752
+ this.messagePopup.error('Failed to verify whitelist status.', 'Whitelist Check Failed');
753
+ return;
754
+ }
755
+
756
+ let adjustedExecAmount = cleanExecAmount;
757
+ if (this.state.direction === 'buy' && this.state.freeSupply > 0 && !this.state.freeMint) {
758
+ const numAmount = parseInt(cleanExecAmount);
759
+ adjustedExecAmount = Math.max(0, numAmount - 1000000).toString();
760
+ }
761
+
762
+ const ethValue = this.blockchainService.parseEther(this.state.ethAmount);
763
+ const execAmountBI = this.blockchainService.parseExec(adjustedExecAmount);
764
+
765
+ if (this.state.direction === 'buy') {
766
+ await this.blockchainService.buyBonding({
767
+ amount: execAmountBI,
768
+ maxCost: ethValue,
769
+ mintNFT: this.transactionOptionsState.nftMintingEnabled,
770
+ proof: proof.proof,
771
+ message: this.transactionOptionsState.message
772
+ }, ethValue);
773
+ } else {
774
+ const minReturn = BigInt(ethValue) * BigInt(999) / BigInt(1000);
775
+ await this.blockchainService.sellBonding({
776
+ amount: execAmountBI,
777
+ minReturn: minReturn,
778
+ proof: proof.proof,
779
+ message: this.transactionOptionsState.message
780
+ });
781
+ }
782
+ }
783
+ } catch (error) {
784
+ console.error('Swap failed:', error);
785
+ let errorMessage = error.message;
786
+ if (errorMessage.includes('Contract call')) {
787
+ const parts = errorMessage.split(': ');
788
+ errorMessage = parts[parts.length - 1];
789
+ }
790
+ const context = this.state.direction === 'buy' ? 'Buy Failed' : 'Sell Failed';
791
+ this.messagePopup.error(`${context}: ${errorMessage}`, 'Transaction Failed');
792
+ }
793
+ }
794
+
795
+ handleQuickFill(e) {
796
+ e.preventDefault();
797
+
798
+ const amount = e.target.dataset.amount;
799
+ const percentage = e.target.dataset.percentage;
800
+
801
+ let value;
802
+
803
+ if (amount) {
804
+ value = amount;
805
+ } else if (percentage) {
806
+ const { balances } = this.state;
807
+ const execBalance = balances.exec;
808
+
809
+ if (!execBalance || execBalance === '0') {
810
+ console.warn('No EXEC balance available for quick fill');
811
+ return;
812
+ }
813
+
814
+ let readableBalance = BigInt(execBalance) / BigInt(1e18);
815
+
816
+ if (this.state.freeMint) {
817
+ readableBalance = readableBalance - BigInt(1000000);
818
+ if (readableBalance <= 0) {
819
+ this.messagePopup.info('You only have free mint tokens which cannot be sold.', 'Cannot Quick Fill');
820
+ return;
821
+ }
822
+ }
823
+
824
+ const amount = (readableBalance * BigInt(percentage)) / BigInt(100);
825
+ value = amount.toString();
826
+ }
827
+
828
+ this.handleInput('top', value);
829
+ }
830
+
831
+ handleContractDataUpdate() {
832
+ try {
833
+ const previousPhase = this.state.isPhase2;
834
+ const wasDataReady = this.state.dataReady;
835
+ const phaseWasUnknown = previousPhase === null;
836
+
837
+ const { contractData } = this.state;
838
+ const isPhase2 = this.isLiquidityDeployed();
839
+
840
+ this.state.isPhase2 = isPhase2;
841
+ this.state.dataReady = true;
842
+
843
+ if (phaseWasUnknown && this.state.isPhase2 !== null) {
844
+ console.log(`[${this.instanceId}] Phase determined: ${isPhase2 ? 'Phase 2' : 'Phase 1'}`);
845
+ this.initializeChildComponents();
846
+ this.setState({ dataReady: true, isPhase2 });
847
+ requestAnimationFrame(() => this.mountChildComponents());
848
+ return;
849
+ }
850
+
851
+ if (!wasDataReady && this.state.dataReady) {
852
+ console.log(`[${this.instanceId}] Data ready, mounting child components`);
853
+ this.setState({ dataReady: true, isPhase2 });
854
+ requestAnimationFrame(() => this.mountChildComponents());
855
+ return;
856
+ }
857
+
858
+ if (previousPhase !== this.state.isPhase2) {
859
+ console.log(`Phase changed`);
860
+ if (this.swapInputs) {
861
+ this.swapInputs.updateProps({ isPhase2: this.state.isPhase2 });
862
+ }
863
+ const priceContainer = this.element.querySelector('.price-display-container');
864
+ if (priceContainer && this.priceDisplay) {
865
+ this.priceDisplay.mount(priceContainer);
866
+ }
867
+ return;
868
+ }
869
+
870
+ const priceContainer = this.element.querySelector('.price-display-container');
871
+ if (priceContainer && this.priceDisplay) {
872
+ const shouldRemount = !this.priceDisplay.element || !priceContainer.contains(this.priceDisplay.element);
873
+ if (shouldRemount) {
874
+ this.priceDisplay.mount(priceContainer);
875
+ } else {
876
+ this.priceDisplay.update();
877
+ }
878
+ }
879
+ } catch (error) {
880
+ console.error('Error in handleContractDataUpdate:', error);
881
+ if (!this.state.dataReady) {
882
+ this.state.dataReady = true;
883
+ this.state.isPhase2 = this.isLiquidityDeployed();
884
+ if (!this.element || !this.element.innerHTML) {
885
+ this.update();
886
+ }
887
+ }
888
+ }
889
+ }
890
+
891
+ render() {
892
+ console.log('🎨 SwapInterface.render - Starting render');
893
+ const { direction, isPhase2, dataReady, balances, freeMint, freeSupply } = this.state;
894
+
895
+ if (!dataReady || isPhase2 === null) {
896
+ return `
897
+ <div class="price-display-container"></div>
898
+ <div class="quick-fill-buttons-container"></div>
899
+ <div class="swap-inputs-container">
900
+ <div style="padding: 20px; text-align: center;">Loading swap interface...</div>
901
+ </div>
902
+ <div class="transaction-options-container"></div>
903
+ <div class="swap-button-container"></div>
904
+ `;
905
+ }
906
+
907
+ console.log('Render - Current State:', { direction, freeMint, freeSupply, isPhase2 });
908
+
909
+ const formattedEthBalance = parseFloat(balances.eth).toFixed(6);
910
+ const formattedExecBalance = parseInt(balances.exec).toLocaleString();
911
+ const availableExecBalance = direction === 'sell' && freeMint
912
+ ? `Available: ${(parseInt(balances.exec) - 1000000).toLocaleString()}`
913
+ : `Balance: ${formattedExecBalance}`;
914
+
915
+ const result = `
916
+ <div class="price-display-container"></div>
917
+ ${direction === 'sell' && freeMint && !isPhase2 ?
918
+ `<div class="free-mint-notice">You have 1,000,000 $EXEC you received for free that cannot be sold here.</div>`
919
+ : direction === 'buy' && freeSupply > 0 && !freeMint && !isPhase2 ?
920
+ `<div class="free-mint-notice free-mint-bonus">1,000,000 $EXEC will be added to your purchase. Thank you.</div>`
921
+ : ''
922
+ }
923
+ <div class="quick-fill-buttons-container"></div>
924
+ <div class="swap-inputs-container"></div>
925
+ <div class="transaction-options-container"></div>
926
+ <div class="swap-button-container"></div>
927
+ `;
928
+ console.log('🎨 SwapInterface.render - Completed render');
929
+ return result;
930
+ }
931
+
932
+ /**
933
+ * Get the user's wallet address, resolving any promise if needed
934
+ * @returns {Promise<string>} Resolved address
935
+ */
936
+ async getAddress() {
937
+ try {
938
+ // Resolve the address if it's a Promise
939
+ const resolvedAddress = await Promise.resolve(this._address);
940
+
941
+ // Log the resolved address for debugging
942
+ console.log(`[${this.instanceId}] Resolved address: ${resolvedAddress}`);
943
+
944
+ return resolvedAddress;
945
+ } catch (error) {
946
+ console.error(`[${this.instanceId}] Error resolving address:`, error);
947
+ return null;
948
+ }
949
+ }
950
+
951
+ /**
952
+ * Update the user's wallet address
953
+ * @param {string} newAddress - The new wallet address
954
+ */
955
+ setAddress(newAddress) {
956
+ this._address = newAddress;
957
+ }
958
+
959
+ /**
960
+ * Property to maintain backward compatibility with old code
961
+ */
962
+ get address() {
963
+ return this._address;
964
+ }
965
+
966
+ /**
967
+ * Property setter to maintain backward compatibility
968
+ */
969
+ set address(newAddress) {
970
+ this._address = newAddress;
971
+ }
972
+ }