vacuum-sol 1.0.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.
Files changed (62) hide show
  1. package/README.md +362 -0
  2. package/dist/config.d.ts +15 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +58 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/core/detector.d.ts +36 -0
  7. package/dist/core/detector.d.ts.map +1 -0
  8. package/dist/core/detector.js +142 -0
  9. package/dist/core/detector.js.map +1 -0
  10. package/dist/core/monitor.d.ts +31 -0
  11. package/dist/core/monitor.d.ts.map +1 -0
  12. package/dist/core/monitor.js +172 -0
  13. package/dist/core/monitor.js.map +1 -0
  14. package/dist/core/reclaimer.d.ts +30 -0
  15. package/dist/core/reclaimer.d.ts.map +1 -0
  16. package/dist/core/reclaimer.js +182 -0
  17. package/dist/core/reclaimer.js.map +1 -0
  18. package/dist/core/types.d.ts +125 -0
  19. package/dist/core/types.d.ts.map +1 -0
  20. package/dist/core/types.js +2 -0
  21. package/dist/core/types.js.map +1 -0
  22. package/dist/db/accounts.d.ts +71 -0
  23. package/dist/db/accounts.d.ts.map +1 -0
  24. package/dist/db/accounts.js +205 -0
  25. package/dist/db/accounts.js.map +1 -0
  26. package/dist/db/index.d.ts +14 -0
  27. package/dist/db/index.d.ts.map +1 -0
  28. package/dist/db/index.js +104 -0
  29. package/dist/db/index.js.map +1 -0
  30. package/dist/db/operators.d.ts +48 -0
  31. package/dist/db/operators.d.ts.map +1 -0
  32. package/dist/db/operators.js +201 -0
  33. package/dist/db/operators.js.map +1 -0
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +473 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/server/index.d.ts +5 -0
  39. package/dist/server/index.d.ts.map +1 -0
  40. package/dist/server/index.js +121 -0
  41. package/dist/server/index.js.map +1 -0
  42. package/dist/services/reporter.d.ts +28 -0
  43. package/dist/services/reporter.d.ts.map +1 -0
  44. package/dist/services/reporter.js +107 -0
  45. package/dist/services/reporter.js.map +1 -0
  46. package/dist/services/solana.d.ts +59 -0
  47. package/dist/services/solana.d.ts.map +1 -0
  48. package/dist/services/solana.js +162 -0
  49. package/dist/services/solana.js.map +1 -0
  50. package/dist/services/telegram.d.ts +20 -0
  51. package/dist/services/telegram.d.ts.map +1 -0
  52. package/dist/services/telegram.js +213 -0
  53. package/dist/services/telegram.js.map +1 -0
  54. package/dist/utils/helpers.d.ts +55 -0
  55. package/dist/utils/helpers.d.ts.map +1 -0
  56. package/dist/utils/helpers.js +116 -0
  57. package/dist/utils/helpers.js.map +1 -0
  58. package/dist/utils/logger.d.ts +14 -0
  59. package/dist/utils/logger.d.ts.map +1 -0
  60. package/dist/utils/logger.js +70 -0
  61. package/dist/utils/logger.js.map +1 -0
  62. package/package.json +69 -0
package/README.md ADDED
@@ -0,0 +1,362 @@
1
+ # ๐Ÿงน Vacuum
2
+
3
+ **Suck up forgotten rent from Solana accounts.**
4
+
5
+ Vacuum is an automated rent-reclaim bot that monitors **operator-owned** token accounts and safely reclaims rent SOL when accounts are closed or have zero balance. Perfect for custodial services, development environments, and anyone managing their own Solana token accounts at scale.
6
+
7
+ ![NPM Version](https://img.shields.io/badge/version-1.0.0-blue)
8
+ ![License](https://img.shields.io/badge/license-MIT-green)
9
+ ![Solana](https://img.shields.io/badge/solana-compatible-purple)
10
+
11
+ ---
12
+
13
+ ## ๐Ÿš€ Quick Start
14
+
15
+ ```bash
16
+ # Clone and install
17
+ git clone https://github.com/your-username/vacuum-sol
18
+ cd vacuum-sol
19
+ npm install && npm run build
20
+
21
+ # Configure
22
+ cp .env.example .env
23
+ # Edit .env with your TREASURY_ADDRESS
24
+
25
+ # Run
26
+ npm start -- scan # Find accounts
27
+ npm start -- check --all # Find reclaimable
28
+ npm start -- reclaim --dry-run # Preview reclaim
29
+ ```
30
+
31
+ ---
32
+
33
+ ## ๐Ÿ“– Understanding Solana Rent
34
+
35
+ ### What is Rent?
36
+
37
+ Every Solana account must hold **rent** (SOL) to stay active:
38
+
39
+ | Account Type | Rent Cost |
40
+ | ------------------------- | ------------ |
41
+ | Token Account (165 bytes) | ~0.00204 SOL |
42
+ | 1KB Account | ~0.00696 SOL |
43
+
44
+ When accounts are **closed**, this rent is returned to a designated address.
45
+
46
+ ### The Problem
47
+
48
+ - Developers and custodial services create thousands of token accounts
49
+ - Many get abandoned (testing, unused wallets, closed positions, etc.)
50
+ - Rent stays locked in zero-balance accounts forever unless reclaimed
51
+ - **Result**: Silent capital loss of ~0.00204 SOL per account
52
+
53
+ > **โš ๏ธ Important**: Vacuum reclaims rent from **accounts you own** (where your operator keypair is the account authority). It does NOT work for accounts where you merely paid transaction fees as a paymaster. See [Authority Model](#-authority-model) for details.
54
+
55
+ ### The Solution
56
+
57
+ Vacuum automatically:
58
+
59
+ 1. **Tracks** sponsored accounts in a local database
60
+ 2. **Detects** accounts with 0 balance (safe to close)
61
+ 3. **Reclaims** rent back to your treasury
62
+ 4. **Reports** locked vs reclaimed totals
63
+
64
+ ---
65
+
66
+ ## โœจ Features
67
+
68
+ | Feature | Description |
69
+ | ----------------------- | ------------------------------------------------- |
70
+ | ๐Ÿ” **Smart Detection** | Finds zero-balance token accounts safe to close |
71
+ | ๐Ÿ›ก๏ธ **Safety First** | Dry-run mode, whitelists, balance verification |
72
+ | ๐Ÿค– **Telegram Bot** | Monitor and trigger reclaims from your phone |
73
+ | ๐Ÿ“Š **Audit Trail** | Every reclaim logged with TX signatures |
74
+ | โฐ **Automation Ready** | Run on schedule with cron, PM2, or GitHub Actions |
75
+ | ๐Ÿ’ป **CLI Interface** | 8 powerful commands for full control |
76
+
77
+ ---
78
+
79
+ ## ๐Ÿ“‹ CLI Commands
80
+
81
+ ```bash
82
+ # Scanning
83
+ vacuum scan # Scan operator's token accounts
84
+ vacuum scan --tx <sig> # Scan specific transactions
85
+
86
+ # Checking
87
+ vacuum check --all # Find all reclaimable accounts
88
+ vacuum check --address <pubkey> # Check specific account
89
+
90
+ # Reclaiming
91
+ vacuum reclaim --dry-run # Preview reclaim (safe)
92
+ vacuum reclaim --yes # Actually reclaim
93
+ vacuum reclaim --max 20 # Limit to 20 accounts
94
+
95
+ # Reporting
96
+ vacuum report # Show summary
97
+ vacuum report --history # Show reclaim history
98
+ vacuum report --format json # Export as JSON
99
+
100
+ # Protection
101
+ vacuum protect --add <pubkey> --reason "Active user"
102
+ vacuum protect --remove <pubkey>
103
+ vacuum protect --list
104
+
105
+ # Listing
106
+ vacuum list # List all tracked accounts
107
+ vacuum list --status reclaimable # Filter by status
108
+
109
+ # Bot
110
+ vacuum bot # Start Telegram bot
111
+
112
+ # Config
113
+ vacuum config # Show configuration
114
+ ```
115
+
116
+ ---
117
+
118
+ ## ๐Ÿค– Telegram Integration
119
+
120
+ ### Setup
121
+
122
+ 1. Create a bot: Message `@BotFather` on Telegram โ†’ `/newbot`
123
+ 2. Get your chat ID: Message `@userinfobot`
124
+ 3. Add to `.env`:
125
+ ```env
126
+ TELEGRAM_BOT_TOKEN=your-bot-token
127
+ TELEGRAM_CHAT_ID=your-chat-id
128
+ ```
129
+
130
+ ### Run the Bot
131
+
132
+ ```bash
133
+ npm start -- bot
134
+ ```
135
+
136
+ ### Telegram Commands
137
+
138
+ | Command | Action |
139
+ | ------------------ | ----------------- |
140
+ | `/status` | Show rent summary |
141
+ | `/scan` | Scan for accounts |
142
+ | `/check` | Find reclaimable |
143
+ | `/reclaim` | Preview reclaim |
144
+ | `/reclaim_execute` | Actually reclaim |
145
+ | `/history` | Recent reclaims |
146
+
147
+ ---
148
+
149
+ ## โš™๏ธ Configuration
150
+
151
+ Create a `.env` file:
152
+
153
+ ```env
154
+ # Required
155
+ SOLANA_RPC_URL=https://api.devnet.solana.com
156
+ TREASURY_ADDRESS=<your-wallet-address>
157
+ OPERATOR_KEYPAIR_PATH=./operator-keypair.json
158
+
159
+ # Optional
160
+ DRY_RUN=true
161
+ COOLDOWN_HOURS=24
162
+ MIN_INACTIVE_DAYS=7
163
+ TELEGRAM_BOT_TOKEN=
164
+ TELEGRAM_CHAT_ID=
165
+
166
+ # Database
167
+ DB_PATH=./data/accounts.db
168
+ ```
169
+
170
+ ---
171
+
172
+ ## ๐Ÿ›ก๏ธ Safety Features
173
+
174
+ โœ… **Dry-Run Mode** - Preview all actions before executing
175
+ โœ… **Zero Balance Check** - Only closes accounts with 0 tokens
176
+ โœ… **Protected Accounts** - Whitelist accounts to never reclaim
177
+ โœ… **Authority Verification** - Confirms operator owns the account
178
+ โœ… **Audit Trail** - All reclaims logged with TX signatures
179
+ โœ… **Cooldown Periods** - Wait N days before reclaiming inactive accounts
180
+
181
+ ---
182
+
183
+ ## ๐Ÿ” Authority Model
184
+
185
+ **Critical Understanding**: Vacuum can only reclaim rent from accounts where your operator keypair is the **account owner/authority**.
186
+
187
+ ### โœ… Works For:
188
+
189
+ - Token accounts created by your operator wallet
190
+ - Custodial service accounts you manage
191
+ - Development/testing accounts you own
192
+ - Any account where `closeAuthority` is your operator
193
+
194
+ ### โŒ Does NOT Work For:
195
+
196
+ - User-owned accounts (even if you paid fees as a Kora paymaster)
197
+ - Accounts where you're only the fee payer, not the owner
198
+ - Third-party wallets using your paymaster service
199
+
200
+ **Why?** On Solana, paying transaction fees โ‰  account ownership. Only the account's `owner` or `closeAuthority` can close it and reclaim rent.
201
+
202
+ ### Use Cases
203
+
204
+ 1. **Custodial Services**: Reclaim rent from your users' accounts you manage
205
+ 2. **Development**: Clean up test accounts from devnet/testnet
206
+ 3. **Personal Management**: Monitor your own token accounts
207
+ 4. **Bulk Account Management**: Manage rent across multiple operator profiles
208
+
209
+ ---
210
+
211
+ ## ๐Ÿ—๏ธ Architecture
212
+
213
+ ```
214
+ vacuum-sol/
215
+ โ”œโ”€โ”€ src/
216
+ โ”‚ โ”œโ”€โ”€ index.ts # CLI entry point
217
+ โ”‚ โ”œโ”€โ”€ config.ts # Environment configuration
218
+ โ”‚ โ”œโ”€โ”€ core/
219
+ โ”‚ โ”‚ โ”œโ”€โ”€ types.ts # TypeScript interfaces
220
+ โ”‚ โ”‚ โ”œโ”€โ”€ monitor.ts # Account scanning
221
+ โ”‚ โ”‚ โ”œโ”€โ”€ detector.ts # Reclaimable detection
222
+ โ”‚ โ”‚ โ””โ”€โ”€ reclaimer.ts # Rent reclaim execution
223
+ โ”‚ โ”œโ”€โ”€ db/
224
+ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # SQLite setup
225
+ โ”‚ โ”‚ โ””โ”€โ”€ accounts.ts # CRUD operations
226
+ โ”‚ โ”œโ”€โ”€ services/
227
+ โ”‚ โ”‚ โ”œโ”€โ”€ solana.ts # Solana RPC wrapper
228
+ โ”‚ โ”‚ โ”œโ”€โ”€ telegram.ts # Telegram bot
229
+ โ”‚ โ”‚ โ””โ”€โ”€ reporter.ts # Report generation
230
+ โ”‚ โ””โ”€โ”€ utils/
231
+ โ”‚ โ”œโ”€โ”€ logger.ts # Colored logging
232
+ โ”‚ โ””โ”€โ”€ helpers.ts # Utilities
233
+ โ”œโ”€โ”€ scripts/
234
+ โ”‚ โ””โ”€โ”€ simulate.ts # Devnet testing
235
+ โ””โ”€โ”€ landing/
236
+ โ””โ”€โ”€ index.html # Landing page
237
+ ```
238
+
239
+ ---
240
+
241
+ ## ๐Ÿงช Testing on Devnet
242
+
243
+ ### 1. Setup
244
+
245
+ ```bash
246
+ # Generate keypair
247
+ solana-keygen new -o operator-keypair.json
248
+
249
+ # Get devnet SOL
250
+ solana airdrop 2 --keypair operator-keypair.json --url devnet
251
+
252
+ # Configure for devnet
253
+ # Edit .env: SOLANA_RPC_URL=https://api.devnet.solana.com
254
+ ```
255
+
256
+ ### 2. Create Test Accounts
257
+
258
+ ```bash
259
+ npx tsx scripts/simulate.ts create
260
+ ```
261
+
262
+ This creates token accounts with 0 balance for testing.
263
+
264
+ ### 3. Run the Bot
265
+
266
+ ```bash
267
+ npm start -- scan
268
+ npm start -- check --all
269
+ npm start -- reclaim --dry-run
270
+ ```
271
+
272
+ ### 4. Actual Reclaim
273
+
274
+ ```bash
275
+ DRY_RUN=false npm start -- reclaim --yes
276
+ ```
277
+
278
+ ---
279
+
280
+ ## ๐Ÿ“Š Example Report
281
+
282
+ ```
283
+ ๐Ÿ“Š Rent Reclaim Summary
284
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
285
+ โ”‚ Total Accounts Tracked: 150 โ”‚
286
+ โ”‚ โ”œโ”€ Active: 85 โ”‚
287
+ โ”‚ โ”œโ”€ Reclaimable: 42 โ”‚
288
+ โ”‚ โ”œโ”€ Reclaimed: 20 โ”‚
289
+ โ”‚ โ””โ”€ Protected: 3 โ”‚
290
+ โ”‚ โ”‚
291
+ โ”‚ ๐Ÿ’ฐ Rent Status โ”‚
292
+ โ”‚ โ”œโ”€ Locked: 0.285 SOL โ”‚
293
+ โ”‚ โ””โ”€ Reclaimed: 0.041 SOL โ”‚
294
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
295
+ ```
296
+
297
+ ---
298
+
299
+ ## ๐Ÿ”ง Automation
300
+
301
+ ### Cron
302
+
303
+ ```bash
304
+ # Add to crontab for daily 6am checks
305
+ 0 6 * * * cd /path/to/vacuum-sol && DRY_RUN=false npm start -- reclaim --yes
306
+ ```
307
+
308
+ ### PM2
309
+
310
+ ```bash
311
+ npm install -g pm2
312
+ pm2 start npm --name "vacuum-reclaim" --cron "0 6 * * *" -- start -- reclaim --yes
313
+ ```
314
+
315
+ ### GitHub Actions
316
+
317
+ ```yaml
318
+ name: Daily Reclaim
319
+ on:
320
+ schedule:
321
+ - cron: '0 6 * * *'
322
+ jobs:
323
+ reclaim:
324
+ runs-on: ubuntu-latest
325
+ steps:
326
+ - uses: actions/checkout@v4
327
+ - uses: actions/setup-node@v4
328
+ - run: npm install && npm run build
329
+ - run: npm start -- reclaim --yes
330
+ env:
331
+ TREASURY_ADDRESS: ${{ secrets.TREASURY_ADDRESS }}
332
+ ```
333
+
334
+ ---
335
+
336
+ ## ๐Ÿค Contributing
337
+
338
+ Contributions welcome! Open an issue or PR.
339
+
340
+ ---
341
+
342
+ ## ๐Ÿ“„ License
343
+
344
+ MIT License - see [LICENSE](LICENSE)
345
+
346
+ ---
347
+
348
+ ## ๐Ÿ™ Acknowledgements
349
+
350
+ - [Kora](https://kora.network) - Gasless transaction infrastructure
351
+ - [Solana](https://solana.com) - High-performance blockchain
352
+ - SuperteamNG - Bounty program
353
+
354
+ ---
355
+
356
+ <div align="center">
357
+ <strong>Built with โค๏ธ for the Solana ecosystem</strong>
358
+ <br><br>
359
+ <a href="https://github.com/your-username/vacuum-sol">GitHub</a> โ€ข
360
+ <a href="https://t.me/vacuumsol">Telegram</a> โ€ข
361
+ <a href="https://your-landing-page.com">Website</a>
362
+ </div>
@@ -0,0 +1,15 @@
1
+ import { PublicKey } from '@solana/web3.js';
2
+ export interface Config {
3
+ rpcUrl: string;
4
+ treasuryAddress: PublicKey;
5
+ operatorKeypairPath: string;
6
+ koraNodeUrl?: string;
7
+ dryRun: boolean;
8
+ cooldownHours: number;
9
+ minInactiveDays: number;
10
+ dbPath: string;
11
+ }
12
+ export declare function loadConfig(): Config;
13
+ export declare function loadOperatorKeypair(keypairPath: string): Uint8Array;
14
+ export declare function getConfig(): Config;
15
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAQ3C,MAAM,WAAW,MAAM;IAErB,MAAM,EAAE,MAAM,CAAA;IAGd,eAAe,EAAE,SAAS,CAAA;IAC1B,mBAAmB,EAAE,MAAM,CAAA;IAG3B,WAAW,CAAC,EAAE,MAAM,CAAA;IAGpB,MAAM,EAAE,OAAO,CAAA;IACf,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,MAAM,CAAA;IAGvB,MAAM,EAAE,MAAM,CAAA;CACf;AAcD,wBAAgB,UAAU,IAAI,MAAM,CA+BnC;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,CAYnE;AAKD,wBAAgB,SAAS,IAAI,MAAM,CAKlC"}
package/dist/config.js ADDED
@@ -0,0 +1,58 @@
1
+ import { PublicKey } from '@solana/web3.js';
2
+ import { config as loadEnv } from 'dotenv';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import path from 'path';
5
+ // Load environment variables
6
+ loadEnv();
7
+ function getEnvOrThrow(key) {
8
+ const value = process.env[key];
9
+ if (!value) {
10
+ throw new Error(`Missing required environment variable: ${key}`);
11
+ }
12
+ return value;
13
+ }
14
+ function getEnvOrDefault(key, defaultValue) {
15
+ return process.env[key] || defaultValue;
16
+ }
17
+ export function loadConfig() {
18
+ const treasuryAddressStr = process.env.TREASURY_ADDRESS;
19
+ if (!treasuryAddressStr) {
20
+ throw new Error('TREASURY_ADDRESS is required. Set it in your .env file.');
21
+ }
22
+ let treasuryAddress;
23
+ try {
24
+ treasuryAddress = new PublicKey(treasuryAddressStr);
25
+ }
26
+ catch {
27
+ throw new Error(`Invalid TREASURY_ADDRESS: ${treasuryAddressStr}. Must be a valid Solana public key.`);
28
+ }
29
+ const operatorKeypairPath = getEnvOrDefault('OPERATOR_KEYPAIR_PATH', './operator-keypair.json');
30
+ return {
31
+ rpcUrl: getEnvOrDefault('SOLANA_RPC_URL', 'https://api.devnet.solana.com'),
32
+ treasuryAddress,
33
+ operatorKeypairPath,
34
+ koraNodeUrl: process.env.KORA_NODE_URL,
35
+ dryRun: getEnvOrDefault('DRY_RUN', 'true') === 'true',
36
+ cooldownHours: parseInt(getEnvOrDefault('COOLDOWN_HOURS', '24'), 10),
37
+ minInactiveDays: parseInt(getEnvOrDefault('MIN_INACTIVE_DAYS', '7'), 10),
38
+ dbPath: getEnvOrDefault('DB_PATH', './data/accounts.db'),
39
+ };
40
+ }
41
+ export function loadOperatorKeypair(keypairPath) {
42
+ const resolvedPath = path.resolve(keypairPath);
43
+ if (!existsSync(resolvedPath)) {
44
+ throw new Error(`Operator keypair not found at: ${resolvedPath}\n` +
45
+ 'Generate one with: solana-keygen new -o operator-keypair.json');
46
+ }
47
+ const keypairData = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
48
+ return Uint8Array.from(keypairData);
49
+ }
50
+ // Export a singleton config (lazy loaded)
51
+ let _config = null;
52
+ export function getConfig() {
53
+ if (!_config) {
54
+ _config = loadConfig();
55
+ }
56
+ return _config;
57
+ }
58
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,MAAM,QAAQ,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AAC7C,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB,6BAA6B;AAC7B,OAAO,EAAE,CAAA;AAsBT,SAAS,aAAa,CAAC,GAAW;IAChC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC9B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,0CAA0C,GAAG,EAAE,CAAC,CAAA;IAClE,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,eAAe,CAAC,GAAW,EAAE,YAAoB;IACxD,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,YAAY,CAAA;AACzC,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAA;IAEvD,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;IAC5E,CAAC;IAED,IAAI,eAA0B,CAAA;IAC9B,IAAI,CAAC;QACH,eAAe,GAAG,IAAI,SAAS,CAAC,kBAAkB,CAAC,CAAA;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,6BAA6B,kBAAkB,sCAAsC,CACtF,CAAA;IACH,CAAC;IAED,MAAM,mBAAmB,GAAG,eAAe,CACzC,uBAAuB,EACvB,yBAAyB,CAC1B,CAAA;IAED,OAAO;QACL,MAAM,EAAE,eAAe,CAAC,gBAAgB,EAAE,+BAA+B,CAAC;QAC1E,eAAe;QACf,mBAAmB;QACnB,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa;QACtC,MAAM,EAAE,eAAe,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,MAAM;QACrD,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC,gBAAgB,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;QACpE,eAAe,EAAE,QAAQ,CAAC,eAAe,CAAC,mBAAmB,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC;QACxE,MAAM,EAAE,eAAe,CAAC,SAAS,EAAE,oBAAoB,CAAC;KACzD,CAAA;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,WAAmB;IACrD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAE9C,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,kCAAkC,YAAY,IAAI;YAChD,+DAA+D,CAClE,CAAA;IACH,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAA;IACnE,OAAO,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;AACrC,CAAC;AAED,0CAA0C;AAC1C,IAAI,OAAO,GAAkB,IAAI,CAAA;AAEjC,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,UAAU,EAAE,CAAA;IACxB,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { PublicKey } from '@solana/web3.js';
2
+ import type { DetectionResult } from './types.js';
3
+ /**
4
+ * Detector for identifying reclaimable accounts
5
+ */
6
+ export declare class ReclaimableDetector {
7
+ private get minInactiveDays();
8
+ /**
9
+ * Check if a single account is reclaimable
10
+ */
11
+ checkAccount(pubkey: PublicKey): Promise<DetectionResult | null>;
12
+ /**
13
+ * Find all reclaimable accounts
14
+ */
15
+ findAllReclaimable(): Promise<DetectionResult[]>;
16
+ /**
17
+ * Find only safe-to-reclaim accounts (zero balance token accounts)
18
+ */
19
+ findSafeReclaimable(): Promise<DetectionResult[]>;
20
+ /**
21
+ * Quick check if an account is closed
22
+ */
23
+ isAccountClosed(pubkey: PublicKey): Promise<boolean>;
24
+ /**
25
+ * Get summary of reclaimable rent
26
+ */
27
+ getReclaimableSummary(): Promise<{
28
+ totalAccounts: number;
29
+ safeToReclaim: number;
30
+ unsafeNeedsReview: number;
31
+ totalReclaimableLamports: number;
32
+ safeReclaimableLamports: number;
33
+ }>;
34
+ }
35
+ export declare const detector: ReclaimableDetector;
36
+ //# sourceMappingURL=detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../../src/core/detector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAW3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;GAEG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,KAAK,eAAe,GAE1B;IAED;;OAEG;IACG,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAwFtE;;OAEG;IACG,kBAAkB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IA+BtD;;OAEG;IACG,mBAAmB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAKvD;;OAEG;IACG,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAK1D;;OAEG;IACG,qBAAqB,IAAI,OAAO,CAAC;QACrC,aAAa,EAAE,MAAM,CAAA;QACrB,aAAa,EAAE,MAAM,CAAA;QACrB,iBAAiB,EAAE,MAAM,CAAA;QACzB,wBAAwB,EAAE,MAAM,CAAA;QAChC,uBAAuB,EAAE,MAAM,CAAA;KAChC,CAAC;CAoBH;AAGD,eAAO,MAAM,QAAQ,qBAA4B,CAAA"}
@@ -0,0 +1,142 @@
1
+ import { getConfig } from '../config.js';
2
+ import { getAllTrackedAccounts, getTrackedAccount, isAccountProtected, updateAccountState, } from '../db/accounts.js';
3
+ import { getAccountInfo, getTokenAccountData } from '../services/solana.js';
4
+ import { daysSince, formatSol } from '../utils/helpers.js';
5
+ import { logger } from '../utils/logger.js';
6
+ /**
7
+ * Detector for identifying reclaimable accounts
8
+ */
9
+ export class ReclaimableDetector {
10
+ get minInactiveDays() {
11
+ return getConfig().minInactiveDays;
12
+ }
13
+ /**
14
+ * Check if a single account is reclaimable
15
+ */
16
+ async checkAccount(pubkey) {
17
+ const trackedAccount = getTrackedAccount(pubkey);
18
+ if (!trackedAccount) {
19
+ logger.warn(`Account not tracked: ${pubkey.toBase58()}`);
20
+ return null;
21
+ }
22
+ // Skip protected accounts
23
+ if (isAccountProtected(pubkey)) {
24
+ logger.debug(`Account is protected: ${pubkey.toBase58()}`);
25
+ return null;
26
+ }
27
+ // Get current account state from chain
28
+ const accountInfo = await getAccountInfo(pubkey);
29
+ // Account is closed (no longer exists on chain)
30
+ if (!accountInfo) {
31
+ const result = {
32
+ account: trackedAccount,
33
+ reason: 'closed',
34
+ reclaimableLamports: trackedAccount.rentLamports,
35
+ safe: true,
36
+ details: 'Account no longer exists on-chain. Rent was already returned to original payer.',
37
+ };
38
+ // Update status in DB
39
+ updateAccountState(pubkey, { status: 'reclaimed', rentLamports: 0 });
40
+ return result;
41
+ }
42
+ // Check if it's a token account with zero balance
43
+ if (trackedAccount.accountType === 'token_account' ||
44
+ trackedAccount.accountType === 'ata') {
45
+ const tokenData = await getTokenAccountData(pubkey);
46
+ if (tokenData && tokenData.amount === 0n) {
47
+ const result = {
48
+ account: {
49
+ ...trackedAccount,
50
+ rentLamports: tokenData.lamports,
51
+ },
52
+ reason: 'zero_balance',
53
+ reclaimableLamports: tokenData.lamports,
54
+ safe: true,
55
+ details: `Token account has 0 balance. Can close and reclaim ${formatSol(tokenData.lamports)}.`,
56
+ };
57
+ // Update status in DB
58
+ updateAccountState(pubkey, {
59
+ status: 'reclaimable',
60
+ rentLamports: tokenData.lamports,
61
+ });
62
+ return result;
63
+ }
64
+ }
65
+ // Check for inactivity
66
+ if (trackedAccount.lastActivityAt) {
67
+ const inactiveDays = daysSince(trackedAccount.lastActivityAt);
68
+ if (inactiveDays >= this.minInactiveDays) {
69
+ const result = {
70
+ account: trackedAccount,
71
+ reason: 'inactive',
72
+ reclaimableLamports: accountInfo.lamports,
73
+ safe: false, // Mark as unsafe since we can't automatically close non-empty accounts
74
+ details: `Account inactive for ${inactiveDays} days. Manual review recommended.`,
75
+ };
76
+ return result;
77
+ }
78
+ }
79
+ // Account is active/not reclaimable
80
+ updateAccountState(pubkey, {
81
+ status: 'active',
82
+ rentLamports: accountInfo.lamports,
83
+ });
84
+ return null;
85
+ }
86
+ /**
87
+ * Find all reclaimable accounts
88
+ */
89
+ async findAllReclaimable() {
90
+ const accounts = getAllTrackedAccounts();
91
+ const results = [];
92
+ logger.info(`Checking ${accounts.length} tracked accounts...`);
93
+ for (const account of accounts) {
94
+ if (account.status === 'reclaimed' || account.status === 'protected') {
95
+ continue;
96
+ }
97
+ try {
98
+ const result = await this.checkAccount(account.pubkey);
99
+ if (result) {
100
+ results.push(result);
101
+ }
102
+ }
103
+ catch (error) {
104
+ logger.error(`Error checking account ${account.pubkey.toBase58()}:`, error);
105
+ }
106
+ }
107
+ logger.info(`Found ${results.length} reclaimable accounts out of ${accounts.length} total`);
108
+ return results;
109
+ }
110
+ /**
111
+ * Find only safe-to-reclaim accounts (zero balance token accounts)
112
+ */
113
+ async findSafeReclaimable() {
114
+ const all = await this.findAllReclaimable();
115
+ return all.filter((r) => r.safe);
116
+ }
117
+ /**
118
+ * Quick check if an account is closed
119
+ */
120
+ async isAccountClosed(pubkey) {
121
+ const info = await getAccountInfo(pubkey);
122
+ return info === null;
123
+ }
124
+ /**
125
+ * Get summary of reclaimable rent
126
+ */
127
+ async getReclaimableSummary() {
128
+ const results = await this.findAllReclaimable();
129
+ const safeResults = results.filter((r) => r.safe);
130
+ const unsafeResults = results.filter((r) => !r.safe);
131
+ return {
132
+ totalAccounts: results.length,
133
+ safeToReclaim: safeResults.length,
134
+ unsafeNeedsReview: unsafeResults.length,
135
+ totalReclaimableLamports: results.reduce((sum, r) => sum + r.reclaimableLamports, 0),
136
+ safeReclaimableLamports: safeResults.reduce((sum, r) => sum + r.reclaimableLamports, 0),
137
+ };
138
+ }
139
+ }
140
+ // Export singleton instance
141
+ export const detector = new ReclaimableDetector();
142
+ //# sourceMappingURL=detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.js","sourceRoot":"","sources":["../../src/core/detector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { PublicKey } from '@solana/web3.js';
2
+ import type { AccountType, ScanOptions, TrackedAccount } from './types.js';
3
+ /**
4
+ * Monitor for tracking sponsored accounts
5
+ */
6
+ export declare class AccountMonitor {
7
+ /**
8
+ * Scan all token accounts owned by the operator and add them to tracking
9
+ */
10
+ scanOperatorAccounts(): Promise<TrackedAccount[]>;
11
+ /**
12
+ * Scan accounts from transaction signatures (for Kora-sponsored accounts)
13
+ * This would parse transaction logs to find sponsored account creations
14
+ */
15
+ scanFromSignatures(signatures: string[], options?: ScanOptions): Promise<TrackedAccount[]>;
16
+ /**
17
+ * Add a single account to tracking
18
+ */
19
+ trackAccount(pubkey: PublicKey, sponsorTx?: string): Promise<TrackedAccount | null>;
20
+ /**
21
+ * Get tracking statistics
22
+ */
23
+ getStats(): Promise<{
24
+ totalTracked: number;
25
+ byType: Record<AccountType, number>;
26
+ byStatus: Record<string, number>;
27
+ totalRentLocked: number;
28
+ }>;
29
+ }
30
+ export declare const monitor: AccountMonitor;
31
+ //# sourceMappingURL=monitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor.d.ts","sourceRoot":"","sources":["../../src/core/monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAc3C,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE1E;;GAEG;AACH,qBAAa,cAAc;IACzB;;OAEG;IACG,oBAAoB,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IA0CvD;;;OAGG;IACG,kBAAkB,CACtB,UAAU,EAAE,MAAM,EAAE,EACpB,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,cAAc,EAAE,CAAC;IA8E5B;;OAEG;IACG,YAAY,CAChB,MAAM,EAAE,SAAS,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAqCjC;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC;QACxB,YAAY,EAAE,MAAM,CAAA;QACpB,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;QACnC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,eAAe,EAAE,MAAM,CAAA;KACxB,CAAC;CAmCH;AAGD,eAAO,MAAM,OAAO,gBAAuB,CAAA"}