@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.
- package/README.md +87 -0
- package/dist/micro-web3.cjs.js +10 -0
- package/dist/micro-web3.cjs.js.map +1 -0
- package/dist/micro-web3.esm.js +10 -0
- package/dist/micro-web3.esm.js.map +1 -0
- package/dist/micro-web3.umd.js +10 -0
- package/dist/micro-web3.umd.js.map +1 -0
- package/package.json +34 -0
- package/rollup.config.cjs +36 -0
- package/src/components/BondingCurve/BondingCurve.js +296 -0
- package/src/components/Display/BalanceDisplay.js +81 -0
- package/src/components/Display/PriceDisplay.js +214 -0
- package/src/components/Ipfs/IpfsImage.js +265 -0
- package/src/components/Modal/ApprovalModal.js +398 -0
- package/src/components/Swap/SwapButton.js +81 -0
- package/src/components/Swap/SwapInputs.js +137 -0
- package/src/components/Swap/SwapInterface.js +972 -0
- package/src/components/Swap/TransactionOptions.js +238 -0
- package/src/components/Util/MessagePopup.js +159 -0
- package/src/components/Wallet/WalletModal.js +69 -0
- package/src/components/Wallet/WalletSplash.js +567 -0
- package/src/index.js +43 -0
- package/src/services/BlockchainService.js +1576 -0
- package/src/services/ContractCache.js +348 -0
- package/src/services/IpfsService.js +249 -0
- package/src/services/PriceService.js +191 -0
- package/src/services/WalletService.js +541 -0
|
@@ -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;
|