@cryptforge/blockchain-evm 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/dist/index.mjs ADDED
@@ -0,0 +1,1140 @@
1
+ import { ethers } from 'ethers';
2
+
3
+ // src/EVMAdapter.ts
4
+ var EVMAdapter = class {
5
+ /**
6
+ * Create a new EVM blockchain adapter
7
+ * @param config - Chain configuration
8
+ */
9
+ constructor(config) {
10
+ this.chainData = config.chainData;
11
+ this.coinType = config.coinType ?? 60;
12
+ this.standardDecimals = config.standardDecimals ?? 18;
13
+ this.account = config.account ?? 0;
14
+ this.change = config.change ?? 0;
15
+ this.addressIndex = config.addressIndex ?? 0;
16
+ this.networks = {};
17
+ if (config.networks) {
18
+ for (const [key, value] of Object.entries(config.networks)) {
19
+ if (value !== void 0) {
20
+ this.networks[key] = value;
21
+ }
22
+ }
23
+ }
24
+ this.currentNetworkKey = config.defaultNetwork ?? "mainnet";
25
+ this.dataProvider = config.dataProvider;
26
+ }
27
+ /**
28
+ * Get the BIP44 derivation path for this chain
29
+ * @returns BIP44 path string (e.g., "m/44'/60'/0'/0/0")
30
+ */
31
+ getDerivationPath() {
32
+ return `m/44'/${this.coinType}'/${this.account}'/${this.change}/${this.addressIndex}`;
33
+ }
34
+ /**
35
+ * Get the current network configuration
36
+ * @returns Current network configuration, or undefined if no network is configured
37
+ */
38
+ getNetwork() {
39
+ return this.networks[this.currentNetworkKey];
40
+ }
41
+ /**
42
+ * Get the current network key (e.g., 'mainnet', 'testnet')
43
+ * @returns Current network key
44
+ */
45
+ getCurrentNetworkKey() {
46
+ return this.currentNetworkKey;
47
+ }
48
+ /**
49
+ * Set the active network
50
+ * @param networkKey - Network key to switch to (e.g., 'mainnet', 'testnet')
51
+ * @throws Error if the network key doesn't exist
52
+ */
53
+ setNetwork(networkKey) {
54
+ if (!this.networks[networkKey]) {
55
+ throw new Error(
56
+ `Network "${networkKey}" is not configured for ${this.chainData.name}. Available networks: ${Object.keys(this.networks).join(", ")}`
57
+ );
58
+ }
59
+ this.currentNetworkKey = networkKey;
60
+ }
61
+ /**
62
+ * Get all available network keys
63
+ * @returns Array of network keys (e.g., ['mainnet', 'testnet'])
64
+ */
65
+ getAvailableNetworks() {
66
+ return Object.keys(this.networks);
67
+ }
68
+ /**
69
+ * Check if a network is configured
70
+ * @param networkKey - Network key to check
71
+ * @returns True if the network exists
72
+ */
73
+ hasNetwork(networkKey) {
74
+ return networkKey in this.networks;
75
+ }
76
+ /**
77
+ * Derive EVM keys from a BIP39 mnemonic.
78
+ * Uses the BIP44 derivation path specified in the configuration.
79
+ *
80
+ * @param mnemonic - BIP39 mnemonic phrase
81
+ * @returns KeyData with derived keys and address
82
+ */
83
+ async deriveKeys(mnemonic) {
84
+ if (!ethers.Mnemonic.isValidMnemonic(mnemonic)) {
85
+ throw new Error("Invalid mnemonic phrase");
86
+ }
87
+ const path = this.getDerivationPath();
88
+ const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic, void 0, path);
89
+ const privateKeyHex = hdNode.privateKey;
90
+ const privateKeyBuffer = this.hexToBytes(privateKeyHex.slice(2));
91
+ const publicKeyHex = hdNode.publicKey;
92
+ const publicKeyBuffer = this.hexToBytes(publicKeyHex.slice(2));
93
+ const address = hdNode.address;
94
+ const seed = this.hexToBytes(ethers.id(mnemonic).slice(2));
95
+ return {
96
+ mnemonic,
97
+ seed,
98
+ privateKey: privateKeyBuffer,
99
+ privateKeyHex: privateKeyHex.slice(2),
100
+ // Without 0x prefix
101
+ publicKey: publicKeyBuffer,
102
+ publicKeyHex: publicKeyHex.slice(2),
103
+ // Without 0x prefix
104
+ address,
105
+ path
106
+ };
107
+ }
108
+ /**
109
+ * Derive keys at a specific address index
110
+ * @param mnemonic - BIP39 mnemonic phrase
111
+ * @param addressIndex - Address index in the BIP44 path
112
+ * @returns KeyData with derived keys
113
+ */
114
+ async deriveKeysAtIndex(mnemonic, addressIndex) {
115
+ if (!ethers.Mnemonic.isValidMnemonic(mnemonic)) {
116
+ throw new Error("Invalid mnemonic phrase");
117
+ }
118
+ const path = `m/44'/${this.coinType}'/${this.account}'/${this.change}/${addressIndex}`;
119
+ const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic, void 0, path);
120
+ const privateKeyHex = hdNode.privateKey;
121
+ const privateKeyBuffer = this.hexToBytes(privateKeyHex.slice(2));
122
+ const publicKeyHex = hdNode.publicKey;
123
+ const publicKeyBuffer = this.hexToBytes(publicKeyHex.slice(2));
124
+ const address = hdNode.address;
125
+ const seed = this.hexToBytes(ethers.id(mnemonic).slice(2));
126
+ return {
127
+ mnemonic,
128
+ seed,
129
+ privateKey: privateKeyBuffer,
130
+ privateKeyHex: privateKeyHex.slice(2),
131
+ // Without 0x prefix
132
+ publicKey: publicKeyBuffer,
133
+ publicKeyHex: publicKeyHex.slice(2),
134
+ // Without 0x prefix
135
+ address,
136
+ path
137
+ };
138
+ }
139
+ /**
140
+ * Derive keys at a custom derivation path
141
+ * @param mnemonic - BIP39 mnemonic phrase
142
+ * @param path - Full BIP44 derivation path
143
+ * @returns KeyData with derived keys
144
+ */
145
+ async deriveKeysAtPath(mnemonic, path) {
146
+ if (!ethers.Mnemonic.isValidMnemonic(mnemonic)) {
147
+ throw new Error("Invalid mnemonic phrase");
148
+ }
149
+ const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic, void 0, path);
150
+ const privateKeyHex = hdNode.privateKey;
151
+ const privateKeyBuffer = this.hexToBytes(privateKeyHex.slice(2));
152
+ const publicKeyHex = hdNode.publicKey;
153
+ const publicKeyBuffer = this.hexToBytes(publicKeyHex.slice(2));
154
+ const address = hdNode.address;
155
+ const seed = this.hexToBytes(ethers.id(mnemonic).slice(2));
156
+ return {
157
+ mnemonic,
158
+ seed,
159
+ privateKey: privateKeyBuffer,
160
+ privateKeyHex: privateKeyHex.slice(2),
161
+ // Without 0x prefix
162
+ publicKey: publicKeyBuffer,
163
+ publicKeyHex: publicKeyHex.slice(2),
164
+ // Without 0x prefix
165
+ address,
166
+ path
167
+ };
168
+ }
169
+ /**
170
+ * Get address at a specific index (read-only, no private key)
171
+ * @param mnemonic - BIP39 mnemonic phrase
172
+ * @param index - Address index
173
+ * @returns Address and public key information
174
+ */
175
+ async getAddressAtIndex(mnemonic, index) {
176
+ if (!ethers.Mnemonic.isValidMnemonic(mnemonic)) {
177
+ throw new Error("Invalid mnemonic phrase");
178
+ }
179
+ const path = `m/44'/${this.coinType}'/${this.account}'/${this.change}/${index}`;
180
+ const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic, void 0, path);
181
+ return {
182
+ address: hdNode.address,
183
+ publicKey: hdNode.publicKey.slice(2),
184
+ // Without 0x prefix
185
+ path
186
+ };
187
+ }
188
+ /**
189
+ * Get multiple addresses starting from an index
190
+ * @param mnemonic - BIP39 mnemonic phrase
191
+ * @param startIndex - Starting address index
192
+ * @param count - Number of addresses to generate
193
+ * @returns Array of addresses with metadata
194
+ */
195
+ async getAddresses(mnemonic, startIndex, count) {
196
+ if (!ethers.Mnemonic.isValidMnemonic(mnemonic)) {
197
+ throw new Error("Invalid mnemonic phrase");
198
+ }
199
+ const addresses = [];
200
+ for (let i = 0; i < count; i++) {
201
+ const index = startIndex + i;
202
+ const path = `m/44'/${this.coinType}'/${this.account}'/${this.change}/${index}`;
203
+ const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic, void 0, path);
204
+ addresses.push({
205
+ address: hdNode.address,
206
+ path,
207
+ index
208
+ });
209
+ }
210
+ return addresses;
211
+ }
212
+ /**
213
+ * Sign a message with a private key
214
+ * @param privateKey - Private key as Uint8Array
215
+ * @param message - Message to sign (string or Uint8Array)
216
+ * @returns Signature
217
+ */
218
+ async signMessage(privateKey, message) {
219
+ const privateKeyHex = "0x" + this.bytesToHex(privateKey);
220
+ const wallet = new ethers.Wallet(privateKeyHex);
221
+ const messageStr = typeof message === "string" ? message : new TextDecoder().decode(message);
222
+ const signature = await wallet.signMessage(messageStr);
223
+ return { signature };
224
+ }
225
+ /**
226
+ * Sign a transaction with a private key
227
+ * @param privateKey - Private key as Uint8Array
228
+ * @param transaction - Chain-specific transaction object
229
+ * @returns Signed transaction and signature
230
+ */
231
+ async signTransaction(privateKey, transaction) {
232
+ const privateKeyHex = "0x" + this.bytesToHex(privateKey);
233
+ const wallet = new ethers.Wallet(privateKeyHex);
234
+ const signedTransaction = await wallet.signTransaction(transaction);
235
+ const tx = ethers.Transaction.from(signedTransaction);
236
+ const signature = tx.signature ? ethers.Signature.from(tx.signature).serialized : "";
237
+ return {
238
+ signedTransaction,
239
+ signature
240
+ };
241
+ }
242
+ /**
243
+ * Verify a signature against a public key
244
+ * @param message - Original message
245
+ * @param signature - Signature to verify
246
+ * @param publicKey - Public key (hex string)
247
+ * @returns True if signature is valid
248
+ */
249
+ async verifySignature(message, signature, publicKey) {
250
+ try {
251
+ const messageStr = typeof message === "string" ? message : new TextDecoder().decode(message);
252
+ const recoveredAddress = ethers.verifyMessage(messageStr, signature);
253
+ const publicKeyWithPrefix = publicKey.startsWith("0x") ? publicKey : "0x" + publicKey;
254
+ const computedAddress = ethers.computeAddress(publicKeyWithPrefix);
255
+ return recoveredAddress.toLowerCase() === computedAddress.toLowerCase();
256
+ } catch (error) {
257
+ return false;
258
+ }
259
+ }
260
+ /**
261
+ * Set or update the data provider
262
+ * @param provider - The blockchain data provider to use
263
+ */
264
+ setDataProvider(provider) {
265
+ this.dataProvider = provider;
266
+ }
267
+ /**
268
+ * Get the current data provider
269
+ * @returns The current data provider, or undefined if not set
270
+ */
271
+ getDataProvider() {
272
+ return this.dataProvider;
273
+ }
274
+ /**
275
+ * Ensure a data provider is configured
276
+ * @throws Error if no data provider is set
277
+ */
278
+ ensureProvider() {
279
+ if (!this.dataProvider) {
280
+ throw new Error(
281
+ `No data provider configured for ${this.chainData.name}. Please provide a BlockchainDataProvider (e.g., AlchemyProvider) in the adapter configuration.`
282
+ );
283
+ }
284
+ }
285
+ /**
286
+ * Get native token balance for an address
287
+ * @param address - Wallet address
288
+ * @returns TokenBalance object with native token information
289
+ * @throws Error if no data provider is configured
290
+ */
291
+ async getNativeBalance(address) {
292
+ this.ensureProvider();
293
+ return this.dataProvider.getNativeBalance(address);
294
+ }
295
+ /**
296
+ * Get all ERC-20 token balances for an address
297
+ * @param address - Wallet address
298
+ * @returns Array of token balances with metadata
299
+ * @throws Error if no data provider is configured
300
+ */
301
+ async getTokenBalances(address) {
302
+ this.ensureProvider();
303
+ return this.dataProvider.getTokenBalances(address);
304
+ }
305
+ /**
306
+ * Get balance for a specific ERC-20 token
307
+ * @param address - Wallet address
308
+ * @param tokenAddress - Token contract address
309
+ * @returns Balance in smallest unit as string
310
+ * @throws Error if no data provider is configured
311
+ */
312
+ async getTokenBalance(address, tokenAddress) {
313
+ this.ensureProvider();
314
+ return this.dataProvider.getTokenBalance(address, tokenAddress);
315
+ }
316
+ /**
317
+ * Get all transactions for an address
318
+ * @param address - Wallet address
319
+ * @param options - Optional filtering and pagination options
320
+ * @returns Array of transactions
321
+ * @throws Error if no data provider is configured
322
+ */
323
+ async getTransactions(address, options) {
324
+ this.ensureProvider();
325
+ return this.dataProvider.getTransactions(address, options);
326
+ }
327
+ /**
328
+ * Get all ERC-20 token transfers for an address
329
+ * @param address - Wallet address
330
+ * @param options - Optional filtering and pagination options
331
+ * @returns Array of token transfers
332
+ * @throws Error if no data provider is configured
333
+ */
334
+ async getTokenTransfers(address, options) {
335
+ this.ensureProvider();
336
+ return this.dataProvider.getTokenTransfers(address, options);
337
+ }
338
+ /**
339
+ * Validate if an address is a valid ERC-20 token contract
340
+ * @param tokenAddress - Token contract address
341
+ * @returns True if valid ERC-20, false otherwise
342
+ */
343
+ async validateTokenContract(tokenAddress) {
344
+ try {
345
+ const network = this.getNetwork();
346
+ if (!network) {
347
+ throw new Error(`No network configured for ${this.chainData.name}`);
348
+ }
349
+ const provider = new ethers.JsonRpcProvider(network.rpcUrl);
350
+ const contract = new ethers.Contract(
351
+ tokenAddress,
352
+ ["function symbol() view returns (string)"],
353
+ provider
354
+ );
355
+ const symbol = await contract.symbol();
356
+ return typeof symbol === "string" && symbol.length > 0;
357
+ } catch (error) {
358
+ return false;
359
+ }
360
+ }
361
+ /**
362
+ * Send native tokens (ETH, S, etc.)
363
+ * @param params - Transaction parameters including private key
364
+ * @returns Transaction receipt
365
+ */
366
+ async sendNativeToken(params) {
367
+ try {
368
+ const network = this.getNetwork();
369
+ if (!network) {
370
+ throw new Error(`No network configured for ${this.chainData.name}`);
371
+ }
372
+ const privateKeyHex = "0x" + this.bytesToHex(params.privateKey);
373
+ const wallet = new ethers.Wallet(privateKeyHex);
374
+ const provider = new ethers.JsonRpcProvider(network.rpcUrl);
375
+ const connectedSigner = wallet.connect(provider);
376
+ const tx = await connectedSigner.sendTransaction({
377
+ to: params.to,
378
+ value: params.amount
379
+ });
380
+ return {
381
+ hash: tx.hash,
382
+ from: tx.from,
383
+ to: tx.to,
384
+ value: tx.value.toString(),
385
+ chainId: network.chainId,
386
+ status: "pending",
387
+ nonce: tx.nonce
388
+ };
389
+ } catch (error) {
390
+ throw new Error(
391
+ `Failed to send native token: ${error instanceof Error ? error.message : String(error)}`
392
+ );
393
+ }
394
+ }
395
+ /**
396
+ * Send ERC-20 tokens
397
+ * Private key must be obtained from AuthClient after password verification
398
+ * @param params - Transaction parameters including private key and token address
399
+ * @returns Transaction receipt
400
+ */
401
+ async sendToken(params) {
402
+ try {
403
+ const isValidToken = await this.validateTokenContract(
404
+ params.tokenAddress
405
+ );
406
+ if (!isValidToken) {
407
+ throw new Error(
408
+ `Invalid ERC-20 token contract: ${params.tokenAddress}`
409
+ );
410
+ }
411
+ const network = this.getNetwork();
412
+ if (!network) {
413
+ throw new Error(`No network configured for ${this.chainData.name}`);
414
+ }
415
+ const privateKeyHex = "0x" + this.bytesToHex(params.privateKey);
416
+ const wallet = new ethers.Wallet(privateKeyHex);
417
+ const provider = new ethers.JsonRpcProvider(network.rpcUrl);
418
+ const connectedSigner = wallet.connect(provider);
419
+ const tokenContract = new ethers.Contract(
420
+ params.tokenAddress,
421
+ [
422
+ "function transfer(address to, uint256 amount) returns (bool)",
423
+ "function decimals() view returns (uint8)",
424
+ "function symbol() view returns (string)"
425
+ ],
426
+ connectedSigner
427
+ );
428
+ const tx = await tokenContract.transfer(params.to, params.amount);
429
+ return {
430
+ hash: tx.hash,
431
+ from: tx.from,
432
+ to: params.tokenAddress,
433
+ // For token transfers, 'to' is the contract address
434
+ value: "0",
435
+ // Native value is 0 for token transfers
436
+ chainId: network.chainId,
437
+ status: "pending",
438
+ nonce: tx.nonce
439
+ };
440
+ } catch (error) {
441
+ throw new Error(
442
+ `Failed to send token: ${error instanceof Error ? error.message : String(error)}`
443
+ );
444
+ }
445
+ }
446
+ /**
447
+ * Wait for transaction confirmation and return detailed status
448
+ * @param hash - Transaction hash
449
+ * @returns Transaction status update
450
+ */
451
+ async getTransactionStatus(hash) {
452
+ try {
453
+ const network = this.getNetwork();
454
+ if (!network) {
455
+ throw new Error(`No network configured for ${this.chainData.name}`);
456
+ }
457
+ const provider = new ethers.JsonRpcProvider(network.rpcUrl);
458
+ const receipt = await provider.getTransactionReceipt(hash);
459
+ if (!receipt) {
460
+ return {
461
+ hash,
462
+ status: "pending"
463
+ };
464
+ }
465
+ const currentBlock = await provider.getBlockNumber();
466
+ const confirmations = currentBlock - receipt.blockNumber + 1;
467
+ return {
468
+ hash,
469
+ status: receipt.status === 1 ? "confirmed" : "failed",
470
+ blockNumber: receipt.blockNumber,
471
+ confirmations,
472
+ gasUsed: receipt.gasUsed.toString(),
473
+ error: receipt.status === 0 ? "Transaction failed" : void 0
474
+ };
475
+ } catch (error) {
476
+ throw new Error(
477
+ `Failed to get transaction status: ${error instanceof Error ? error.message : String(error)}`
478
+ );
479
+ }
480
+ }
481
+ // Helper methods for byte/hex conversion
482
+ bytesToHex(bytes) {
483
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
484
+ }
485
+ hexToBytes(hex) {
486
+ const bytes = new Uint8Array(hex.length / 2);
487
+ for (let i = 0; i < bytes.length; i++) {
488
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
489
+ }
490
+ return bytes;
491
+ }
492
+ };
493
+
494
+ // src/utils/formatBalance.ts
495
+ var formatBalance = (balance, decimals) => {
496
+ const divisor = Math.pow(10, decimals);
497
+ const balanceNum = parseFloat(balance) / divisor;
498
+ return balanceNum.toLocaleString("en-US", {
499
+ minimumFractionDigits: 0,
500
+ maximumFractionDigits: decimals,
501
+ useGrouping: false
502
+ });
503
+ };
504
+
505
+ // src/providers/AlchemyProvider.ts
506
+ var AlchemyProvider = class {
507
+ constructor(config) {
508
+ this.apiKey = config.apiKey;
509
+ this.network = config.network;
510
+ this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
511
+ this.nativeTokenSymbol = config.nativeTokenSymbol;
512
+ this.nativeTokenName = config.nativeTokenName;
513
+ this.nativeTokenDecimals = config.nativeTokenDecimals ?? 18;
514
+ this.cmc_id = config.cmc_id;
515
+ console.log(`\u2705 AlchemyProvider initialized for ${config.network}`);
516
+ }
517
+ /**
518
+ * Get native token balance (e.g., ETH, S) for an address using RPC
519
+ */
520
+ async getNativeBalance(address) {
521
+ try {
522
+ const balance = await this.provider.getBalance(address);
523
+ const balanceStr = balance.toString();
524
+ return {
525
+ contractAddress: "native",
526
+ // Native token doesn't have a contract address
527
+ balance: balanceStr,
528
+ balanceFormatted: formatBalance(balanceStr, this.nativeTokenDecimals),
529
+ symbol: this.nativeTokenSymbol,
530
+ name: this.nativeTokenName,
531
+ decimals: this.nativeTokenDecimals,
532
+ logo: `https://s2.coinmarketcap.com/static/img/coins/128x128/${this.cmc_id}.png`,
533
+ isNFT: false
534
+ };
535
+ } catch (error) {
536
+ throw new Error(
537
+ `Failed to fetch native balance: ${error instanceof Error ? error.message : String(error)}`
538
+ );
539
+ }
540
+ }
541
+ /**
542
+ * Get all ERC-20 token balances for an address using Alchemy Portfolio API
543
+ */
544
+ async getTokenBalances(address) {
545
+ try {
546
+ const url = `https://api.g.alchemy.com/data/v1/${this.apiKey}/assets/tokens/by-address`;
547
+ const response = await fetch(url, {
548
+ method: "POST",
549
+ headers: { "Content-Type": "application/json" },
550
+ body: JSON.stringify({
551
+ addresses: [
552
+ {
553
+ address,
554
+ networks: [this.network]
555
+ }
556
+ ],
557
+ withMetadata: true,
558
+ includeNativeTokens: false,
559
+ // We get native balance separately
560
+ includeErc20Tokens: true
561
+ })
562
+ });
563
+ if (!response.ok) {
564
+ const errorText = await response.text();
565
+ throw new Error(
566
+ `Portfolio API request failed: ${response.status} ${errorText}`
567
+ );
568
+ }
569
+ const data = await response.json();
570
+ const tokens = data.data?.tokens || [];
571
+ const tokenBalances = [];
572
+ for (const token of tokens) {
573
+ if (token.error) {
574
+ console.warn(
575
+ `Error fetching token ${token.tokenAddress}:`,
576
+ token.error
577
+ );
578
+ continue;
579
+ }
580
+ let balance = token.tokenBalance || "0";
581
+ if (balance.startsWith("0x")) {
582
+ balance = BigInt(balance).toString();
583
+ }
584
+ if (balance === "0") {
585
+ continue;
586
+ }
587
+ const decimals = token.tokenMetadata?.decimals ?? 18;
588
+ tokenBalances.push({
589
+ contractAddress: token.tokenAddress,
590
+ balance,
591
+ balanceFormatted: formatBalance(balance, decimals),
592
+ symbol: token.tokenMetadata?.symbol || "UNKNOWN",
593
+ name: token.tokenMetadata?.name || "Unknown Token",
594
+ decimals,
595
+ logo: token.tokenMetadata?.logo,
596
+ // This endpoint returns ERC-20 tokens only, not NFTs
597
+ isNFT: false
598
+ });
599
+ }
600
+ return tokenBalances;
601
+ } catch (error) {
602
+ throw new Error(
603
+ `Failed to fetch token balances: ${error instanceof Error ? error.message : String(error)}`
604
+ );
605
+ }
606
+ }
607
+ /**
608
+ * Get balance for a specific ERC-20 token using RPC
609
+ */
610
+ async getTokenBalance(address, tokenAddress) {
611
+ try {
612
+ const contract = new ethers.Contract(
613
+ tokenAddress,
614
+ ["function balanceOf(address) view returns (uint256)"],
615
+ this.provider
616
+ );
617
+ const balance = await contract.balanceOf(address);
618
+ return balance.toString();
619
+ } catch (error) {
620
+ throw new Error(
621
+ `Failed to fetch token balance: ${error instanceof Error ? error.message : String(error)}`
622
+ );
623
+ }
624
+ }
625
+ /**
626
+ * Get all transactions for an address using Alchemy's getAssetTransfers API
627
+ */
628
+ async getTransactions(address, options = {}) {
629
+ try {
630
+ const transactions = [];
631
+ const sentTransfers = await this.fetchAssetTransfers({
632
+ fromAddress: address,
633
+ category: ["external", "internal"],
634
+ order: options.sort === "asc" ? "asc" : "desc",
635
+ maxCount: options.offset ? `0x${options.offset.toString(16)}` : "0x64",
636
+ // Default 100
637
+ withMetadata: true
638
+ });
639
+ const receivedTransfers = await this.fetchAssetTransfers({
640
+ toAddress: address,
641
+ category: ["external", "internal"],
642
+ order: options.sort === "asc" ? "asc" : "desc",
643
+ maxCount: options.offset ? `0x${options.offset.toString(16)}` : "0x64",
644
+ withMetadata: true
645
+ });
646
+ const allTransfers = [
647
+ ...sentTransfers.transfers,
648
+ ...receivedTransfers.transfers
649
+ ];
650
+ const uniqueHashes = /* @__PURE__ */ new Set();
651
+ for (const transfer of allTransfers) {
652
+ if (uniqueHashes.has(transfer.hash)) continue;
653
+ uniqueHashes.add(transfer.hash);
654
+ transactions.push({
655
+ hash: transfer.hash,
656
+ from: transfer.from,
657
+ to: transfer.to || "",
658
+ value: transfer.value ? transfer.value.toString() : "0",
659
+ timestamp: transfer.metadata?.blockTimestamp ? new Date(transfer.metadata.blockTimestamp).getTime() / 1e3 : 0,
660
+ blockNumber: parseInt(transfer.blockNum, 16),
661
+ gasUsed: "0",
662
+ // Not provided by getAssetTransfers
663
+ gasPrice: "0",
664
+ // Not provided by getAssetTransfers
665
+ status: "success",
666
+ // Transfers API only returns successful transactions
667
+ isError: false
668
+ });
669
+ }
670
+ transactions.sort(
671
+ (a, b) => options.sort === "asc" ? a.blockNumber - b.blockNumber : b.blockNumber - a.blockNumber
672
+ );
673
+ return transactions;
674
+ } catch (error) {
675
+ throw new Error(
676
+ `Failed to fetch transactions: ${error instanceof Error ? error.message : String(error)}`
677
+ );
678
+ }
679
+ }
680
+ /**
681
+ * Get all ERC-20 token transfers for an address using Alchemy's getAssetTransfers API
682
+ */
683
+ async getTokenTransfers(address, options = {}) {
684
+ try {
685
+ const tokenTransfers = [];
686
+ const sentTransfers = await this.fetchAssetTransfers({
687
+ fromAddress: address,
688
+ category: ["erc20"],
689
+ order: options.sort === "asc" ? "asc" : "desc",
690
+ maxCount: options.offset ? `0x${options.offset.toString(16)}` : "0x64",
691
+ withMetadata: true
692
+ });
693
+ const receivedTransfers = await this.fetchAssetTransfers({
694
+ toAddress: address,
695
+ category: ["erc20"],
696
+ order: options.sort === "asc" ? "asc" : "desc",
697
+ maxCount: options.offset ? `0x${options.offset.toString(16)}` : "0x64",
698
+ withMetadata: true
699
+ });
700
+ const allTransfers = [
701
+ ...sentTransfers.transfers,
702
+ ...receivedTransfers.transfers
703
+ ];
704
+ const uniqueIds = /* @__PURE__ */ new Set();
705
+ for (const transfer of allTransfers) {
706
+ if (uniqueIds.has(transfer.uniqueId)) continue;
707
+ uniqueIds.add(transfer.uniqueId);
708
+ tokenTransfers.push({
709
+ hash: transfer.hash,
710
+ from: transfer.from,
711
+ to: transfer.to || "",
712
+ value: transfer.value ? transfer.value.toString() : "0",
713
+ timestamp: transfer.metadata?.blockTimestamp ? new Date(transfer.metadata.blockTimestamp).getTime() / 1e3 : 0,
714
+ blockNumber: parseInt(transfer.blockNum, 16),
715
+ tokenAddress: transfer.rawContract?.address || "",
716
+ tokenSymbol: transfer.asset || "UNKNOWN",
717
+ tokenName: transfer.asset || "Unknown Token",
718
+ tokenDecimal: transfer.rawContract?.decimal || "18"
719
+ });
720
+ }
721
+ tokenTransfers.sort(
722
+ (a, b) => options.sort === "asc" ? a.blockNumber - b.blockNumber : b.blockNumber - a.blockNumber
723
+ );
724
+ return tokenTransfers;
725
+ } catch (error) {
726
+ throw new Error(
727
+ `Failed to fetch token transfers: ${error instanceof Error ? error.message : String(error)}`
728
+ );
729
+ }
730
+ }
731
+ /**
732
+ * Helper method to fetch asset transfers from Alchemy API
733
+ */
734
+ async fetchAssetTransfers(params) {
735
+ const response = await fetch(this.provider._getConnection().url, {
736
+ method: "POST",
737
+ headers: { "Content-Type": "application/json" },
738
+ body: JSON.stringify({
739
+ jsonrpc: "2.0",
740
+ id: 1,
741
+ method: "alchemy_getAssetTransfers",
742
+ params: [
743
+ {
744
+ fromBlock: "0x0",
745
+ toBlock: "latest",
746
+ ...params,
747
+ excludeZeroValue: true
748
+ }
749
+ ]
750
+ })
751
+ });
752
+ if (!response.ok) {
753
+ const errorText = await response.text();
754
+ throw new Error(
755
+ `getAssetTransfers request failed: ${response.status} ${errorText}`
756
+ );
757
+ }
758
+ const data = await response.json();
759
+ if (data.error) {
760
+ throw new Error(`Alchemy API error: ${data.error.message}`);
761
+ }
762
+ return data.result || { transfers: [] };
763
+ }
764
+ };
765
+
766
+ // src/providers/EtherscanProvider.ts
767
+ var EtherscanProvider = class {
768
+ /**
769
+ * Create an Etherscan provider using API V2
770
+ * @param config - Provider configuration
771
+ */
772
+ constructor(config) {
773
+ this.apiKey = config.apiKey;
774
+ this.chainId = config.chainId;
775
+ this.nativeTokenSymbol = config.nativeTokenSymbol;
776
+ this.nativeTokenName = config.nativeTokenName;
777
+ this.nativeTokenDecimals = config.nativeTokenDecimals ?? 18;
778
+ this.cmc_id = config.cmc_id;
779
+ this.baseUrl = config.baseUrl ?? "https://api.etherscan.io/v2/api";
780
+ }
781
+ /**
782
+ * Get native token balance for an address
783
+ * @param address - Wallet address
784
+ * @returns TokenBalance object with native token information
785
+ */
786
+ async getNativeBalance(address) {
787
+ const params = new URLSearchParams({
788
+ chainid: this.chainId.toString(),
789
+ module: "account",
790
+ action: "balance",
791
+ address,
792
+ tag: "latest",
793
+ apikey: this.apiKey
794
+ });
795
+ const url = `${this.baseUrl}?${params.toString()}`;
796
+ const response = await this.fetchWithRetry(url);
797
+ const data = await response.json();
798
+ if (data.status !== "1") {
799
+ throw new Error(
800
+ `Etherscan API error: ${data.message} (result: ${data.result})`
801
+ );
802
+ }
803
+ return {
804
+ contractAddress: "native",
805
+ // Native token doesn't have a contract address
806
+ balance: data.result,
807
+ balanceFormatted: formatBalance(data.result, this.nativeTokenDecimals),
808
+ symbol: this.nativeTokenSymbol,
809
+ name: this.nativeTokenName,
810
+ decimals: this.nativeTokenDecimals,
811
+ logo: `https://s2.coinmarketcap.com/static/img/coins/128x128/${this.cmc_id}.png`,
812
+ isNFT: false
813
+ };
814
+ }
815
+ /**
816
+ * Get all ERC-20 token balances for an address
817
+ * @param address - Wallet address
818
+ * @returns Array of token balances with metadata
819
+ *
820
+ * Note: This endpoint prefers the Etherscan API Pro endpoint for better performance,
821
+ * but automatically falls back to a free-tier compatible approach if needed.
822
+ */
823
+ async getTokenBalances(address) {
824
+ const params = new URLSearchParams({
825
+ chainid: this.chainId.toString(),
826
+ module: "account",
827
+ action: "addresstokenbalance",
828
+ address,
829
+ page: "1",
830
+ offset: "100",
831
+ apikey: this.apiKey
832
+ });
833
+ const url = `${this.baseUrl}?${params.toString()}`;
834
+ const response = await this.fetchWithRetry(url);
835
+ const data = await response.json();
836
+ if (data.status !== "1") {
837
+ if (data.result && typeof data.result === "string" && data.result.includes("API Pro")) {
838
+ return this.getTokenBalancesFallback(address);
839
+ }
840
+ if (data.result && typeof data.result === "string" && data.result.includes("rate limit")) {
841
+ console.warn("Rate limit hit, using fallback method with delays...");
842
+ await this.delay(1e3);
843
+ return this.getTokenBalancesFallback(address);
844
+ }
845
+ throw new Error(
846
+ `Etherscan API error: ${data.message} (result: ${data.result})`
847
+ );
848
+ }
849
+ if (!Array.isArray(data.result)) {
850
+ throw new Error("Unexpected API response format");
851
+ }
852
+ return data.result.map((token) => {
853
+ const decimals = parseInt(token.TokenDivisor, 10);
854
+ return {
855
+ contractAddress: token.TokenAddress,
856
+ balance: token.TokenQuantity,
857
+ balanceFormatted: formatBalance(token.TokenQuantity, decimals),
858
+ symbol: token.TokenSymbol,
859
+ name: token.TokenName,
860
+ decimals,
861
+ // This endpoint returns ERC-20 tokens only, not NFTs
862
+ isNFT: false
863
+ };
864
+ });
865
+ }
866
+ /**
867
+ * Free-tier fallback for getTokenBalances
868
+ * Discovers tokens via transfer history and fetches individual balances
869
+ * Rate-limited to respect Etherscan free tier limits (3 calls/sec)
870
+ * @param address - Wallet address
871
+ * @returns Array of token balances with metadata
872
+ */
873
+ async getTokenBalancesFallback(address) {
874
+ const transfers = await this.getTokenTransfers(address, {
875
+ page: 1,
876
+ offset: 1e4
877
+ // Get more transfers to discover more tokens
878
+ });
879
+ if (transfers.length === 0) {
880
+ return [];
881
+ }
882
+ const uniqueTokens = /* @__PURE__ */ new Map();
883
+ for (const transfer of transfers) {
884
+ if (!uniqueTokens.has(transfer.tokenAddress)) {
885
+ uniqueTokens.set(transfer.tokenAddress, {
886
+ symbol: transfer.tokenSymbol,
887
+ name: transfer.tokenName,
888
+ decimals: parseInt(transfer.tokenDecimal, 10)
889
+ });
890
+ }
891
+ }
892
+ await this.delay(1e3);
893
+ const balances = [];
894
+ const batchSize = 2;
895
+ const delayMs = 1e3;
896
+ const tokenEntries = Array.from(uniqueTokens.entries());
897
+ for (let i = 0; i < tokenEntries.length; i++) {
898
+ const [contractAddress, metadata] = tokenEntries[i];
899
+ try {
900
+ const balance = await this.getTokenBalance(address, contractAddress);
901
+ if (balance !== "0") {
902
+ balances.push({
903
+ contractAddress,
904
+ balance,
905
+ balanceFormatted: formatBalance(balance, metadata.decimals),
906
+ symbol: metadata.symbol,
907
+ name: metadata.name,
908
+ decimals: metadata.decimals,
909
+ // This endpoint returns ERC-20 tokens only, not NFTs
910
+ isNFT: false
911
+ });
912
+ }
913
+ } catch (error) {
914
+ console.warn(
915
+ `Failed to fetch balance for token ${contractAddress}:`,
916
+ error
917
+ );
918
+ }
919
+ if ((i + 1) % batchSize === 0 && i + 1 < tokenEntries.length) {
920
+ await this.delay(delayMs);
921
+ }
922
+ }
923
+ return balances;
924
+ }
925
+ /**
926
+ * Helper method to add delay (for rate limiting)
927
+ * @param ms - Milliseconds to delay
928
+ */
929
+ delay(ms) {
930
+ return new Promise((resolve) => setTimeout(resolve, ms));
931
+ }
932
+ /**
933
+ * Wrapper for fetch with automatic retry on rate limit
934
+ * @param url - URL to fetch
935
+ * @param maxRetries - Maximum number of retries (default: 2)
936
+ * @returns Response
937
+ */
938
+ async fetchWithRetry(url, maxRetries = 2) {
939
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
940
+ const response = await fetch(url);
941
+ if (!response.ok) {
942
+ if (response.status === 429) {
943
+ if (attempt < maxRetries) {
944
+ const delayMs = 1e3 * (attempt + 1);
945
+ console.warn(
946
+ `Rate limit hit (429), retrying in ${delayMs}ms... (attempt ${attempt + 1}/${maxRetries})`
947
+ );
948
+ await this.delay(delayMs);
949
+ continue;
950
+ }
951
+ }
952
+ throw new Error(
953
+ `HTTP error! status: ${response.status} ${response.statusText}`
954
+ );
955
+ }
956
+ return response;
957
+ }
958
+ throw new Error("Max retries exceeded");
959
+ }
960
+ /**
961
+ * Get balance for a specific ERC-20 token
962
+ * @param address - Wallet address
963
+ * @param tokenAddress - Token contract address
964
+ * @returns Balance in smallest unit as string
965
+ */
966
+ async getTokenBalance(address, tokenAddress) {
967
+ const params = new URLSearchParams({
968
+ chainid: this.chainId.toString(),
969
+ module: "account",
970
+ action: "tokenbalance",
971
+ contractaddress: tokenAddress,
972
+ address,
973
+ tag: "latest",
974
+ apikey: this.apiKey
975
+ });
976
+ const url = `${this.baseUrl}?${params.toString()}`;
977
+ const response = await this.fetchWithRetry(url);
978
+ const data = await response.json();
979
+ if (data.status !== "1") {
980
+ throw new Error(
981
+ `Etherscan API error: ${data.message} (result: ${data.result})`
982
+ );
983
+ }
984
+ return data.result;
985
+ }
986
+ /**
987
+ * Get all transactions for an address
988
+ * @param address - Wallet address
989
+ * @param options - Optional filtering and pagination options
990
+ * @returns Array of transactions
991
+ */
992
+ async getTransactions(address, options) {
993
+ const params = new URLSearchParams({
994
+ chainid: this.chainId.toString(),
995
+ module: "account",
996
+ action: "txlist",
997
+ address,
998
+ startblock: (options?.startBlock ?? 0).toString(),
999
+ endblock: (options?.endBlock ?? 99999999).toString(),
1000
+ page: (options?.page ?? 1).toString(),
1001
+ offset: (options?.offset ?? 100).toString(),
1002
+ sort: options?.sort ?? "desc",
1003
+ apikey: this.apiKey
1004
+ });
1005
+ const url = `${this.baseUrl}?${params.toString()}`;
1006
+ const response = await this.fetchWithRetry(url);
1007
+ const data = await response.json();
1008
+ if (data.status !== "1") {
1009
+ if (data.message === "No transactions found") {
1010
+ return [];
1011
+ }
1012
+ throw new Error(
1013
+ `Etherscan API error: ${data.message} (result: ${data.result})`
1014
+ );
1015
+ }
1016
+ return data.result.map((tx) => ({
1017
+ hash: tx.hash,
1018
+ from: tx.from,
1019
+ to: tx.to,
1020
+ value: tx.value,
1021
+ timestamp: parseInt(tx.timeStamp, 10),
1022
+ blockNumber: parseInt(tx.blockNumber, 10),
1023
+ gasUsed: tx.gasUsed,
1024
+ gasPrice: tx.gasPrice,
1025
+ status: tx.txreceipt_status === "1" ? "success" : tx.txreceipt_status === "0" ? "failed" : "pending",
1026
+ isError: tx.isError === "1"
1027
+ }));
1028
+ }
1029
+ /**
1030
+ * Get all ERC-20 token transfers for an address
1031
+ * @param address - Wallet address
1032
+ * @param options - Optional filtering and pagination options
1033
+ * @returns Array of token transfers
1034
+ */
1035
+ async getTokenTransfers(address, options) {
1036
+ const params = new URLSearchParams({
1037
+ chainid: this.chainId.toString(),
1038
+ module: "account",
1039
+ action: "tokentx",
1040
+ address,
1041
+ startblock: (options?.startBlock ?? 0).toString(),
1042
+ endblock: (options?.endBlock ?? 99999999).toString(),
1043
+ page: (options?.page ?? 1).toString(),
1044
+ offset: (options?.offset ?? 100).toString(),
1045
+ sort: options?.sort ?? "desc",
1046
+ apikey: this.apiKey
1047
+ });
1048
+ const url = `${this.baseUrl}?${params.toString()}`;
1049
+ const response = await this.fetchWithRetry(url);
1050
+ const data = await response.json();
1051
+ if (data.status !== "1") {
1052
+ if (data.message === "No transactions found") {
1053
+ return [];
1054
+ }
1055
+ throw new Error(
1056
+ `Etherscan API error: ${data.message} (result: ${data.result})`
1057
+ );
1058
+ }
1059
+ return data.result.map((tx) => ({
1060
+ hash: tx.hash,
1061
+ from: tx.from,
1062
+ to: tx.to,
1063
+ value: tx.value,
1064
+ timestamp: parseInt(tx.timeStamp, 10),
1065
+ blockNumber: parseInt(tx.blockNumber, 10),
1066
+ tokenAddress: tx.contractAddress,
1067
+ tokenSymbol: tx.tokenSymbol,
1068
+ tokenName: tx.tokenName,
1069
+ tokenDecimal: tx.tokenDecimal
1070
+ }));
1071
+ }
1072
+ };
1073
+
1074
+ // src/chains/ethereum.ts
1075
+ var EthereumAdapter = new EVMAdapter({
1076
+ chainData: {
1077
+ name: "Ethereum",
1078
+ symbol: "ETH",
1079
+ tokenStandard: "ERC20",
1080
+ chainId: 60,
1081
+ cmc_id: 1027,
1082
+ // CoinMarketCap ID for Ethereum
1083
+ decimals: 18
1084
+ },
1085
+ coinType: 60,
1086
+ // Standard Ethereum BIP44 coin type
1087
+ standardDecimals: 18,
1088
+ networks: {
1089
+ mainnet: {
1090
+ name: "Ethereum Mainnet",
1091
+ rpcUrl: "https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY",
1092
+ // Replace with your API key
1093
+ explorerUrl: "https://etherscan.io",
1094
+ chainId: 1
1095
+ },
1096
+ testnet: {
1097
+ name: "Ethereum Testnet (Sepolia)",
1098
+ rpcUrl: "https://sepolia.infura.io/v3/YOUR_INFURA_API_KEY",
1099
+ // Replace with your API key
1100
+ explorerUrl: "https://sepolia.etherscan.io",
1101
+ chainId: 11155111
1102
+ }
1103
+ },
1104
+ defaultNetwork: "mainnet"
1105
+ });
1106
+
1107
+ // src/chains/sonic.ts
1108
+ var SonicAdapter = new EVMAdapter({
1109
+ chainData: {
1110
+ name: "Sonic",
1111
+ symbol: "S",
1112
+ tokenStandard: "ERC20",
1113
+ chainId: 60,
1114
+ cmc_id: 32684,
1115
+ // CoinMarketCap ID for Sonic
1116
+ decimals: 18
1117
+ },
1118
+ coinType: 60,
1119
+ // Uses standard Ethereum coin type (EVM-compatible)
1120
+ standardDecimals: 18,
1121
+ networks: {
1122
+ mainnet: {
1123
+ name: "Sonic Mainnet",
1124
+ rpcUrl: "https://rpc.sonic.soniclabs.com",
1125
+ explorerUrl: "https://sonicscan.org",
1126
+ chainId: 146
1127
+ },
1128
+ testnet: {
1129
+ name: "Sonic Testnet",
1130
+ rpcUrl: "https://rpc.blaze.soniclabs.com",
1131
+ explorerUrl: "https://testnet.sonicscan.org",
1132
+ chainId: 57054
1133
+ }
1134
+ },
1135
+ defaultNetwork: "mainnet"
1136
+ });
1137
+
1138
+ export { AlchemyProvider, EVMAdapter, EthereumAdapter, EtherscanProvider, SonicAdapter };
1139
+ //# sourceMappingURL=index.mjs.map
1140
+ //# sourceMappingURL=index.mjs.map