multi-chain-balance-diff 0.1.0 → 0.1.2

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 CHANGED
@@ -110,8 +110,13 @@ mcbd --address 0x... --alert-if-diff ">0.01" # CI threshold
110
110
  | `base` | EVM | ETH | USDC, cbETH, DAI |
111
111
  | `arbitrum` | EVM | ETH | USDC, ARB, GMX |
112
112
  | `optimism` | EVM | ETH | USDC, OP, SNX |
113
+ | `bnb` | EVM | BNB | USDT, USDC, BUSD |
114
+ | `avalanche` | EVM | AVAX | USDC, USDT |
115
+ | `fantom` | EVM | FTM | USDC, USDT, DAI |
116
+ | `zksync` | EVM | ETH | USDC, USDT |
113
117
  | `solana` | Solana | SOL | USDC, BONK, JUP |
114
118
  | `helium` | Solana | SOL | HNT, MOBILE, IOT, DC |
119
+ | `ton` | TON | TON | — |
115
120
 
116
121
  ## Options
117
122
 
@@ -130,8 +135,9 @@ mcbd --address 0x... --alert-if-diff ">0.01" # CI threshold
130
135
  | `--no-tokens` | Skip ERC-20/SPL token checks |
131
136
  | `--alert-if-diff` | Exit 1 if diff matches condition (e.g., `">0.01"`, `"<-1"`) |
132
137
  | `--alert-pct` | Exit 1 if diff exceeds % of balance (e.g., `">5"`, `"<-10"`) |
138
+ | `--timeout` | RPC request timeout in seconds (default: `30`) |
133
139
 
134
- **Exit codes:** `0` OK · `1` diff triggered · `2` RPC failure · `130` SIGINT
140
+ **Exit codes:** `0` OK · `1` diff triggered · `2` RPC failure/timeout · `130` SIGINT
135
141
 
136
142
  ---
137
143
 
@@ -148,6 +154,39 @@ mcbd --address 0x... --alert-if-diff ">0.01" # CI threshold
148
154
 
149
155
  ---
150
156
 
157
+ ## Configuration
158
+
159
+ ### Custom RPC Endpoints
160
+
161
+ Override default public RPCs with environment variables:
162
+
163
+ | Variable | Network |
164
+ |----------|---------|
165
+ | `RPC_URL_ETH` | Ethereum Mainnet |
166
+ | `RPC_URL_POLYGON` | Polygon |
167
+ | `RPC_URL_BASE` | Base |
168
+ | `RPC_URL_ARBITRUM` | Arbitrum |
169
+ | `RPC_URL_OPTIMISM` | Optimism |
170
+ | `RPC_URL_SOLANA` | Solana / Helium |
171
+ | `RPC_URL_SEPOLIA` | Sepolia testnet |
172
+
173
+ ```bash
174
+ # Use private RPC for reliability
175
+ export RPC_URL_ETH=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
176
+ mcbd -a 0x... -n mainnet --json
177
+ ```
178
+
179
+ ### Timeout
180
+
181
+ Default timeout is 30 seconds. Adjust for slow or unreliable RPCs:
182
+
183
+ ```bash
184
+ mcbd -a 0x... -n mainnet --timeout 60 # 60 seconds
185
+ mcbd -a 0x... -n solana --timeout 10 # 10 seconds (fast-fail)
186
+ ```
187
+
188
+ ---
189
+
151
190
  ## Common Failure Modes
152
191
 
153
192
  ### RPC Rate Limits
@@ -155,17 +194,24 @@ mcbd --address 0x... --alert-if-diff ">0.01" # CI threshold
155
194
  Public RPCs have strict rate limits. Symptoms: `429 Too Many Requests` or slow responses.
156
195
 
157
196
  ```bash
158
- # Solution: Use a private RPC (Alchemy, Infura, QuickNode)
159
- export ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
197
+ # Solution: Use a private RPC
198
+ export RPC_URL_ETH=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
199
+ ```
200
+
201
+ ### Timeout / Slow RPC
202
+
203
+ ```bash
204
+ # Increase timeout for slow networks
205
+ mcbd -a 0x... -n mainnet --timeout 60
160
206
  ```
161
207
 
162
208
  ### Unavailable Chain / RPC Down
163
209
 
164
210
  ```json
165
- {"schemaVersion":"0.1.0","error":"connect ECONNREFUSED 127.0.0.1:8545","code":"ECONNREFUSED","exitCode":2}
211
+ {"schemaVersion":"0.1.0","error":"connect ECONNREFUSED","code":"ECONNREFUSED","exitCode":2}
166
212
  ```
167
213
 
168
- Exit code `2` indicates RPC failure. Check network connectivity and RPC URL validity.
214
+ Exit code `2` indicates RPC failure. Check network connectivity and RPC URL.
169
215
 
170
216
  ### Invalid Address Format
171
217
 
@@ -176,15 +222,6 @@ $ mcbd --address invalid --network mainnet --json
176
222
 
177
223
  EVM addresses must be `0x` + 40 hex chars. Solana addresses are base58 encoded.
178
224
 
179
- ### Missing Environment Variables
180
-
181
- If using custom RPC endpoints via env vars, ensure they're set before running:
182
-
183
- ```bash
184
- # Check if RPC env is set
185
- [ -z "$ETH_RPC_URL" ] && echo "Warning: Using public RPC (rate limited)"
186
- ```
187
-
188
225
  ---
189
226
 
190
227
  ## Watch Mode for CI/Cron
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-chain-balance-diff",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI tool to fetch and compare wallet balances across EVM and Solana chains",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "start": "node src/index.js",
17
- "test": "node --test 'tests/*.test.js'"
17
+ "test": "node --test tests/*.test.js"
18
18
  },
19
19
  "keywords": [
20
20
  "ethereum",
@@ -44,9 +44,9 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@solana/web3.js": "^1.95.0",
47
+ "@ton/ton": "^15.4.0",
47
48
  "commander": "^12.1.0",
48
49
  "dotenv": "^16.4.5",
49
50
  "ethers": "^6.13.4"
50
51
  }
51
52
  }
52
-
@@ -295,3 +295,4 @@
295
295
  }
296
296
  }
297
297
 
298
+
@@ -119,3 +119,4 @@ class BaseAdapter {
119
119
  }
120
120
 
121
121
  module.exports = BaseAdapter;
122
+
@@ -6,7 +6,7 @@
6
6
  * all compatible networks.
7
7
  */
8
8
 
9
- const { ethers } = require('ethers');
9
+ const { ethers, FetchRequest } = require('ethers');
10
10
  const BaseAdapter = require('./baseAdapter');
11
11
 
12
12
  // Minimal ERC-20 ABI for balance queries
@@ -16,10 +16,14 @@ const ERC20_ABI = [
16
16
  'function symbol() view returns (string)',
17
17
  ];
18
18
 
19
+ // Default timeout: 30 seconds
20
+ const DEFAULT_TIMEOUT_MS = 30000;
21
+
19
22
  class EVMAdapter extends BaseAdapter {
20
- constructor(networkConfig) {
23
+ constructor(networkConfig, options = {}) {
21
24
  super(networkConfig);
22
25
  this.provider = null;
26
+ this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
23
27
  }
24
28
 
25
29
  getChainType() {
@@ -27,7 +31,11 @@ class EVMAdapter extends BaseAdapter {
27
31
  }
28
32
 
29
33
  async connect() {
30
- this.provider = new ethers.JsonRpcProvider(this.networkConfig.rpcUrl);
34
+ // Create FetchRequest with timeout
35
+ const fetchRequest = new FetchRequest(this.networkConfig.rpcUrl);
36
+ fetchRequest.timeout = this.timeoutMs;
37
+
38
+ this.provider = new ethers.JsonRpcProvider(fetchRequest);
31
39
  // Verify connection by fetching network
32
40
  await this.provider.getNetwork();
33
41
  this.connection = this.provider;
@@ -124,3 +132,5 @@ class EVMAdapter extends BaseAdapter {
124
132
  }
125
133
 
126
134
  module.exports = EVMAdapter;
135
+
136
+
@@ -12,6 +12,7 @@
12
12
 
13
13
  const EVMAdapter = require('./evmAdapter');
14
14
  const SolanaAdapter = require('./solanaAdapter');
15
+ const TonAdapter = require('./tonAdapter');
15
16
 
16
17
  /**
17
18
  * Chain type to adapter class mapping.
@@ -19,16 +20,19 @@ const SolanaAdapter = require('./solanaAdapter');
19
20
  const ADAPTER_MAP = {
20
21
  evm: EVMAdapter,
21
22
  solana: SolanaAdapter,
23
+ ton: TonAdapter,
22
24
  };
23
25
 
24
26
  /**
25
27
  * Create an adapter for the given network configuration.
26
28
  *
27
29
  * @param {object} networkConfig - Network configuration from networks.js
30
+ * @param {object} options - Adapter options
31
+ * @param {number} options.timeoutMs - RPC timeout in milliseconds (default: 30000)
28
32
  * @returns {BaseAdapter} Chain-specific adapter instance
29
33
  * @throws {Error} If chain type is not supported
30
34
  */
31
- function createAdapter(networkConfig) {
35
+ function createAdapter(networkConfig, options = {}) {
32
36
  // Determine chain type (default to 'evm' for backwards compatibility)
33
37
  const chainType = networkConfig.chainType || 'evm';
34
38
 
@@ -41,7 +45,7 @@ function createAdapter(networkConfig) {
41
45
  );
42
46
  }
43
47
 
44
- return new AdapterClass(networkConfig);
48
+ return new AdapterClass(networkConfig, options);
45
49
  }
46
50
 
47
51
  /**
@@ -67,4 +71,7 @@ module.exports = {
67
71
  isChainTypeSupported,
68
72
  EVMAdapter,
69
73
  SolanaAdapter,
74
+ TonAdapter,
70
75
  };
76
+
77
+
@@ -18,20 +18,48 @@ const BaseAdapter = require('./baseAdapter');
18
18
  // SPL Token Program ID
19
19
  const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
20
20
 
21
+ // Default timeout: 30 seconds
22
+ const DEFAULT_TIMEOUT_MS = 30000;
23
+
21
24
  class SolanaAdapter extends BaseAdapter {
22
- constructor(networkConfig) {
25
+ constructor(networkConfig, options = {}) {
23
26
  super(networkConfig);
24
27
  this.conn = null;
28
+ this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
25
29
  }
26
30
 
27
31
  getChainType() {
28
32
  return 'solana';
29
33
  }
30
34
 
35
+ /**
36
+ * Create a fetch function with timeout support.
37
+ */
38
+ _createFetchWithTimeout() {
39
+ const timeoutMs = this.timeoutMs;
40
+ return async (url, options) => {
41
+ const controller = new AbortController();
42
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
43
+
44
+ try {
45
+ const response = await fetch(url, {
46
+ ...options,
47
+ signal: controller.signal,
48
+ });
49
+ return response;
50
+ } finally {
51
+ clearTimeout(timeoutId);
52
+ }
53
+ };
54
+ }
55
+
31
56
  async connect() {
32
57
  this.conn = new Connection(
33
58
  this.networkConfig.rpcUrl,
34
- { commitment: 'confirmed' }
59
+ {
60
+ commitment: 'confirmed',
61
+ fetch: this._createFetchWithTimeout(),
62
+ }
35
63
  );
36
64
  // Verify connection
37
65
  await this.conn.getSlot();
@@ -177,3 +205,5 @@ class SolanaAdapter extends BaseAdapter {
177
205
  }
178
206
 
179
207
  module.exports = SolanaAdapter;
208
+
209
+
@@ -0,0 +1,95 @@
1
+ /**
2
+ * TON (The Open Network) Adapter
3
+ *
4
+ * Handles TON blockchain balance queries.
5
+ * Uses @ton/ton SDK for RPC communication.
6
+ */
7
+
8
+ const { TonClient, Address, fromNano } = require('@ton/ton');
9
+ const BaseAdapter = require('./baseAdapter');
10
+
11
+ // Default timeout: 30 seconds
12
+ const DEFAULT_TIMEOUT_MS = 30000;
13
+
14
+ class TonAdapter extends BaseAdapter {
15
+ constructor(networkConfig, options = {}) {
16
+ super(networkConfig);
17
+ this.client = null;
18
+ this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
19
+ }
20
+
21
+ getChainType() {
22
+ return 'ton';
23
+ }
24
+
25
+ async connect() {
26
+ this.client = new TonClient({
27
+ endpoint: this.networkConfig.rpcUrl,
28
+ timeout: this.timeoutMs,
29
+ });
30
+ // Verify connection by getting masterchain info
31
+ await this.client.getMasterchainInfo();
32
+ this.connection = this.client;
33
+ }
34
+
35
+ async getCurrentBlock() {
36
+ const info = await this.client.getMasterchainInfo();
37
+ return info.last.seqno;
38
+ }
39
+
40
+ async getNativeBalance(address, blockTag = 'latest') {
41
+ const addr = Address.parse(address);
42
+
43
+ // TON doesn't easily support historical balance queries via standard API
44
+ // We fetch current balance
45
+ const balance = await this.client.getBalance(addr);
46
+
47
+ return {
48
+ raw: balance,
49
+ formatted: this.formatBalance(balance, 9), // TON has 9 decimals
50
+ decimals: 9,
51
+ };
52
+ }
53
+
54
+ async getTokenBalances(address, tokens) {
55
+ // TON Jettons (tokens) require parsing wallet contract state
56
+ // This is more complex than EVM - simplified implementation
57
+ // Returns empty for now, can be extended later
58
+ return [];
59
+ }
60
+
61
+ isValidAddress(address) {
62
+ try {
63
+ Address.parse(address);
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ formatBalance(raw, decimals = 9) {
71
+ // fromNano converts from nanoTON to TON
72
+ return fromNano(raw);
73
+ }
74
+
75
+ formatBalanceWithSymbol(raw, symbol, decimals = 9) {
76
+ return `${this.formatBalance(raw, decimals)} ${symbol}`;
77
+ }
78
+
79
+ formatDiff(diff, symbol, decimals = 9) {
80
+ const formatted = this.formatBalance(diff < 0n ? -diff : diff, decimals);
81
+ const prefix = diff >= 0n ? '+' : '-';
82
+ return `${prefix}${formatted} ${symbol}`;
83
+ }
84
+
85
+ getExplorerUrl(address) {
86
+ const isTestnet = this.networkConfig.rpcUrl.includes('testnet');
87
+ const baseUrl = isTestnet
88
+ ? 'https://testnet.tonscan.org'
89
+ : 'https://tonscan.org';
90
+ return `${baseUrl}/address/${address}`;
91
+ }
92
+ }
93
+
94
+ module.exports = TonAdapter;
95
+
@@ -118,6 +118,66 @@ const networks = {
118
118
  ],
119
119
  },
120
120
 
121
+ bnb: {
122
+ name: 'BNB Chain',
123
+ chainType: 'evm',
124
+ chainId: 56,
125
+ rpcUrl: process.env.RPC_URL_BNB || 'https://bsc-dataseed.binance.org',
126
+ nativeSymbol: 'BNB',
127
+ nativeDecimals: 18,
128
+ blockExplorer: 'https://bscscan.com',
129
+ tokens: [
130
+ { symbol: 'USDT', address: '0x55d398326f99059fF775485246999027B3197955', decimals: 18 },
131
+ { symbol: 'USDC', address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', decimals: 18 },
132
+ { symbol: 'BUSD', address: '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56', decimals: 18 },
133
+ { symbol: 'WBNB', address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', decimals: 18 },
134
+ ],
135
+ },
136
+
137
+ avalanche: {
138
+ name: 'Avalanche C-Chain',
139
+ chainType: 'evm',
140
+ chainId: 43114,
141
+ rpcUrl: process.env.RPC_URL_AVAX || 'https://api.avax.network/ext/bc/C/rpc',
142
+ nativeSymbol: 'AVAX',
143
+ nativeDecimals: 18,
144
+ blockExplorer: 'https://snowtrace.io',
145
+ tokens: [
146
+ { symbol: 'USDC', address: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', decimals: 6 },
147
+ { symbol: 'USDT', address: '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', decimals: 6 },
148
+ { symbol: 'WAVAX', address: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', decimals: 18 },
149
+ ],
150
+ },
151
+
152
+ fantom: {
153
+ name: 'Fantom Opera',
154
+ chainType: 'evm',
155
+ chainId: 250,
156
+ rpcUrl: process.env.RPC_URL_FTM || 'https://rpc.ftm.tools',
157
+ nativeSymbol: 'FTM',
158
+ nativeDecimals: 18,
159
+ blockExplorer: 'https://ftmscan.com',
160
+ tokens: [
161
+ { symbol: 'USDC', address: '0x04068DA6C83AFCFA0e13ba15A6696662335D5B75', decimals: 6 },
162
+ { symbol: 'USDT', address: '0x049d68029688eAbF473097a2fC38ef61633A3C7A', decimals: 6 },
163
+ { symbol: 'DAI', address: '0x8D11eC38a3EB5E956B052f67Da8Bdc9bef8Abf3E', decimals: 18 },
164
+ ],
165
+ },
166
+
167
+ zksync: {
168
+ name: 'zkSync Era',
169
+ chainType: 'evm',
170
+ chainId: 324,
171
+ rpcUrl: process.env.RPC_URL_ZKSYNC || 'https://mainnet.era.zksync.io',
172
+ nativeSymbol: 'ETH',
173
+ nativeDecimals: 18,
174
+ blockExplorer: 'https://explorer.zksync.io',
175
+ tokens: [
176
+ { symbol: 'USDC', address: '0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4', decimals: 6 },
177
+ { symbol: 'USDT', address: '0x493257fD37EDB34451f62EDf8D2a0C418852bA4C', decimals: 6 },
178
+ ],
179
+ },
180
+
121
181
  // ==========================================================================
122
182
  // Solana Networks
123
183
  // ==========================================================================
@@ -179,6 +239,34 @@ const networks = {
179
239
  blockExplorer: 'https://explorer.solana.com',
180
240
  tokens: [],
181
241
  },
242
+
243
+ // ==========================================================================
244
+ // TON Networks
245
+ // ==========================================================================
246
+
247
+ ton: {
248
+ name: 'TON Mainnet',
249
+ chainType: 'ton',
250
+ chainId: null,
251
+ rpcUrl: process.env.RPC_URL_TON || 'https://toncenter.com/api/v2/jsonRPC',
252
+ nativeSymbol: 'TON',
253
+ nativeDecimals: 9,
254
+ blockExplorer: 'https://tonscan.org',
255
+ tokens: [
256
+ // Jettons can be added here later
257
+ ],
258
+ },
259
+
260
+ 'ton-testnet': {
261
+ name: 'TON Testnet',
262
+ chainType: 'ton',
263
+ chainId: null,
264
+ rpcUrl: process.env.RPC_URL_TON_TESTNET || 'https://testnet.toncenter.com/api/v2/jsonRPC',
265
+ nativeSymbol: 'TON',
266
+ nativeDecimals: 9,
267
+ blockExplorer: 'https://testnet.tonscan.org',
268
+ tokens: [],
269
+ },
182
270
  };
183
271
 
184
272
  // ==========================================================================
@@ -232,3 +320,4 @@ module.exports = {
232
320
  getChainType,
233
321
  };
234
322
 
323
+
package/src/index.js CHANGED
@@ -58,6 +58,7 @@ program
58
58
  .option('--config <path>', 'Path to config file')
59
59
  .option('--alert-if-diff <threshold>', 'Exit 1 if diff exceeds threshold (e.g., ">0.01", ">=1", "<-0.5")')
60
60
  .option('--alert-pct <threshold>', 'Exit 1 if diff exceeds % of balance (e.g., ">5", "<-10")')
61
+ .option('--timeout <seconds>', 'RPC request timeout in seconds', '30')
61
62
  .parse(process.argv);
62
63
 
63
64
  const options = program.opts();
@@ -325,6 +326,10 @@ function listNetworks() {
325
326
  const net = getNetwork(key);
326
327
  return { key, name: net.name, symbol: net.nativeSymbol };
327
328
  }),
329
+ ton: getNetworksByType('ton').map(key => {
330
+ const net = getNetwork(key);
331
+ return { key, name: net.name, symbol: net.nativeSymbol };
332
+ }),
328
333
  };
329
334
  console.log(JSON.stringify(networks, null, 2));
330
335
  return;
@@ -343,12 +348,18 @@ function listNetworks() {
343
348
  const net = getNetwork(key);
344
349
  console.log(` ${c('cyan')}${key.padEnd(12)}${c('reset')} ${net.name} (${net.nativeSymbol})`);
345
350
  }
351
+
352
+ console.log('\n TON Chains:');
353
+ for (const key of getNetworksByType('ton')) {
354
+ const net = getNetwork(key);
355
+ console.log(` ${c('cyan')}${key.padEnd(12)}${c('reset')} ${net.name} (${net.nativeSymbol})`);
356
+ }
346
357
 
347
358
  console.log('\nUsage:');
348
359
  console.log(' mcbd --address <ADDR> --network mainnet');
349
360
  console.log(' mcbd --address <ADDR> --network base');
350
361
  console.log(' mcbd --address <ADDR> --network solana');
351
- console.log(' mcbd --address <ADDR> --network helium --json\n');
362
+ console.log(' mcbd --address <ADDR> --network ton --json\n');
352
363
  }
353
364
 
354
365
  // ==========================================================================
@@ -803,8 +814,11 @@ async function main() {
803
814
  process.exit(1);
804
815
  }
805
816
 
817
+ // Parse timeout (convert seconds to milliseconds)
818
+ const timeoutMs = parseInt(options.timeout, 10) * 1000;
819
+
806
820
  // Create the appropriate adapter for this chain
807
- const adapter = createAdapter(networkConfig);
821
+ const adapter = createAdapter(networkConfig, { timeoutMs });
808
822
 
809
823
  // Validate all addresses before connecting (fail fast)
810
824
  for (const addr of addresses) {
@@ -154,3 +154,4 @@ module.exports = {
154
154
  formatBalance,
155
155
  formatBalanceDiff,
156
156
  };
157
+