@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,1576 @@
1
+ import { ethers } from 'ethers';
2
+
3
+ class BlockchainService {
4
+ constructor(eventBus, contractCache) {
5
+ if (!eventBus || !contractCache) {
6
+ throw new Error('BlockchainService requires eventBus and contractCache instances.');
7
+ }
8
+ this.provider = null;
9
+ this.signer = null;
10
+ this.contract = null;
11
+ this.mirrorContract = null;
12
+ this.connectionState = 'disconnected';
13
+ this.networkConfig = null;
14
+ this.ethers = ethers;
15
+ this.eventBus = eventBus;
16
+ this.contractCache = contractCache;
17
+
18
+ this.swapRouterAddress = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; // Uniswap v2 Router on Mainnet
19
+ this.swapRouter = null;
20
+
21
+ this.isInternalNetworkChange = false;
22
+
23
+ // Add transaction tracking
24
+ this.transactionCounter = 0;
25
+ this.activeTransactions = new Map();
26
+
27
+ // Network change state machine
28
+ this.networkChangeState = 'idle'; // 'idle' | 'switching' | 'switched' | 'error'
29
+ this.networkChangeTimeout = null;
30
+ this.networkChangeTimeoutDuration = 30000; // 30 seconds
31
+ this.pendingNetworkChange = null;
32
+ this.previousNetworkId = null;
33
+
34
+ // Track initialization state to prevent race conditions
35
+ this.isInitializing = false;
36
+
37
+ // Store handlers for later setup (after initialization)
38
+ this.chainChangedHandler = () => this.handleNetworkChange();
39
+ this.accountsChangedHandler = () => this.handleAccountChange();
40
+ }
41
+
42
+ // Initialize the service with network configuration
43
+ async initialize(networkConfig) {
44
+ this.isInitializing = true;
45
+ try {
46
+ if (!networkConfig) {
47
+ throw new Error("Network configuration is required for initialization.");
48
+ }
49
+ this.networkConfig = networkConfig;
50
+
51
+ await this.initializeProvider();
52
+ this.connectionState = 'connected';
53
+ this.setupNetworkListeners();
54
+ this.this.eventBus.emit('blockchain:initialized');
55
+ this.isInitializing = false;
56
+ return true;
57
+ } catch (error) {
58
+ this.connectionState = 'error';
59
+ this.isInitializing = false;
60
+ this.this.eventBus.emit('blockchain:error', error);
61
+ throw this.wrapError(error, 'Blockchain initialization failed');
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Set up network change listeners (called after initialization)
67
+ * @private
68
+ */
69
+ setupNetworkListeners() {
70
+ if (window.ethereum && this.chainChangedHandler && this.accountsChangedHandler) {
71
+ try {
72
+ window.ethereum.removeListener('chainChanged', this.chainChangedHandler);
73
+ window.ethereum.removeListener('accountsChanged', this.accountsChangedHandler);
74
+ } catch (error) {
75
+ // Ignore errors
76
+ }
77
+ window.ethereum.on('chainChanged', this.chainChangedHandler);
78
+ window.ethereum.on('accountsChanged', this.accountsChangedHandler);
79
+ }
80
+ }
81
+
82
+ async initializeProvider() {
83
+ try {
84
+ if (window.ethereum) {
85
+ const targetNetwork = parseInt(this.networkConfig?.network || '1');
86
+ await this.waitForCorrectNetwork(targetNetwork);
87
+
88
+ const web3Provider = new ethers.providers.Web3Provider(window.ethereum, 'any');
89
+ this.provider = web3Provider;
90
+ this.signer = web3Provider.getSigner();
91
+
92
+ const network = await this.provider.getNetwork();
93
+ if (network.chainId !== targetNetwork) {
94
+ this.this.eventBus.emit('network:switching', { from: network.chainId, to: targetNetwork, automatic: true });
95
+ try {
96
+ this.isInternalNetworkChange = true;
97
+ await window.ethereum.request({
98
+ method: 'wallet_switchEthereumChain',
99
+ params: [{ chainId: `0x${targetNetwork.toString(16)}` }],
100
+ });
101
+ this.provider = new ethers.providers.Web3Provider(window.ethereum);
102
+ this.signer = this.provider.getSigner();
103
+ this.this.eventBus.emit('network:switched', { from: network.chainId, to: targetNetwork, success: true });
104
+ } catch (switchError) {
105
+ this.isInternalNetworkChange = false;
106
+ this.this.eventBus.emit('network:switched', { from: network.chainId, to: targetNetwork, success: false, error: switchError.message });
107
+ if (switchError.code === 4902) {
108
+ await window.ethereum.request({
109
+ method: 'wallet_addEthereumChain',
110
+ params: [{
111
+ chainId: `0x${targetNetwork.toString(16)}`,
112
+ rpcUrls: [this.networkConfig.rpcUrl],
113
+ chainName: this.networkConfig.chainName,
114
+ nativeCurrency: this.networkConfig.nativeCurrency
115
+ }]
116
+ });
117
+ this.provider = new ethers.providers.Web3Provider(window.ethereum);
118
+ this.signer = this.provider.getSigner();
119
+ } else {
120
+ throw switchError;
121
+ }
122
+ } finally {
123
+ this.isInternalNetworkChange = false;
124
+ }
125
+ }
126
+ } else {
127
+ this.provider = new ethers.providers.JsonRpcProvider(this.networkConfig.rpcUrl);
128
+ }
129
+ } catch (error) {
130
+ this.isInternalNetworkChange = false;
131
+ throw this.wrapError(error, 'Provider initialization failed');
132
+ }
133
+ }
134
+
135
+ async initializeContract(contractAddress, contractABI, mirrorABI, routerABI) {
136
+ try {
137
+ if (!contractAddress || !contractABI || !mirrorABI || !routerABI) {
138
+ throw new Error("Contract address and all ABIs are required.");
139
+ }
140
+
141
+ this.contract = new ethers.Contract(contractAddress, contractABI, this.provider);
142
+ const mirrorAddress = await this.contract.mirrorERC721();
143
+ this.mirrorContract = new ethers.Contract(mirrorAddress, mirrorABI, this.provider);
144
+ this.swapRouter = new ethers.Contract(this.swapRouterAddress, routerABI, this.provider);
145
+
146
+ const liquidityPoolAddress = await this.getLiquidityPool();
147
+ if (liquidityPoolAddress && liquidityPoolAddress !== '0x0000000000000000000000000000000000000000') {
148
+ const poolABI = ["function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)", "function token0() external view returns (address)", "function token1() external view returns (address)"];
149
+ this.v2PoolContract = new ethers.Contract(liquidityPoolAddress, poolABI, this.provider);
150
+ } else {
151
+ console.warn('Liquidity pool address is zero, skipping V2 pool contract initialization.');
152
+ }
153
+ this.this.eventBus.emit('contract:updated');
154
+ } catch (error) {
155
+ throw this.wrapError(error, 'Contract initialization failed');
156
+ }
157
+ }
158
+
159
+ // Merkle proof functionality removed - whitelisting no longer uses Merkle trees
160
+ async getMerkleProof(address, tier = null) {
161
+ // Return null - merkle proofs no longer supported
162
+ // Users can use alternative whitelisting methods (e.g., passwords)
163
+ return null;
164
+ }
165
+
166
+ /**
167
+ * Enhanced executeContractCall with contract selection
168
+ */
169
+ async executeContractCall(method, args = [], options = {}) {
170
+ try {
171
+ if (!this.contract) {
172
+ throw new Error('Contract not initialized');
173
+ }
174
+
175
+ // Select contract instance
176
+ let contractInstance = options.useContract === 'mirror' ?
177
+ this.mirrorContract :
178
+ this.contract;
179
+
180
+ if (options.useContract === 'router') {
181
+ contractInstance = this.swapRouter;
182
+ }
183
+
184
+ if (options.useContract === 'v2pool') {
185
+ contractInstance = this.v2PoolContract;
186
+ }
187
+
188
+ // Add signer if needed
189
+ if (options.requiresSigner) {
190
+ if (!this.signer) {
191
+ throw new Error('No wallet connected');
192
+ }
193
+ contractInstance = contractInstance.connect(this.signer);
194
+ }
195
+
196
+ // Check if method exists on contract
197
+ if (typeof contractInstance[method] !== 'function') {
198
+ throw new Error(`Method ${method} not found on contract`);
199
+ }
200
+
201
+ // Execute the contract call
202
+ const result = await contractInstance[method](...(args || []), options.txOptions || {});
203
+
204
+ // If this is a transaction, wait for confirmation
205
+ if (result.wait) {
206
+ const receipt = await result.wait();
207
+ this.eventBus.emit('transaction:confirmed', {
208
+ hash: receipt.transactionHash,
209
+ method,
210
+ args
211
+ });
212
+ return receipt;
213
+ }
214
+
215
+ return result;
216
+ } catch (error) {
217
+ throw this.handleContractError(error, method);
218
+ }
219
+ }
220
+
221
+ // Error handling methods
222
+ handleContractError(error, method) {
223
+ // Extract the revert reason if it exists
224
+ let message = error.message;
225
+
226
+ // Handle common contract errors
227
+ if (error.code === 'INSUFFICIENT_FUNDS') {
228
+ return new Error('Insufficient funds to complete transaction');
229
+ }
230
+ if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
231
+ // Try to extract the revert reason from the error
232
+ const revertMatch = error.message.match(/execution reverted: (.*?)(?:\"|$)/);
233
+ message = revertMatch ? `Tx Reverted: ${revertMatch[1]}` : 'Transaction would fail - check your inputs';
234
+ return new Error(message);
235
+ }
236
+ if (error.code === 4001 || error.code === 'ACTION_REJECTED') {
237
+ return new Error('Transaction rejected by user');
238
+ }
239
+
240
+ // Extract revert reason from other error types
241
+ if (error.message.includes('execution reverted')) {
242
+ const revertMatch = error.message.match(/execution reverted: (.*?)(?:\"|$)/);
243
+ message = revertMatch ? `Tx Reverted: ${revertMatch[1]}` : error.message;
244
+ }
245
+
246
+ // Log unexpected errors
247
+ console.error(`Contract error in ${method}:`, error);
248
+ return new Error(message);
249
+ }
250
+
251
+ wrapError(error, context) {
252
+ const wrappedError = new Error(`${context}: ${error.message}`);
253
+ wrappedError.originalError = error;
254
+ wrappedError.code = error.code;
255
+ return wrappedError;
256
+ }
257
+
258
+ // Network and account change handlers
259
+ async handleNetworkChange() {
260
+ try {
261
+ // Ignore network changes during initialization to prevent race conditions
262
+ if (this.isInitializing) {
263
+ console.log('BlockchainService: Ignoring network change during initialization');
264
+ return;
265
+ }
266
+
267
+ if (this.isInternalNetworkChange) {
268
+ return;
269
+ }
270
+
271
+ // Get current network directly from window.ethereum to avoid provider issues
272
+ let currentNetworkId = null;
273
+ try {
274
+ // Use eth_chainId directly to avoid "underlying network changed" errors
275
+ const chainIdHex = await window.ethereum.request({ method: 'eth_chainId' });
276
+ currentNetworkId = parseInt(chainIdHex, 16);
277
+ } catch (error) {
278
+ console.warn('Could not get current network from window.ethereum:', error);
279
+ // Fallback to provider if direct call fails
280
+ try {
281
+ if (this.provider) {
282
+ const network = await this.provider.getNetwork();
283
+ currentNetworkId = network.chainId;
284
+ }
285
+ } catch (providerError) {
286
+ console.warn('Could not get current network from provider:', providerError);
287
+ }
288
+ }
289
+
290
+ // Set previous network for fallback
291
+ this.previousNetworkId = currentNetworkId;
292
+
293
+ // Check if user switched to wrong network - if so, we need to switch back
294
+ const targetNetwork = parseInt(this.networkConfig?.network || '1');
295
+ if (currentNetworkId && currentNetworkId !== targetNetwork) {
296
+ console.log(`User switched to network ${currentNetworkId}, but we need ${targetNetwork}. Switching back...`);
297
+ }
298
+
299
+ // Transition to switching state
300
+ this.networkChangeState = 'switching';
301
+ this.pendingNetworkChange = {
302
+ from: currentNetworkId,
303
+ to: null,
304
+ startTime: Date.now()
305
+ };
306
+
307
+ // Emit switching event
308
+ this.eventBus.emit('network:switching', {
309
+ from: currentNetworkId,
310
+ to: null,
311
+ automatic: true
312
+ });
313
+
314
+ // Set timeout for network switch
315
+ this.networkChangeTimeout = setTimeout(() => {
316
+ if (this.networkChangeState === 'switching') {
317
+ this.handleNetworkChangeTimeout();
318
+ }
319
+ }, this.networkChangeTimeoutDuration);
320
+
321
+ // Initialize provider (this will handle the actual network switch)
322
+ await this.initializeProvider();
323
+
324
+ // Get new network after switch (using direct call to avoid provider issues)
325
+ let newNetworkId = null;
326
+ try {
327
+ // Use eth_chainId directly to avoid "underlying network changed" errors
328
+ const chainIdHex = await window.ethereum.request({ method: 'eth_chainId' });
329
+ newNetworkId = parseInt(chainIdHex, 16);
330
+ } catch (error) {
331
+ console.warn('Could not get new network from window.ethereum:', error);
332
+ // Fallback to provider if direct call fails
333
+ try {
334
+ if (this.provider) {
335
+ const network = await this.provider.getNetwork();
336
+ newNetworkId = network.chainId;
337
+ }
338
+ } catch (providerError) {
339
+ console.warn('Could not get new network from provider:', providerError);
340
+ }
341
+ }
342
+
343
+ // Calculate duration before clearing pendingNetworkChange
344
+ const duration = this.pendingNetworkChange ?
345
+ Date.now() - this.pendingNetworkChange.startTime : 0;
346
+
347
+ // Clear timeout
348
+ if (this.networkChangeTimeout) {
349
+ clearTimeout(this.networkChangeTimeout);
350
+ this.networkChangeTimeout = null;
351
+ }
352
+
353
+ // Transition to switched state
354
+ this.networkChangeState = 'switched';
355
+ const pendingChange = this.pendingNetworkChange;
356
+ this.pendingNetworkChange = null;
357
+
358
+ // Emit switched event
359
+ this.eventBus.emit('network:switched', {
360
+ from: currentNetworkId,
361
+ to: newNetworkId,
362
+ success: true,
363
+ duration: duration
364
+ });
365
+
366
+ // Emit legacy event for backward compatibility (mark as automatic)
367
+ this.eventBus.emit('network:changed', { automatic: true });
368
+
369
+ // Also emit contract updated event for UI components that need to update
370
+ this.eventBus.emit('contract:updated');
371
+
372
+ // Reset state after a short delay
373
+ setTimeout(() => {
374
+ if (this.networkChangeState === 'switched') {
375
+ this.networkChangeState = 'idle';
376
+ }
377
+ }, 1000);
378
+
379
+ } catch (error) {
380
+ // Clear timeout
381
+ if (this.networkChangeTimeout) {
382
+ clearTimeout(this.networkChangeTimeout);
383
+ this.networkChangeTimeout = null;
384
+ }
385
+
386
+ // Transition to error state
387
+ this.networkChangeState = 'error';
388
+
389
+ // Emit error event
390
+ this.eventBus.emit('network:switch:error', {
391
+ from: this.previousNetworkId,
392
+ to: null,
393
+ error: error.message || 'Network switch failed',
394
+ originalError: error
395
+ });
396
+
397
+ // Emit legacy error event for backward compatibility
398
+ this.eventBus.emit('blockchain:error', error);
399
+
400
+ // Reset state after error handling
401
+ setTimeout(() => {
402
+ if (this.networkChangeState === 'error') {
403
+ this.networkChangeState = 'idle';
404
+ this.pendingNetworkChange = null;
405
+ }
406
+ }, 2000);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Handle network change timeout
412
+ * @private
413
+ */
414
+ handleNetworkChangeTimeout() {
415
+ console.warn('Network change timed out after', this.networkChangeTimeoutDuration, 'ms');
416
+
417
+ // Transition to error state
418
+ this.networkChangeState = 'error';
419
+
420
+ // Emit timeout event
421
+ this.eventBus.emit('network:switch:timeout', {
422
+ from: this.previousNetworkId,
423
+ to: null,
424
+ timeout: this.networkChangeTimeoutDuration
425
+ });
426
+
427
+ // Clear timeout
428
+ if (this.networkChangeTimeout) {
429
+ clearTimeout(this.networkChangeTimeout);
430
+ this.networkChangeTimeout = null;
431
+ }
432
+
433
+ // Reset state after timeout handling
434
+ setTimeout(() => {
435
+ if (this.networkChangeState === 'error') {
436
+ this.networkChangeState = 'idle';
437
+ this.pendingNetworkChange = null;
438
+ }
439
+ }, 2000);
440
+ }
441
+
442
+ /**
443
+ * Cancel pending network change
444
+ * @returns {boolean} Whether cancellation was successful
445
+ */
446
+ cancelNetworkChange() {
447
+ if (this.networkChangeState === 'switching') {
448
+ // Clear timeout
449
+ if (this.networkChangeTimeout) {
450
+ clearTimeout(this.networkChangeTimeout);
451
+ this.networkChangeTimeout = null;
452
+ }
453
+
454
+ // Reset state
455
+ this.networkChangeState = 'idle';
456
+ this.pendingNetworkChange = null;
457
+
458
+ // Emit cancellation event
459
+ this.eventBus.emit('network:switch:cancelled', {
460
+ from: this.previousNetworkId
461
+ });
462
+
463
+ return true;
464
+ }
465
+ return false;
466
+ }
467
+
468
+ /**
469
+ * Get current network change state
470
+ * @returns {string} Current state ('idle' | 'switching' | 'switched' | 'error')
471
+ */
472
+ getNetworkChangeState() {
473
+ return this.networkChangeState;
474
+ }
475
+
476
+ /**
477
+ * Wait for network to be on the correct network before proceeding
478
+ * @param {number} targetNetworkId - The target network ID
479
+ * @param {number} timeout - Maximum time to wait in milliseconds (default: 5000)
480
+ * @returns {Promise<void>}
481
+ * @private
482
+ */
483
+ async waitForCorrectNetwork(targetNetworkId, timeout = 5000) {
484
+ const startTime = Date.now();
485
+ const pollInterval = 100; // Check every 100ms
486
+
487
+ while (Date.now() - startTime < timeout) {
488
+ try {
489
+ // Get chainId directly from window.ethereum to avoid provider issues
490
+ const chainIdHex = await window.ethereum.request({ method: 'eth_chainId' });
491
+ const chainId = parseInt(chainIdHex, 16);
492
+
493
+ if (chainId === targetNetworkId) {
494
+ // Network is correct, wait a bit for it to stabilize
495
+ await new Promise(resolve => setTimeout(resolve, 200));
496
+ return;
497
+ }
498
+ } catch (error) {
499
+ // Provider might not be ready yet, continue polling
500
+ // Don't log to avoid spam
501
+ }
502
+
503
+ // Wait before next poll
504
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
505
+ }
506
+
507
+ // If we get here, network didn't match within timeout
508
+ // This is OK - we'll handle it in the main initialization flow
509
+ console.warn(`Network check timeout: Expected ${targetNetworkId}, will verify in initializeProvider`);
510
+ }
511
+
512
+ async handleAccountChange() {
513
+ try {
514
+ if (window.ethereum) {
515
+ const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
516
+ this.signer = web3Provider.getSigner();
517
+ this.eventBus.emit('account:changed');
518
+
519
+ // Also emit contract updated event
520
+ this.eventBus.emit('contract:updated');
521
+ }
522
+ } catch (error) {
523
+ this.eventBus.emit('blockchain:error', error);
524
+ }
525
+ }
526
+
527
+ // Getters for connection state
528
+ getConnectionState() {
529
+ return this.connectionState;
530
+ }
531
+
532
+ isConnected() {
533
+ return this.connectionState === 'connected';
534
+ }
535
+
536
+ async getCurrentTier() {
537
+ try {
538
+ // Check cache first
539
+ const cached = this.contractCache.get('getCurrentTier', []);
540
+ if (cached !== null) {
541
+ return cached;
542
+ }
543
+
544
+ const tier = await this.executeContractCall('getCurrentTier');
545
+ const result = parseInt(tier.toString());
546
+
547
+ // Cache the result
548
+ this.contractCache.set('getCurrentTier', [], result);
549
+
550
+ return result;
551
+ } catch (error) {
552
+ throw this.wrapError(error, 'Failed to get current tier');
553
+ }
554
+ }
555
+
556
+ async getCurrentRoot() {
557
+ try {
558
+ const root = await this.executeContractCall('getCurrentRoot');
559
+ return root.toString();
560
+ } catch (error) {
561
+ throw this.wrapError(error, 'Failed to get current root');
562
+ }
563
+ }
564
+
565
+ async getLiquidityPool() {
566
+ try {
567
+ // Check cache first
568
+ const cached = this.contractCache.get('getLiquidityPool', []);
569
+ if (cached !== null) {
570
+ return cached;
571
+ }
572
+
573
+ const liquidityPool = await this.executeContractCall('liquidityPair');
574
+ const result = liquidityPool.toString();
575
+
576
+ // Cache the result
577
+ this.contractCache.set('getLiquidityPool', [], result);
578
+
579
+ return result;
580
+ } catch (error) {
581
+ throw this.wrapError(error, 'Failed to get liquidity pool address');
582
+ }
583
+ }
584
+
585
+ async getTotalBondingSupply() {
586
+ try {
587
+ // Check cache first
588
+ const cached = this.contractCache.get('getTotalBondingSupply', []);
589
+ if (cached !== null) {
590
+ return cached;
591
+ }
592
+
593
+ const supply = await this.executeContractCall('totalBondingSupply');
594
+ // Convert from BigNumber to number and format
595
+ const result = parseFloat(this.ethers.utils.formatUnits(supply, 0));
596
+
597
+ // Cache the result
598
+ this.contractCache.set('getTotalBondingSupply', [], result);
599
+
600
+ return result;
601
+ } catch (error) {
602
+ throw this.wrapError(error, 'Failed to get total bonding supply');
603
+ }
604
+ }
605
+
606
+ async getTotalMessages() {
607
+ try {
608
+ // Check cache first
609
+ const cached = this.contractCache.get('getTotalMessages', []);
610
+ if (cached !== null) {
611
+ return cached;
612
+ }
613
+
614
+ const messages = await this.executeContractCall('totalMessages');
615
+ const result = parseFloat(this.ethers.utils.formatUnits(messages, 0));
616
+
617
+ // Cache the result
618
+ this.contractCache.set('getTotalMessages', [], result);
619
+
620
+ return result;
621
+ } catch (error) {
622
+ throw this.wrapError(error, 'Failed to get total messages');
623
+ }
624
+ }
625
+
626
+ async getMessagesBatch(startIndex, endIndex) {
627
+ const messages = await this.executeContractCall('getMessagesBatch', [startIndex, endIndex]);
628
+ return messages.map(message => message.toString());
629
+ }
630
+
631
+ /**
632
+ * Gets token balance for an address
633
+ * @param {string} address - The address to check
634
+ * @returns {Promise<string>} Balance in wei
635
+ */
636
+ async getTokenBalance(address) {
637
+ // Validate address before proceeding
638
+ if (!address || typeof address !== 'string') {
639
+ throw new Error('Invalid address provided to getTokenBalance');
640
+ }
641
+
642
+ // Check cache first
643
+ const cached = this.contractCache.get('getTokenBalance', [address]);
644
+ if (cached !== null) {
645
+ return cached;
646
+ }
647
+
648
+ const result = await this.executeContractCall('balanceOf', [address]);
649
+
650
+ // Cache the result
651
+ this.contractCache.set('getTokenBalance', [address], result);
652
+
653
+ return result;
654
+ }
655
+
656
+ /**
657
+ * Gets NFT balance for an address using mirror contract
658
+ * @param {string} address - The address to check
659
+ * @returns {Promise<number>} Number of NFTs owned
660
+ */
661
+ async getNFTBalance(address) {
662
+ try {
663
+ // Validate address before proceeding
664
+ if (!address || typeof address !== 'string') {
665
+ throw new Error('Invalid address provided to getNFTBalance');
666
+ }
667
+
668
+ // Check cache first
669
+ const cached = this.contractCache.get('getNFTBalance', [address]);
670
+ if (cached !== null) {
671
+ return cached;
672
+ }
673
+
674
+ const balance = await this.executeContractCall(
675
+ 'balanceOf',
676
+ [address],
677
+ { useContract: 'mirror' }
678
+ );
679
+ const result = parseInt(balance.toString());
680
+
681
+ // Cache the result
682
+ this.contractCache.set('getNFTBalance', [address], result);
683
+
684
+ return result;
685
+ } catch (error) {
686
+ throw this.wrapError(error, 'Failed to get NFT balance');
687
+ }
688
+ }
689
+
690
+ async getNFTSupply() {
691
+ try {
692
+ return this.executeContractCall('totalSupply', [], { useContract: 'mirror' });
693
+ } catch (error) {
694
+ throw this.wrapError(error, 'Failed to get NFT supply');
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Gets ETH balance for an address
700
+ * @param {string} address - The address to check
701
+ * @returns {Promise<string>} Balance in wei
702
+ */
703
+ async getEthBalance(address) {
704
+ try {
705
+ // Validate address before proceeding
706
+ if (!address || typeof address !== 'string') {
707
+ throw new Error('Invalid address provided to getEthBalance');
708
+ }
709
+
710
+ // Check cache first
711
+ const cached = this.contractCache.get('getEthBalance', [address]);
712
+ if (cached !== null) {
713
+ return cached;
714
+ }
715
+
716
+ const balance = await this.provider.getBalance(address);
717
+ const result = balance.toString();
718
+
719
+ // Cache the result
720
+ this.contractCache.set('getEthBalance', [address], result);
721
+
722
+ return result;
723
+ } catch (error) {
724
+ throw this.wrapError(error, 'Failed to get ETH balance');
725
+ }
726
+ }
727
+
728
+ /**
729
+ * Execute buy bonding transaction
730
+ * @param {Object} params - Transaction parameters
731
+ * @param {string} params.amount - Amount of EXEC to buy
732
+ * @param {string} params.maxCost - Maximum ETH cost in wei
733
+ * @param {boolean} params.mintNFT - Whether to mint NFT
734
+ * @param {Array} params.proof - Merkle proof
735
+ * @param {string} params.message - Transaction message
736
+ * @param {string} ethValue - ETH value in ether
737
+ * @returns {Promise<Object>} Transaction receipt
738
+ */
739
+ async buyBonding(params, ethValue) {
740
+ try {
741
+ this.eventBus.emit('transaction:pending', { type: 'buy' });
742
+ console.log('Buy bonding called with params:', params);
743
+
744
+ const receipt = await this.executeContractCall(
745
+ 'buyBonding',
746
+ [params.amount, params.maxCost, params.mintNFT, params.proof, params.message],
747
+ {
748
+ requiresSigner: true,
749
+ txOptions: { value: ethValue }
750
+ }
751
+ );
752
+
753
+ this.eventBus.emit('transaction:success', {
754
+ type: 'buy',
755
+ receipt,
756
+ amount: params.amount
757
+ });
758
+
759
+ return receipt;
760
+ } catch (error) {
761
+ this.eventBus.emit('transaction:error', {
762
+ type: 'buy',
763
+ error: this.wrapError(error, 'Buy bonding failed')
764
+ });
765
+ throw error;
766
+ }
767
+ }
768
+
769
+ /**
770
+ * Execute sell bonding transaction
771
+ * @param {Object} params - Transaction parameters
772
+ * @param {string} params.amount - Amount of EXEC to sell
773
+ * @param {string} params.minReturn - Minimum ETH return in wei
774
+ * @param {Array} params.proof - Merkle proof
775
+ * @param {string} params.message - Transaction message
776
+ * @returns {Promise<Object>} Transaction receipt
777
+ */
778
+ async sellBonding(params) {
779
+ try {
780
+ this.eventBus.emit('transaction:pending', { type: 'sell' });
781
+
782
+ const receipt = await this.executeContractCall(
783
+ 'sellBonding',
784
+ [params.amount, params.minReturn, params.proof, params.message],
785
+ { requiresSigner: true }
786
+ );
787
+
788
+ this.eventBus.emit('transaction:success', {
789
+ type: 'sell',
790
+ receipt,
791
+ amount: params.amount
792
+ });
793
+
794
+ return receipt;
795
+ } catch (error) {
796
+ this.eventBus.emit('transaction:error', {
797
+ type: 'sell',
798
+ error: this.wrapError(error, 'Sell bonding failed')
799
+ });
800
+ throw error;
801
+ }
802
+ }
803
+
804
+
805
+
806
+
807
+ async getTokenPrice() {
808
+ try {
809
+ if (!this.contract) {
810
+ throw new Error('Contract not initialized');
811
+ }
812
+
813
+ // Format amount with 18 decimals (like ETH)
814
+ const amount = ethers.utils.parseEther('1000000');
815
+
816
+ // Get price using calculateCost
817
+ const price = await this.executeContractCall('calculateCost', [amount]);
818
+
819
+ // Ensure price is properly formatted
820
+ if (!price || !ethers.BigNumber.isBigNumber(price)) {
821
+ throw new Error('Invalid price format from contract');
822
+ }
823
+
824
+ // Convert BigNumber to number and format
825
+ const priceInEth = parseFloat(this.ethers.utils.formatEther(price));
826
+
827
+ return priceInEth;
828
+ } catch (error) {
829
+ throw this.wrapError(error, 'Failed to get token price');
830
+ }
831
+ }
832
+
833
+ async getCurrentPrice() {
834
+ try {
835
+ // Check cache first
836
+ const cached = this.contractCache.get('getCurrentPrice', []);
837
+ if (cached !== null) {
838
+ return cached;
839
+ }
840
+
841
+ const result = await this.getTokenPrice();
842
+
843
+ // Cache the result
844
+ this.contractCache.set('getCurrentPrice', [], result);
845
+
846
+ return result;
847
+ } catch (error) {
848
+ throw this.wrapError(error, 'Failed to get current price');
849
+ }
850
+ }
851
+
852
+ async calculateCost(execAmount) {
853
+ try {
854
+ const response = await this.executeContractCall('calculateCost', [execAmount]);
855
+ return response;
856
+
857
+ } catch (error) {
858
+ throw this.wrapError(error, 'Failed to calculate cost');
859
+ }
860
+ }
861
+
862
+ async getExecForEth(ethAmount) {
863
+ try {
864
+ const weiAmount = ethers.utils.parseEther(ethAmount.toString());
865
+ const execAmount = await this.executeContractCall('getExecForEth', [weiAmount]);
866
+ return this.formatExec(execAmount);
867
+ } catch (error) {
868
+ throw this.wrapError(error, 'Failed to calculate EXEC for ETH amount');
869
+ }
870
+ }
871
+
872
+ async getEthForExec(execAmount) {
873
+ try {
874
+ const execWei = ethers.utils.parseUnits(execAmount.toString(), 18);
875
+ const ethAmount = await this.executeContractCall('getEthForExec', [execWei]);
876
+ return this.formatEther(ethAmount);
877
+ } catch (error) {
878
+ throw this.wrapError(error, 'Failed to calculate ETH for EXEC amount');
879
+ }
880
+ }
881
+
882
+ async getFreeSupply() {
883
+ try {
884
+ // Check cache first
885
+ const cached = this.contractCache.get('getFreeSupply', []);
886
+ if (cached !== null) {
887
+ return cached;
888
+ }
889
+
890
+ const freeExec = await this.executeContractCall('freeSupply');
891
+ const result = this.formatExec(freeExec);
892
+
893
+ // Cache the result
894
+ this.contractCache.set('getFreeSupply', [], result);
895
+
896
+ return result;
897
+ } catch (error) {
898
+ throw this.wrapError(error, 'Failed to get free supply');
899
+ }
900
+ }
901
+
902
+ async getFreeMint(address) {
903
+ try {
904
+ // Validate address before proceeding
905
+ if (!address || typeof address !== 'string') {
906
+ throw new Error('Invalid address provided to getFreeMint');
907
+ }
908
+
909
+ // Check cache first
910
+ const cached = this.contractCache.get('getFreeMint', [address]);
911
+ if (cached !== null) {
912
+ return cached;
913
+ }
914
+
915
+ const freeMint = await this.executeContractCall('freeMint', [address]);
916
+ console.log('FREE MINT', freeMint);
917
+
918
+ // Cache the result
919
+ this.contractCache.set('getFreeMint', [address], freeMint);
920
+
921
+ return freeMint;
922
+ } catch (error) {
923
+ throw this.wrapError(error, 'Failed to get free mint');
924
+ }
925
+ }
926
+
927
+ async getFreeSituation(address) {
928
+ try {
929
+ const freeMint = await this.getFreeMint(address);
930
+ const freeSupply = await this.getFreeSupply();
931
+ const freeSituation = {
932
+ freeMint,
933
+ freeSupply
934
+ };
935
+ return freeSituation;
936
+ } catch (error) {
937
+ throw this.wrapError(error, 'Failed to get free situation');
938
+ }
939
+ }
940
+
941
+ async getUserNFTIds(address) {
942
+ try {
943
+ const nftIds = await this.executeContractCall('getOwnerTokens', [address]);
944
+ return nftIds;
945
+ } catch (error) {
946
+ throw this.wrapError(error, 'Failed to get user NFT IDs');
947
+ }
948
+ }
949
+
950
+ async getTokenUri(tokenId) {
951
+ try {
952
+ const uri = await this.executeContractCall('tokenURI', [tokenId]);
953
+ return uri;
954
+ } catch (error) {
955
+ throw this.wrapError(error, 'Failed to get token URI');
956
+ }
957
+ }
958
+
959
+ async getContractEthBalance() {
960
+ try {
961
+ // Check cache first
962
+ const cached = this.contractCache.get('getContractEthBalance', []);
963
+ if (cached !== null) {
964
+ return cached;
965
+ }
966
+
967
+ const balance = await this.provider.getBalance(this.contract.address);
968
+ const formattedBalance = this.formatEther(balance);
969
+ console.log('CONTRACT ETH BALANCE', formattedBalance);
970
+
971
+ // Cache the result
972
+ this.contractCache.set('getContractEthBalance', [], formattedBalance);
973
+
974
+ return formattedBalance;
975
+ } catch (error) {
976
+ throw this.wrapError(error, 'Failed to get contract ETH balance');
977
+ }
978
+ }
979
+
980
+ async getUserNFTs(address) {
981
+ try {
982
+ // Validate address before proceeding
983
+ if (!address || typeof address !== 'string') {
984
+ throw new Error('Invalid address provided to getUserNFTs');
985
+ }
986
+
987
+ const nftIds = await this.executeContractCall('getOwnerTokens', [address]);
988
+ return nftIds;
989
+ } catch (error) {
990
+ throw this.wrapError(error, 'Failed to get user NFT IDs');
991
+ }
992
+ }
993
+
994
+ async getNFTMetadata(tokenId) {
995
+ try {
996
+ const metadata = await this.executeContractCall('tokenURI', [tokenId]);
997
+ console.log('NFT METADATA', metadata);
998
+ return metadata;
999
+ } catch (error) {
1000
+ throw this.wrapError(error, 'Failed to get NFT metadata');
1001
+ }
1002
+ }
1003
+
1004
+ async getNFTMetadataBatch(tokenIds) {
1005
+ try {
1006
+ const metadataPromises = tokenIds.map(id =>
1007
+ this.executeContractCall('tokenURI', [id])
1008
+ );
1009
+
1010
+ const metadata = await Promise.all(metadataPromises);
1011
+ console.log('Batch NFT Metadata:', metadata);
1012
+ return metadata;
1013
+ } catch (error) {
1014
+ throw this.wrapError(error, 'Failed to get NFT metadata batch');
1015
+ }
1016
+ }
1017
+
1018
+ async getUserNFTsWithMetadata(address, limit = 5) {
1019
+ try {
1020
+ // Validate address before proceeding
1021
+ if (!address || typeof address !== 'string') {
1022
+ throw new Error('Invalid address provided to getUserNFTsWithMetadata');
1023
+ }
1024
+
1025
+ // Get all NFT IDs for the user
1026
+ const nftIds = await this.getUserNFTs(address);
1027
+
1028
+ // If no NFTs, return empty array
1029
+ if (!nftIds || nftIds.length === 0) {
1030
+ return [];
1031
+ }
1032
+
1033
+ // Take only the first 'limit' number of NFTs
1034
+ const selectedIds = nftIds.slice(0, limit);
1035
+
1036
+ // Fetch metadata for selected NFTs
1037
+ const metadata = await this.getNFTMetadataBatch(selectedIds);
1038
+
1039
+ // Combine IDs with their metadata
1040
+ const nftsWithMetadata = selectedIds.map((id, index) => ({
1041
+ tokenId: id,
1042
+ metadata: metadata[index]
1043
+ }));
1044
+
1045
+ console.log('NFTs with metadata:', nftsWithMetadata);
1046
+ return nftsWithMetadata;
1047
+ } catch (error) {
1048
+ throw this.wrapError(error, 'Failed to get user NFTs with metadata');
1049
+ }
1050
+ }
1051
+
1052
+ async balanceMint(amount) {
1053
+ try {
1054
+ // Emit pending event
1055
+ this.eventBus.emit('transaction:pending', { type: 'mint' });
1056
+
1057
+ const receipt = await this.executeContractCall(
1058
+ 'balanceMint',
1059
+ [amount],
1060
+ { requiresSigner: true }
1061
+ );
1062
+
1063
+ // Emit success event
1064
+ this.eventBus.emit('transaction:success', {
1065
+ type: 'mint',
1066
+ receipt,
1067
+ amount: amount
1068
+ });
1069
+
1070
+ return receipt;
1071
+ } catch (error) {
1072
+ // Emit error event
1073
+ this.eventBus.emit('transaction:error', {
1074
+ type: 'mint',
1075
+ error: this.wrapError(error, 'Failed to mint NFTs')
1076
+ });
1077
+ throw error;
1078
+ }
1079
+ }
1080
+
1081
+ async transferNFT(address, recipient, tokenId) {
1082
+ try {
1083
+ // Emit pending event
1084
+ this.eventBus.emit('transaction:pending', { type: 'send' });
1085
+ const receipt = await this.executeContractCall('transferFrom', [address, recipient, tokenId], { requiresSigner: true, useContract: 'mirror' });
1086
+ // Emit success event
1087
+ this.eventBus.emit('transaction:success', {
1088
+ type: 'send',
1089
+ receipt,
1090
+ tokenId: tokenId,
1091
+ recipient: recipient
1092
+ });
1093
+ return receipt;
1094
+ } catch (error) {
1095
+ this.eventBus.emit('transaction:error', {
1096
+ type: 'send',
1097
+ error: this.wrapError(error, 'Failed to send NFT')
1098
+ });
1099
+ throw error;
1100
+ }
1101
+ }
1102
+
1103
+ /**
1104
+ * Get skipNFT status for an address
1105
+ * @param {string} address - The address to check
1106
+ * @returns {Promise<boolean>} True if skipNFT is set, false otherwise
1107
+ */
1108
+ async getSkipNFT(address) {
1109
+ try {
1110
+ const skipNFT = await this.executeContractCall('getSkipNFT', [address]);
1111
+ return skipNFT;
1112
+ } catch (error) {
1113
+ throw this.wrapError(error, 'Failed to get skipNFT status');
1114
+ }
1115
+ }
1116
+
1117
+ /**
1118
+ * Set skipNFT status for the connected wallet
1119
+ * @param {boolean} skipNFT - True to skip NFT minting, false to enable it
1120
+ * @returns {Promise<Object>} Transaction receipt
1121
+ */
1122
+ async setSkipNFT(skipNFT) {
1123
+ try {
1124
+ if (!this.signer) {
1125
+ throw new Error('No wallet connected');
1126
+ }
1127
+
1128
+ // Emit pending event
1129
+ this.eventBus.emit('transaction:pending', { type: 'setSkipNFT', skipNFT });
1130
+
1131
+ // Call setSkipNFT on the contract
1132
+ const receipt = await this.executeContractCall(
1133
+ 'setSkipNFT',
1134
+ [skipNFT],
1135
+ { requiresSigner: true }
1136
+ );
1137
+
1138
+ // Emit success event
1139
+ this.eventBus.emit('transaction:success', {
1140
+ type: 'setSkipNFT',
1141
+ receipt,
1142
+ skipNFT: skipNFT
1143
+ });
1144
+
1145
+ return receipt;
1146
+ } catch (error) {
1147
+ this.eventBus.emit('transaction:error', {
1148
+ type: 'setSkipNFT',
1149
+ error: this.wrapError(error, 'Failed to set skipNFT')
1150
+ });
1151
+ throw error;
1152
+ }
1153
+ }
1154
+
1155
+ /**
1156
+ * Transfer EXEC tokens to self (for re-rolling NFTs)
1157
+ * @param {string} amount - Amount of tokens to transfer (in wei/raw format)
1158
+ * @returns {Promise<Object>} Transaction receipt
1159
+ */
1160
+ async transferTokensToSelf(amount) {
1161
+ try {
1162
+ if (!this.signer) {
1163
+ throw new Error('No wallet connected');
1164
+ }
1165
+
1166
+ const address = await this.signer.getAddress();
1167
+
1168
+ // Emit pending event
1169
+ this.eventBus.emit('transaction:pending', { type: 'reroll' });
1170
+
1171
+ // Transfer tokens to self
1172
+ const receipt = await this.executeContractCall(
1173
+ 'transfer',
1174
+ [address, amount],
1175
+ { requiresSigner: true }
1176
+ );
1177
+
1178
+ // Emit success event
1179
+ this.eventBus.emit('transaction:success', {
1180
+ type: 'reroll',
1181
+ receipt,
1182
+ amount: amount
1183
+ });
1184
+
1185
+ return receipt;
1186
+ } catch (error) {
1187
+ this.eventBus.emit('transaction:error', {
1188
+ type: 'reroll',
1189
+ error: this.wrapError(error, 'Failed to re-roll NFTs')
1190
+ });
1191
+ throw error;
1192
+ }
1193
+ }
1194
+
1195
+ /**
1196
+ * Convert ETH amount to Wei
1197
+ * @param {string} ethAmount
1198
+ * @returns {string} Amount in Wei
1199
+ */
1200
+ parseEther(ethAmount) {
1201
+ return ethers.utils.parseEther(ethAmount).toString();
1202
+ }
1203
+
1204
+ formatEther(weiAmount) {
1205
+ return parseFloat(ethers.utils.formatEther(weiAmount));
1206
+ }
1207
+
1208
+ /**
1209
+ * Convert EXEC amount to BigNumber string with 18 decimals
1210
+ * @param {string} execAmount
1211
+ * @returns {string} BigNumber string with proper decimals
1212
+ */
1213
+ parseExec(execAmount) {
1214
+ return ethers.utils.parseUnits(execAmount, 18).toString();
1215
+ }
1216
+
1217
+ formatExec(weiAmount) {
1218
+ return parseFloat(ethers.utils.formatUnits(weiAmount, 18));
1219
+ }
1220
+
1221
+ /**
1222
+ * Fetches the reserves of token0 and token1 from the Uniswap V2 pair contract
1223
+ * and calculates the price of token0 in terms of token1.
1224
+ * @returns {Promise<number>} The price of token0 in terms of token1.
1225
+ */
1226
+ async getToken0PriceInToken1(pairAddress) {
1227
+ try {
1228
+ const pairContract = new this.ethers.Contract(
1229
+ pairAddress,
1230
+ [
1231
+ "function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)"
1232
+ ],
1233
+ this.provider,
1234
+ { useContract: 'v2pool' }
1235
+ );
1236
+
1237
+ const [reserve0, reserve1] = await pairContract.getReserves();
1238
+ const price = reserve1 / reserve0;
1239
+
1240
+ return price;
1241
+ } catch (error) {
1242
+ throw this.wrapError(error, 'Failed to get token0 price in terms of token1');
1243
+ }
1244
+ }
1245
+
1246
+ /**
1247
+ * Checks if a given token address is token0 in the Uniswap V2 pair.
1248
+ * @param {string} pairAddress - The address of the Uniswap V2 pair contract.
1249
+ * @param {string} tokenAddress - The known contract address of the token to check.
1250
+ * @returns {Promise<boolean>} True if the token is token0, false otherwise.
1251
+ */
1252
+ async isToken0(pairAddress, tokenAddress) {
1253
+ try {
1254
+ const pairContract = new this.ethers.Contract(
1255
+ pairAddress,
1256
+ [
1257
+ "function token0() external view returns (address)",
1258
+ "function token1() external view returns (address)"
1259
+ ],
1260
+ this.provider,
1261
+ { useContract: 'v2pool' }
1262
+ );
1263
+
1264
+ const token0Address = await pairContract.token0();
1265
+ return token0Address.toLowerCase() === tokenAddress.toLowerCase();
1266
+ } catch (error) {
1267
+ throw this.wrapError(error, 'Failed to check if token is token0');
1268
+ }
1269
+ }
1270
+
1271
+ async getApproval(address, target = null) {
1272
+ try {
1273
+ if (!address) {
1274
+ throw new Error("User address is required for approval check");
1275
+ }
1276
+
1277
+ let targetAddress;
1278
+
1279
+ if (target === null) {
1280
+ targetAddress = this.swapRouter.address || this.swapRouter;
1281
+ } else if (typeof target === 'string') {
1282
+ targetAddress = target;
1283
+ } else if (target && typeof target === 'object' && target.address) {
1284
+ // If target is a contract object, use its address
1285
+ targetAddress = target.address;
1286
+ } else {
1287
+ throw new Error("Invalid target for approval check");
1288
+ }
1289
+
1290
+ console.log(`Checking allowance for ${address} to spend tokens at ${targetAddress}`);
1291
+
1292
+ const response = await this.executeContractCall('allowance', [address, targetAddress]);
1293
+ // Convert BigNumber response to string
1294
+ return response.toString();
1295
+ } catch (error) {
1296
+ throw this.wrapError(error, 'Failed to get approval');
1297
+ }
1298
+ }
1299
+
1300
+ async setApproval(target = null, amount) {
1301
+ try {
1302
+ let targetAddress;
1303
+
1304
+ if (target === null) {
1305
+ targetAddress = this.swapRouter.address;
1306
+ } else if (typeof target === 'string') {
1307
+ targetAddress = target;
1308
+ } else if (target && typeof target === 'object' && target.address) {
1309
+ // If target is a contract object, use its address
1310
+ targetAddress = target.address;
1311
+ } else {
1312
+ throw new Error("Invalid target for approval");
1313
+ }
1314
+
1315
+ console.log(`Setting approval for ${targetAddress} to spend ${amount} tokens`);
1316
+
1317
+ // Create a unique transaction ID for tracking
1318
+ const txId = `tx_approve_${++this.transactionCounter}_${Date.now()}`;
1319
+
1320
+ // Emit pending event with ID for UI feedback
1321
+ const pendingEvent = {
1322
+ type: 'approve',
1323
+ id: txId,
1324
+ pending: true
1325
+ };
1326
+
1327
+ // Store active transaction
1328
+ this.activeTransactions.set(txId, pendingEvent);
1329
+
1330
+ // Emit event
1331
+ console.log(`[BlockchainService] Emitting transaction:pending for approve ${txId}`);
1332
+ this.eventBus.emit('transaction:pending', pendingEvent);
1333
+
1334
+ // Execute the approve call
1335
+ const response = await this.executeContractCall('approve', [targetAddress, amount], { requiresSigner: true });
1336
+
1337
+ // Emit success event with same ID
1338
+ const successEvent = {
1339
+ type: 'approve',
1340
+ id: txId,
1341
+ receipt: response,
1342
+ amount: amount
1343
+ };
1344
+
1345
+ // Update transaction status
1346
+ this.activeTransactions.set(txId, successEvent);
1347
+
1348
+ console.log(`[BlockchainService] Emitting transaction:success for ${txId}`);
1349
+ this.eventBus.emit('transaction:success', successEvent);
1350
+
1351
+ return response;
1352
+ } catch (error) {
1353
+ const errorEvent = {
1354
+ type: 'approve',
1355
+ id: `error_approve_${++this.transactionCounter}_${Date.now()}`,
1356
+ error: this.wrapError(error, 'Failed to set approval')
1357
+ };
1358
+
1359
+ console.log(`[BlockchainService] Emitting transaction:error for approval error`);
1360
+ this.eventBus.emit('transaction:error', errorEvent);
1361
+
1362
+ throw error;
1363
+ }
1364
+ }
1365
+ //now used within swapExactTokenForEthSupportingFeeOnTransferV2
1366
+ // async getAmountsOut(amountIn, path) {
1367
+ // try {
1368
+ // const amounts = await this.executeContractCall(
1369
+ // 'getAmountsOut',
1370
+ // [amountIn, path],
1371
+ // { useContract: 'router' }
1372
+ // );
1373
+ // return amounts;
1374
+ // } catch (error) {
1375
+ // throw this.wrapError(error, 'Failed to get amounts out');
1376
+ // }
1377
+ // }
1378
+
1379
+ async swapExactTokenForEthSupportingFeeOnTransferV2(address, params) {
1380
+ try {
1381
+ // Create a unique transaction ID for tracking
1382
+ const txId = `tx_${++this.transactionCounter}_${Date.now()}`;
1383
+
1384
+ // Emit pending event with ID
1385
+ const pendingEvent = {
1386
+ type: 'swap',
1387
+ id: txId,
1388
+ pending: true
1389
+ };
1390
+
1391
+ // Store active transaction
1392
+ this.activeTransactions.set(txId, pendingEvent);
1393
+
1394
+ // Emit event
1395
+ console.log(`[BlockchainService] Emitting transaction:pending for ${txId}`);
1396
+ this.eventBus.emit('transaction:pending', pendingEvent);
1397
+
1398
+ const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
1399
+ const TOKEN = '0x185485bF2e26e0Da48149aee0A8032c8c2060Db2';
1400
+ const path = [TOKEN, WETH];
1401
+
1402
+ // Get the connected address for the 'to' parameter
1403
+ const to = address;
1404
+
1405
+ // Set deadline to 20 minutes from now
1406
+ const deadline = Math.floor(Date.now() / 1000) + 1200;
1407
+
1408
+ // Calculate minimum amount out accounting for 4% tax + 2% slippage
1409
+ const amounts = await this.executeContractCall(
1410
+ 'getAmountsOut',
1411
+ [params.amount, path],
1412
+ { useContract: 'router' }
1413
+ );
1414
+ const expectedAmountOut = amounts[1];
1415
+
1416
+ // Apply 6% buffer for tax + slippage
1417
+ const amountOutMin = BigInt(expectedAmountOut) * BigInt(940) / BigInt(1000);
1418
+
1419
+ console.log('Sell transaction parameters:', {
1420
+ amountIn: params.amount.toString(),
1421
+ amountOutMin: amountOutMin.toString(),
1422
+ path,
1423
+ txId
1424
+ });
1425
+
1426
+ const receipt = await this.executeContractCall(
1427
+ 'swapExactTokensForETHSupportingFeeOnTransferTokens',
1428
+ [
1429
+ params.amount, // amountIn
1430
+ amountOutMin, // amountOutMin with tax + slippage buffer
1431
+ path,
1432
+ to,
1433
+ deadline
1434
+ ],
1435
+ { useContract: 'router', requiresSigner: true }
1436
+ );
1437
+
1438
+ // Emit success event with same ID
1439
+ const successEvent = {
1440
+ type: 'swap',
1441
+ id: txId,
1442
+ receipt,
1443
+ amount: params.amount
1444
+ };
1445
+
1446
+ // Update transaction status
1447
+ this.activeTransactions.set(txId, successEvent);
1448
+
1449
+ console.log(`[BlockchainService] Emitting transaction:success for ${txId}`);
1450
+ this.eventBus.emit('transaction:success', successEvent);
1451
+
1452
+ // Remove from active transactions after a delay
1453
+ setTimeout(() => {
1454
+ this.activeTransactions.delete(txId);
1455
+ }, 1000);
1456
+
1457
+ return receipt;
1458
+ } catch (error) {
1459
+ const errorEvent = {
1460
+ type: 'swap',
1461
+ id: `error_${++this.transactionCounter}_${Date.now()}`,
1462
+ error: this.wrapError(error, 'Failed to swap tokens for ETH')
1463
+ };
1464
+
1465
+ console.log(`[BlockchainService] Emitting transaction:error for error transaction`);
1466
+ this.eventBus.emit('transaction:error', errorEvent);
1467
+ throw error;
1468
+ }
1469
+ }
1470
+
1471
+ async swapExactEthForTokenSupportingFeeOnTransfer(address, params, ethValue) {
1472
+ try {
1473
+ // Create a unique transaction ID for tracking
1474
+ const txId = `tx_${++this.transactionCounter}_${Date.now()}`;
1475
+
1476
+ // Emit pending event with ID
1477
+ const pendingEvent = {
1478
+ type: 'swap',
1479
+ id: txId,
1480
+ pending: true
1481
+ };
1482
+
1483
+ // Store active transaction
1484
+ this.activeTransactions.set(txId, pendingEvent);
1485
+
1486
+ // Emit event
1487
+ console.log(`[BlockchainService] Emitting transaction:pending for ${txId}`);
1488
+ this.eventBus.emit('transaction:pending', pendingEvent);
1489
+
1490
+ const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
1491
+ const TOKEN = '0x185485bF2e26e0Da48149aee0A8032c8c2060Db2';
1492
+ const path = [WETH, TOKEN];
1493
+
1494
+ // Get the connected address for the 'to' parameter
1495
+ const to = address;
1496
+
1497
+ // Set deadline to 20 minutes from now
1498
+ const deadline = Math.floor(Date.now() / 1000) + 1200;
1499
+
1500
+ // For 4% tax tokens, we need a much lower amountOutMin
1501
+ // Calculate minimum expected output with 6% buffer (4% tax + 2% slippage)
1502
+ const amountOutMin = BigInt(params.amount) * BigInt(940) / BigInt(1000);
1503
+
1504
+ console.log('Buy transaction parameters:', {
1505
+ amountOutMin: amountOutMin.toString(),
1506
+ ethValue: ethValue.toString(),
1507
+ path,
1508
+ txId
1509
+ });
1510
+
1511
+ const receipt = await this.executeContractCall(
1512
+ 'swapExactETHForTokensSupportingFeeOnTransferTokens',
1513
+ [
1514
+ amountOutMin, // amountOutMin with 6% buffer for tax + slippage
1515
+ path,
1516
+ to,
1517
+ deadline
1518
+ ],
1519
+ { useContract: 'router', requiresSigner: true, txOptions: { value: ethValue } }
1520
+ );
1521
+
1522
+ // Emit success event with same ID
1523
+ const successEvent = {
1524
+ type: 'swap',
1525
+ id: txId,
1526
+ receipt,
1527
+ amount: params.amount
1528
+ };
1529
+
1530
+ // Update transaction status
1531
+ this.activeTransactions.set(txId, successEvent);
1532
+
1533
+ console.log(`[BlockchainService] Emitting transaction:success for ${txId}`);
1534
+ this.eventBus.emit('transaction:success', successEvent);
1535
+
1536
+ // Remove from active transactions after a delay
1537
+ setTimeout(() => {
1538
+ this.activeTransactions.delete(txId);
1539
+ }, 1000);
1540
+
1541
+ return receipt;
1542
+ } catch (error) {
1543
+ const errorEvent = {
1544
+ type: 'swap',
1545
+ id: `error_${++this.transactionCounter}_${Date.now()}`,
1546
+ error: this.wrapError(error, 'Failed to swap ETH for tokens')
1547
+ };
1548
+
1549
+ console.log(`[BlockchainService] Emitting transaction:error for error transaction`);
1550
+ this.eventBus.emit('transaction:error', errorEvent);
1551
+ throw error;
1552
+ }
1553
+ }
1554
+
1555
+ /**
1556
+ * Get the current block information
1557
+ * @returns {Promise<Object>} Block information
1558
+ */
1559
+ async getCurrentBlockInfo() {
1560
+ try {
1561
+ const blockNumber = await this.provider.getBlockNumber();
1562
+ const block = await this.provider.getBlock(blockNumber);
1563
+
1564
+ return {
1565
+ number: blockNumber,
1566
+ timestamp: block.timestamp,
1567
+ hash: block.hash,
1568
+ date: new Date(block.timestamp * 1000) // Convert seconds to milliseconds
1569
+ };
1570
+ } catch (error) {
1571
+ throw this.wrapError(error, 'Failed to get current block info');
1572
+ }
1573
+ }
1574
+ }
1575
+
1576
+ export default BlockchainService;