multi-chain-balance-diff 0.1.3 โ 0.1.6
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 +44 -1
- package/package.json +6 -2
- package/schema/mcbd-output.schema.json +3 -0
- package/src/adapters/baseAdapter.js +3 -0
- package/src/adapters/evmAdapter.js +3 -0
- package/src/adapters/solanaAdapter.js +3 -0
- package/src/adapters/tonAdapter.js +3 -0
- package/src/config/networks.js +15 -0
- package/src/index.js +95 -0
- package/src/services/balanceService.js +3 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ mcbd --address 0x... --network mainnet --json | jq '.native.diff'
|
|
|
11
11
|
|
|
12
12
|
## What
|
|
13
13
|
|
|
14
|
-
CLI that fetches balance at block N and block N-50, computes the difference, outputs JSON or pretty-prints. Supports Ethereum, Polygon, Base, Arbitrum, Optimism, BNB Chain, Avalanche, Fantom, zkSync Era, Solana, Helium, and TON.
|
|
14
|
+
CLI that fetches balance at block N and block N-50, computes the difference, outputs JSON or pretty-prints. Supports Ethereum, Polygon, Base, Arbitrum, Optimism, BNB Chain, Avalanche, Fantom, zkSync Era, Scroll, Solana, Helium, and TON.
|
|
15
15
|
|
|
16
16
|
## Who
|
|
17
17
|
|
|
@@ -42,6 +42,8 @@ mcbd --address $FEE_WALLET --network base --alert-if-diff "<-0.1"
|
|
|
42
42
|
# exit 0 = OK, exit 1 = threshold breached, exit 2 = RPC error
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
> ๐ See [`.github/examples/`](./.github/examples/) for ready-to-use GitHub Actions workflows.
|
|
46
|
+
|
|
45
47
|
### 3. DeFi/Rewards: Staking reward accrual verification
|
|
46
48
|
|
|
47
49
|
```bash
|
|
@@ -114,6 +116,7 @@ mcbd --address 0x... --alert-if-diff ">0.01" # CI threshold
|
|
|
114
116
|
| `avalanche` | EVM | AVAX | USDC, USDT |
|
|
115
117
|
| `fantom` | EVM | FTM | USDC, USDT, DAI |
|
|
116
118
|
| `zksync` | EVM | ETH | USDC, USDT |
|
|
119
|
+
| `scroll` | EVM | ETH | USDC, USDT, WETH |
|
|
117
120
|
| `solana` | Solana | SOL | USDC, BONK, JUP |
|
|
118
121
|
| `helium` | Solana | SOL | HNT, MOBILE, IOT, DC |
|
|
119
122
|
| `ton` | TON | TON | โ |
|
|
@@ -136,6 +139,7 @@ mcbd --address 0x... --alert-if-diff ">0.01" # CI threshold
|
|
|
136
139
|
| `--alert-if-diff` | Exit 1 if diff matches condition (e.g., `">0.01"`, `"<-1"`) |
|
|
137
140
|
| `--alert-pct` | Exit 1 if diff exceeds % of balance (e.g., `">5"`, `"<-10"`) |
|
|
138
141
|
| `--timeout` | RPC request timeout in seconds (default: `30`) |
|
|
142
|
+
| `--webhook` | POST JSON payload to URL when alert triggers |
|
|
139
143
|
|
|
140
144
|
**Exit codes:** `0` OK ยท `1` diff triggered ยท `2` RPC failure/timeout ยท `130` SIGINT
|
|
141
145
|
|
|
@@ -185,6 +189,26 @@ mcbd -a 0x... -n mainnet --timeout 60 # 60 seconds
|
|
|
185
189
|
mcbd -a 0x... -n solana --timeout 10 # 10 seconds (fast-fail)
|
|
186
190
|
```
|
|
187
191
|
|
|
192
|
+
### Webhooks
|
|
193
|
+
|
|
194
|
+
POST JSON payload to a URL when an alert triggers:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Slack webhook
|
|
198
|
+
mcbd -a 0xTreasury -n mainnet --alert-if-diff "<-1" \
|
|
199
|
+
--webhook https://hooks.slack.com/services/XXX/YYY/ZZZ --json
|
|
200
|
+
|
|
201
|
+
# Discord webhook
|
|
202
|
+
mcbd -a 0xTreasury -n base --alert-pct "<-5" \
|
|
203
|
+
--webhook https://discord.com/api/webhooks/XXX/YYY --json
|
|
204
|
+
|
|
205
|
+
# Custom endpoint
|
|
206
|
+
mcbd -a 0xTreasury -n polygon --alert-if-diff ">0.1" \
|
|
207
|
+
--webhook https://your-server.com/balance-alert --json
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The webhook payload includes the full JSON output plus a `webhook.sentAt` timestamp.
|
|
211
|
+
|
|
188
212
|
---
|
|
189
213
|
|
|
190
214
|
## Common Failure Modes
|
|
@@ -360,6 +384,25 @@ MIT
|
|
|
360
384
|
|
|
361
385
|
---
|
|
362
386
|
|
|
387
|
+
## Author
|
|
388
|
+
|
|
389
|
+
Built by [Alex Alexapolsky](https://www.linkedin.com/in/alexey-a-181a614/) ([@metawake](https://github.com/metawake)). Available for Web3 infra consulting and contract work.
|
|
390
|
+
|
|
391
|
+
โญ **If you find this useful, [star the repo](https://github.com/metawake/multi-chain-balance-diff)** โ it helps others discover it.
|
|
392
|
+
|
|
393
|
+
## Support Development
|
|
394
|
+
|
|
395
|
+
If this tool saves you time or money, consider supporting its development:
|
|
396
|
+
|
|
397
|
+
**ETH donations (any EVM chain):**
|
|
398
|
+
```
|
|
399
|
+
0x0a542565b3615e8fc934cc3cc4921a0c22e5dc5e
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
๐ก *Hint: This tool monitors balance changes โ you can use it to verify your own donation went through!*
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
363
406
|
<!--
|
|
364
407
|
GitHub repo description (1 sentence):
|
|
365
408
|
CLI tool to fetch and diff wallet balances across EVM and Solana chains. JSON output, CI-friendly exit codes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multi-chain-balance-diff",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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": {
|
|
@@ -29,8 +29,12 @@
|
|
|
29
29
|
"crypto",
|
|
30
30
|
"wallet"
|
|
31
31
|
],
|
|
32
|
-
"author": "",
|
|
32
|
+
"author": "Alex Alexapolsky (https://github.com/metawake)",
|
|
33
33
|
"license": "MIT",
|
|
34
|
+
"funding": {
|
|
35
|
+
"type": "github",
|
|
36
|
+
"url": "https://github.com/sponsors/metawake"
|
|
37
|
+
},
|
|
34
38
|
"repository": {
|
|
35
39
|
"type": "git",
|
|
36
40
|
"url": "git+https://github.com/metawake/multi-chain-balance-diff.git"
|
package/src/config/networks.js
CHANGED
|
@@ -178,6 +178,21 @@ const networks = {
|
|
|
178
178
|
],
|
|
179
179
|
},
|
|
180
180
|
|
|
181
|
+
scroll: {
|
|
182
|
+
name: 'Scroll',
|
|
183
|
+
chainType: 'evm',
|
|
184
|
+
chainId: 534352,
|
|
185
|
+
rpcUrl: process.env.RPC_URL_SCROLL || 'https://rpc.scroll.io',
|
|
186
|
+
nativeSymbol: 'ETH',
|
|
187
|
+
nativeDecimals: 18,
|
|
188
|
+
blockExplorer: 'https://scrollscan.com',
|
|
189
|
+
tokens: [
|
|
190
|
+
{ symbol: 'USDC', address: '0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4', decimals: 6 },
|
|
191
|
+
{ symbol: 'USDT', address: '0xf55BEC9cafDbE8730f096Aa55dad6D22d44099Df', decimals: 6 },
|
|
192
|
+
{ symbol: 'WETH', address: '0x5300000000000000000000000000000000000004', decimals: 18 },
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
|
|
181
196
|
// ==========================================================================
|
|
182
197
|
// Solana Networks
|
|
183
198
|
// ==========================================================================
|
package/src/index.js
CHANGED
|
@@ -59,6 +59,7 @@ program
|
|
|
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
61
|
.option('--timeout <seconds>', 'RPC request timeout in seconds', '30')
|
|
62
|
+
.option('--webhook <url>', 'POST JSON payload to URL when alert triggers')
|
|
62
63
|
.parse(process.argv);
|
|
63
64
|
|
|
64
65
|
const options = program.opts();
|
|
@@ -310,6 +311,88 @@ function checkPercentageThreshold(diffRaw, previousRaw, decimals, threshold) {
|
|
|
310
311
|
}
|
|
311
312
|
}
|
|
312
313
|
|
|
314
|
+
// ==========================================================================
|
|
315
|
+
// Webhook Support
|
|
316
|
+
// ==========================================================================
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* POST JSON payload to webhook URL
|
|
320
|
+
* @param {string} url - Webhook URL
|
|
321
|
+
* @param {object} payload - JSON payload to send
|
|
322
|
+
* @returns {Promise<{success: boolean, statusCode?: number, error?: string}>}
|
|
323
|
+
*/
|
|
324
|
+
async function sendWebhook(url, payload) {
|
|
325
|
+
try {
|
|
326
|
+
const urlObj = new URL(url);
|
|
327
|
+
const isHttps = urlObj.protocol === 'https:';
|
|
328
|
+
const http = isHttps ? require('https') : require('http');
|
|
329
|
+
|
|
330
|
+
const postData = JSON.stringify(payload);
|
|
331
|
+
|
|
332
|
+
const requestOptions = {
|
|
333
|
+
hostname: urlObj.hostname,
|
|
334
|
+
port: urlObj.port || (isHttps ? 443 : 80),
|
|
335
|
+
path: urlObj.pathname + urlObj.search,
|
|
336
|
+
method: 'POST',
|
|
337
|
+
headers: {
|
|
338
|
+
'Content-Type': 'application/json',
|
|
339
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
340
|
+
'User-Agent': `multi-chain-balance-diff/${VERSION}`,
|
|
341
|
+
},
|
|
342
|
+
timeout: 10000, // 10 second timeout for webhook
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return new Promise((resolve) => {
|
|
346
|
+
const req = http.request(requestOptions, (res) => {
|
|
347
|
+
resolve({ success: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode });
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
req.on('error', (error) => {
|
|
351
|
+
resolve({ success: false, error: error.message });
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
req.on('timeout', () => {
|
|
355
|
+
req.destroy();
|
|
356
|
+
resolve({ success: false, error: 'Request timeout' });
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
req.write(postData);
|
|
360
|
+
req.end();
|
|
361
|
+
});
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return { success: false, error: error.message };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Build and send webhook if configured and alert triggered
|
|
369
|
+
*/
|
|
370
|
+
async function maybeNotifyWebhook(payload, alertTriggered) {
|
|
371
|
+
if (!options.webhook || !alertTriggered) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const webhookPayload = {
|
|
376
|
+
...payload,
|
|
377
|
+
webhook: {
|
|
378
|
+
sentAt: new Date().toISOString(),
|
|
379
|
+
trigger: 'alert',
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const result = await sendWebhook(options.webhook, webhookPayload);
|
|
384
|
+
|
|
385
|
+
if (!options.json) {
|
|
386
|
+
if (result.success) {
|
|
387
|
+
console.log(`${c('green')}โ Webhook sent${c('reset')}`);
|
|
388
|
+
} else {
|
|
389
|
+
console.log(`${c('red')}โ Webhook failed: ${result.error || `HTTP ${result.statusCode}`}${c('reset')}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
|
|
313
396
|
// ==========================================================================
|
|
314
397
|
// Network List Command
|
|
315
398
|
// ==========================================================================
|
|
@@ -479,10 +562,15 @@ function printPrettyOutput(networkConfig, address, balanceDiff, tokenBalances, a
|
|
|
479
562
|
|
|
480
563
|
if (!isMulti) {
|
|
481
564
|
printSeparator('โ');
|
|
565
|
+
printSponsorHint();
|
|
482
566
|
console.log();
|
|
483
567
|
}
|
|
484
568
|
}
|
|
485
569
|
|
|
570
|
+
function printSponsorHint() {
|
|
571
|
+
console.log(` ${c('dim')}โ
Tips: 0x0a54...dc5e ยท github.com/metawake/multi-chain-balance-diff${c('reset')}`);
|
|
572
|
+
}
|
|
573
|
+
|
|
486
574
|
function printMultiAddressSummary(networkConfig, results, blocksBack) {
|
|
487
575
|
console.log();
|
|
488
576
|
printSeparator('โ');
|
|
@@ -512,6 +600,7 @@ function printMultiAddressSummary(networkConfig, results, blocksBack) {
|
|
|
512
600
|
const totalDiffColored = formatDiffColored(totalDiff, networkConfig.nativeSymbol, networkConfig.nativeDecimals);
|
|
513
601
|
console.log(` ${c('bright')}Total:${c('reset')} ${totalFormatted.padStart(12)} ${networkConfig.nativeSymbol} ${totalDiffColored}`);
|
|
514
602
|
printSeparator('โ');
|
|
603
|
+
printSponsorHint();
|
|
515
604
|
console.log();
|
|
516
605
|
}
|
|
517
606
|
|
|
@@ -938,6 +1027,9 @@ async function main() {
|
|
|
938
1027
|
console.log(`${c('yellow')}โ ๏ธ Alert: threshold ${which} triggered${c('reset')}\n`);
|
|
939
1028
|
}
|
|
940
1029
|
}
|
|
1030
|
+
|
|
1031
|
+
// Send webhook if configured and alert triggered
|
|
1032
|
+
await maybeNotifyWebhook(output, anyAlertTriggered);
|
|
941
1033
|
|
|
942
1034
|
process.exit(anyAlertTriggered ? EXIT_DIFF : EXIT_OK);
|
|
943
1035
|
}
|
|
@@ -999,6 +1091,9 @@ async function main() {
|
|
|
999
1091
|
}
|
|
1000
1092
|
}
|
|
1001
1093
|
|
|
1094
|
+
// Send webhook if configured and alert triggered
|
|
1095
|
+
await maybeNotifyWebhook(output, alertTriggered);
|
|
1096
|
+
|
|
1002
1097
|
// Exit with appropriate code
|
|
1003
1098
|
process.exit(alertTriggered ? EXIT_DIFF : EXIT_OK);
|
|
1004
1099
|
|