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