solana-mint-recovery-engine 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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const scan_1 = require("../src/commands/scan");
6
+ const inspect_1 = require("../src/commands/inspect");
7
+ const build_1 = require("../src/commands/build");
8
+ const sign_1 = require("../src/commands/sign");
9
+ const simulate_1 = require("../src/commands/simulate");
10
+ const submit_1 = require("../src/commands/submit");
11
+ const recover_1 = require("../src/commands/recover");
12
+ const config_1 = require("../src/commands/config");
13
+ const doctor_1 = require("../src/commands/doctor");
14
+ const program = new commander_1.Command();
15
+ program
16
+ .name('smre')
17
+ .description('Solana Mint & Token Account Recovery Engine (SMRE) CLI')
18
+ .version('1.0.0')
19
+ .option('-c, --config <path>', 'Path to custom configuration JSON file');
20
+ program
21
+ .command('scan <address-file>')
22
+ .description('Scan a list of addresses in a file for recoverable SOL above the rent floor')
23
+ .option('--json', 'Output results as JSON')
24
+ .action((addressFile, options) => {
25
+ (0, scan_1.runScan)(addressFile, { json: options.json, config: program.opts().config });
26
+ });
27
+ program
28
+ .command('inspect <address>')
29
+ .description('Inspect details of a single account (balance, rent floor, authority)')
30
+ .option('--json', 'Output results as JSON')
31
+ .action((address, options) => {
32
+ (0, inspect_1.runInspect)(address, { json: options.json, config: program.opts().config });
33
+ });
34
+ program
35
+ .command('build <address>')
36
+ .description('Build an unsigned transaction to withdraw excess lamports from an account')
37
+ .option('-d, --destination <address>', 'Recipient address for the recovered SOL')
38
+ .option('-o, --output <path>', 'Output transaction envelope JSON file path', 'unsigned_tx.json')
39
+ .option('--fee-wallet <address>', 'Wallet to receive the platform fee')
40
+ .option('--fee-percent <number>', 'Platform fee percentage (0-100)')
41
+ .option('--fee-payer <address>', 'Fee payer for the transaction')
42
+ .action((address, options) => {
43
+ (0, build_1.runBuild)(address, {
44
+ destination: options.destination,
45
+ output: options.output,
46
+ config: program.opts().config,
47
+ feeWallet: options.feeWallet,
48
+ feePercent: options.feePercent,
49
+ feePayer: options.feePayer,
50
+ });
51
+ });
52
+ program
53
+ .command('simulate <envelope-file>')
54
+ .description('Simulate the transaction envelope on-chain to verify safety')
55
+ .action((envelopeFile) => {
56
+ (0, simulate_1.runSimulate)(envelopeFile, { config: program.opts().config });
57
+ });
58
+ program
59
+ .command('sign <envelope-file>')
60
+ .description('Sign an offline transaction envelope using a local private key or keypair file')
61
+ .requiredOption('-k, --keypair <secret>', 'Path to keypair JSON file, base58 private key, or JSON array')
62
+ .option('-o, --output <path>', 'Output signed transaction envelope path (defaults to input file)')
63
+ .action((envelopeFile, options) => {
64
+ (0, sign_1.runSign)(envelopeFile, {
65
+ keypair: options.keypair,
66
+ output: options.output,
67
+ });
68
+ });
69
+ program
70
+ .command('submit <envelope-file>')
71
+ .description('Broadcast a fully signed transaction envelope to the Solana network')
72
+ .action((envelopeFile) => {
73
+ (0, submit_1.runSubmit)(envelopeFile, { config: program.opts().config });
74
+ });
75
+ program
76
+ .command('recover <address>')
77
+ .description('Convenience command to inspect, build, sign, and submit recovery in one online step')
78
+ .requiredOption('-k, --keypair <secret>', 'Signing keypair file, array, or base58 private key')
79
+ .option('-d, --destination <address>', 'Recipient address for the recovered SOL')
80
+ .option('--fee-wallet <address>', 'Wallet to receive the platform fee')
81
+ .option('--fee-percent <number>', 'Platform fee percentage (0-100)')
82
+ .action((address, options) => {
83
+ (0, recover_1.runRecover)(address, {
84
+ keypair: options.keypair,
85
+ destination: options.destination,
86
+ config: program.opts().config,
87
+ feeWallet: options.feeWallet,
88
+ feePercent: options.feePercent,
89
+ });
90
+ });
91
+ // Nested config commands
92
+ const configCmd = program.command('config').description('Configure SMRE settings');
93
+ configCmd
94
+ .command('init')
95
+ .description('Initialize a default config file in the current directory')
96
+ .option('-o, --output <path>', 'Output path for config file', 'smre.config.json')
97
+ .action((options) => {
98
+ (0, config_1.initConfig)(options.output);
99
+ });
100
+ program
101
+ .command('doctor')
102
+ .description('Run system health checks, verify RPC endpoints and latency')
103
+ .action(() => {
104
+ (0, doctor_1.runDoctor)(program.opts().config);
105
+ });
106
+ program.parse(process.argv);
@@ -0,0 +1,8 @@
1
+ export declare function runBuild(addressStr: string, options: {
2
+ destination?: string;
3
+ output: string;
4
+ config?: string;
5
+ feeWallet?: string;
6
+ feePercent?: string;
7
+ feePayer?: string;
8
+ }): Promise<void>;
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runBuild = runBuild;
7
+ const web3_js_1 = require("@solana/web3.js");
8
+ const rpc_1 = require("../io/rpc");
9
+ const rent_1 = require("../core/rent");
10
+ const transaction_1 = require("../core/transaction");
11
+ const file_1 = require("../io/file");
12
+ const chalk_1 = __importDefault(require("chalk"));
13
+ async function runBuild(addressStr, options) {
14
+ try {
15
+ const config = (0, file_1.loadConfig)(options.config);
16
+ const rpc = new rpc_1.RpcAdapter(config.rpcUrl, config.commitment);
17
+ const source = new web3_js_1.PublicKey(addressStr);
18
+ // 1. Inspect account state
19
+ const account = await rpc.inspectAccount(source);
20
+ if (!account.isToken2022) {
21
+ throw new Error(`Account is not owned by the Token-2022 program. Recoveries are not supported.`);
22
+ }
23
+ const { excess, rentFloor } = (0, rent_1.calculateExcessLamports)(account.dataSize, account.balance);
24
+ if (excess <= 0n) {
25
+ throw new Error(`Account has no excess lamports above the rent floor (${rentFloor.toLocaleString()} lamports).`);
26
+ }
27
+ // 2. Resolve destination
28
+ const destinationStr = options.destination || config.defaultDestination;
29
+ if (!destinationStr) {
30
+ throw new Error(`No destination address specified. Use --destination <address> or define defaultDestination in config.`);
31
+ }
32
+ const destination = new web3_js_1.PublicKey(destinationStr);
33
+ // 3. Resolve authority
34
+ const authority = account.authority;
35
+ if (!authority) {
36
+ throw new Error(`No authority found for account. It is not recoverable.`);
37
+ }
38
+ // 4. Resolve fee payer
39
+ let feePayer = authority;
40
+ if (account.type === 'multisig' && account.multisigSigners && account.multisigSigners.length > 0) {
41
+ if (options.feePayer) {
42
+ feePayer = new web3_js_1.PublicKey(options.feePayer);
43
+ }
44
+ else {
45
+ // Default to the first co-signer for multisig accounts
46
+ feePayer = account.multisigSigners[0];
47
+ }
48
+ }
49
+ else if (options.feePayer) {
50
+ feePayer = new web3_js_1.PublicKey(options.feePayer);
51
+ }
52
+ // 5. Resolve platform fee wallet & percentage
53
+ const feeWalletStr = options.feeWallet || config.feeWallet;
54
+ const feePercentage = options.feePercent !== undefined
55
+ ? parseInt(options.feePercent, 10)
56
+ : config.feePercentage;
57
+ const feeWallet = feeWalletStr ? new web3_js_1.PublicKey(feeWalletStr) : undefined;
58
+ // 6. Fetch latest blockhash
59
+ const blockhash = await rpc.getLatestBlockhash();
60
+ // 7. Build instructions
61
+ const instructions = (0, transaction_1.buildRecoveryInstructions)({
62
+ source,
63
+ destination,
64
+ authority,
65
+ excessAmount: excess,
66
+ developerFeeWallet: feeWallet,
67
+ feePercentage,
68
+ multiSigners: account.multisigSigners,
69
+ });
70
+ // 8. Compile transaction
71
+ const tx = new web3_js_1.Transaction();
72
+ tx.add(...instructions);
73
+ tx.recentBlockhash = blockhash;
74
+ tx.feePayer = feePayer;
75
+ // Serialize unsigned transaction
76
+ const serialized = tx.serialize({
77
+ requireAllSignatures: false,
78
+ verifySignatures: false,
79
+ });
80
+ const rawTransactionB64 = serialized.toString('base64');
81
+ // 9. Create envelope JSON
82
+ const envelope = {
83
+ version: '1.0.0',
84
+ type: 'withdraw-excess-lamports',
85
+ metadata: {
86
+ sourceAddress: source.toBase58(),
87
+ sourceType: account.type,
88
+ destinationAddress: destination.toBase58(),
89
+ authorityAddress: authority.toBase58(),
90
+ excessLamports: excess.toString(),
91
+ rentFloor: rentFloor.toString(),
92
+ createdAt: new Date().toISOString(),
93
+ blockhash,
94
+ },
95
+ rawTransactionB64,
96
+ signatures: [],
97
+ };
98
+ if (account.type === 'multisig' && account.multisigSigners) {
99
+ envelope.metadata.multisigThreshold = account.multisigThreshold;
100
+ envelope.signatures = account.multisigSigners.map(signer => ({
101
+ pubkey: signer.toBase58(),
102
+ signature: null,
103
+ }));
104
+ }
105
+ else {
106
+ envelope.signatures = [
107
+ {
108
+ pubkey: authority.toBase58(),
109
+ signature: null,
110
+ },
111
+ ];
112
+ }
113
+ // Ensure feePayer is present in signatures
114
+ const feePayerStr = feePayer.toBase58();
115
+ if (!envelope.signatures.some(s => s.pubkey === feePayerStr)) {
116
+ envelope.signatures.push({
117
+ pubkey: feePayerStr,
118
+ signature: null,
119
+ });
120
+ }
121
+ // Write envelope file
122
+ (0, file_1.writeEnvelope)(options.output, envelope);
123
+ console.log(chalk_1.default.green(`✔ Built transaction envelope and saved to ${options.output}`));
124
+ console.log('\nTransaction Metadata:');
125
+ console.log(` Source: ${envelope.metadata.sourceAddress} (${envelope.metadata.sourceType})`);
126
+ console.log(` Destination: ${envelope.metadata.destinationAddress}`);
127
+ console.log(` Authority: ${envelope.metadata.authorityAddress}`);
128
+ console.log(` Fee Payer: ${feePayer.toBase58()}`);
129
+ console.log(` Recoverable: ${Number(excess) / 1e9} SOL`);
130
+ if (feeWallet) {
131
+ console.log(` Fee Split: ${feePercentage}% to ${feeWallet.toBase58()}`);
132
+ }
133
+ console.log(` Blockhash: ${envelope.metadata.blockhash}`);
134
+ }
135
+ catch (err) {
136
+ console.error(chalk_1.default.red(`✘ Failed to build transaction: ${err.message}`));
137
+ process.exit(1);
138
+ }
139
+ }
@@ -0,0 +1 @@
1
+ export declare function initConfig(outputPath?: string): void;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.initConfig = initConfig;
7
+ const file_1 = require("../io/file");
8
+ const validation_1 = require("../core/validation");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ function initConfig(outputPath = 'smre.config.json') {
11
+ try {
12
+ const defaultConfig = validation_1.ConfigSchema.parse({});
13
+ (0, file_1.writeConfig)(outputPath, defaultConfig);
14
+ console.log(chalk_1.default.green(`✔ Initialized default configuration at ${outputPath}`));
15
+ console.log(JSON.stringify(defaultConfig, null, 2));
16
+ }
17
+ catch (err) {
18
+ console.error(chalk_1.default.red(`✘ Failed to initialize configuration: ${err.message}`));
19
+ process.exit(1);
20
+ }
21
+ }
@@ -0,0 +1 @@
1
+ export declare function runDoctor(configPath?: string): Promise<void>;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runDoctor = runDoctor;
7
+ const web3_js_1 = require("@solana/web3.js");
8
+ const file_1 = require("../io/file");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ async function runDoctor(configPath) {
11
+ console.log(chalk_1.default.bold('Running SMRE Doctor Diagnostics...'));
12
+ let isHealthy = true;
13
+ // 1. Validate Config Load
14
+ try {
15
+ const config = (0, file_1.loadConfig)(configPath);
16
+ console.log(chalk_1.default.green(`✔ Configuration loaded successfully.`));
17
+ console.log(` RPC Endpoint: ${config.rpcUrl}`);
18
+ console.log(` Commitment: ${config.commitment}`);
19
+ console.log(` Default Destination: ${config.defaultDestination || 'None specified'}`);
20
+ console.log(` Platform Fee %: ${config.feePercentage}%`);
21
+ // 2. Validate RPC Connectivity
22
+ console.log('\nTesting RPC connectivity...');
23
+ const startTime = Date.now();
24
+ const connection = new web3_js_1.Connection(config.rpcUrl, config.commitment);
25
+ const version = await connection.getVersion();
26
+ const latency = Date.now() - startTime;
27
+ console.log(chalk_1.default.green(`✔ Connected to Solana node version: ${version['solana-core']}`));
28
+ console.log(` Latency: ${latency}ms`);
29
+ const slot = await connection.getSlot();
30
+ console.log(chalk_1.default.green(`✔ Latest confirmed slot: ${slot}`));
31
+ }
32
+ catch (err) {
33
+ console.error(chalk_1.default.red(`✘ Diagnostic failed: ${err.message}`));
34
+ isHealthy = false;
35
+ }
36
+ if (isHealthy) {
37
+ console.log(chalk_1.default.bold.green('\n✔ Environment is healthy! You are ready to run SMRE.'));
38
+ process.exit(0);
39
+ }
40
+ else {
41
+ console.log(chalk_1.default.bold.red('\n✘ Some checks failed. Please review your settings.'));
42
+ process.exit(1);
43
+ }
44
+ }
@@ -0,0 +1,4 @@
1
+ export declare function runInspect(addressStr: string, options: {
2
+ json?: boolean;
3
+ config?: string;
4
+ }): Promise<void>;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runInspect = runInspect;
7
+ const web3_js_1 = require("@solana/web3.js");
8
+ const rpc_1 = require("../io/rpc");
9
+ const rent_1 = require("../core/rent");
10
+ const file_1 = require("../io/file");
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ async function runInspect(addressStr, options) {
13
+ try {
14
+ const config = (0, file_1.loadConfig)(options.config);
15
+ const rpc = new rpc_1.RpcAdapter(config.rpcUrl, config.commitment);
16
+ const address = new web3_js_1.PublicKey(addressStr);
17
+ const account = await rpc.inspectAccount(address);
18
+ const { excess, rentFloor } = (0, rent_1.calculateExcessLamports)(account.dataSize, account.balance);
19
+ const solBalance = Number(account.balance) / 1e9;
20
+ const solRentFloor = Number(rentFloor) / 1e9;
21
+ const solExcess = Number(excess) / 1e9;
22
+ if (options.json) {
23
+ console.log(JSON.stringify({
24
+ address: account.address.toBase58(),
25
+ type: account.type,
26
+ programOwner: account.ownerProgram.toBase58(),
27
+ balance: account.balance.toString(),
28
+ solBalance,
29
+ rentFloor: rentFloor.toString(),
30
+ solRentFloor,
31
+ excessLamports: excess.toString(),
32
+ solExcess,
33
+ authority: account.authority ? account.authority.toBase58() : null,
34
+ isToken2022: account.isToken2022,
35
+ isRecoverable: excess > 0n && account.isToken2022,
36
+ multisigSigners: account.multisigSigners?.map(s => s.toBase58()),
37
+ multisigThreshold: account.multisigThreshold,
38
+ }, null, 2));
39
+ process.exit(excess > 0n && account.isToken2022 ? 0 : 3);
40
+ }
41
+ console.log(chalk_1.default.bold(`\nAccount Check: ${address.toBase58()}`));
42
+ console.log('------------------------------------------------------');
43
+ console.log(`Program Owner: ${account.isToken2022 ? chalk_1.default.green('Token-2022') : chalk_1.default.yellow('Legacy Token')} (${account.ownerProgram.toBase58()})`);
44
+ console.log(`Account Type: ${chalk_1.default.blue(account.type.toUpperCase())}`);
45
+ console.log(`Total Balance: ${chalk_1.default.cyan(solBalance.toFixed(9) + ' SOL')} (${account.balance.toLocaleString()} lamports)`);
46
+ console.log(`Rent Floor: ${solRentFloor.toFixed(9)} SOL (${rentFloor.toLocaleString()} lamports)`);
47
+ console.log(`Recoverable: ${excess > 0n ? chalk_1.default.green(solExcess.toFixed(9) + ' SOL') : chalk_1.default.grey('0.000000000 SOL')} (${excess.toLocaleString()} lamports)`);
48
+ if (account.authority) {
49
+ console.log(`Authority: ${account.authority.toBase58()}`);
50
+ }
51
+ else {
52
+ console.log(`Authority: ${chalk_1.default.red('NONE (Mint authority disabled)')}`);
53
+ }
54
+ if (account.type === 'multisig') {
55
+ console.log(`Threshold: ${account.multisigThreshold} of ${account.multisigSigners?.length}`);
56
+ console.log('Co-signers:');
57
+ account.multisigSigners?.forEach((signer, index) => {
58
+ console.log(` - [${index + 1}] ${signer.toBase58()}`);
59
+ });
60
+ }
61
+ if (!account.isToken2022) {
62
+ console.log(chalk_1.default.red('\n⚠ ERROR: Legacy Token accounts do not support WithdrawExcessLamports.'));
63
+ process.exit(1);
64
+ }
65
+ if (excess === 0n) {
66
+ console.log(chalk_1.default.yellow('\nℹ Account has no excess lamports above the rent floor.'));
67
+ process.exit(3);
68
+ }
69
+ console.log(chalk_1.default.green('\n✔ STATUS: RECOVERABLE'));
70
+ process.exit(0);
71
+ }
72
+ catch (err) {
73
+ console.error(chalk_1.default.red(`\n✘ Error inspecting account: ${err.message}`));
74
+ process.exit(1);
75
+ }
76
+ }
@@ -0,0 +1,7 @@
1
+ export declare function runRecover(addressStr: string, options: {
2
+ keypair: string;
3
+ destination?: string;
4
+ config?: string;
5
+ feeWallet?: string;
6
+ feePercent?: string;
7
+ }): Promise<void>;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runRecover = runRecover;
7
+ const web3_js_1 = require("@solana/web3.js");
8
+ const rpc_1 = require("../io/rpc");
9
+ const rent_1 = require("../core/rent");
10
+ const transaction_1 = require("../core/transaction");
11
+ const file_1 = require("../io/file");
12
+ const signer_1 = require("../signers/signer");
13
+ const chalk_1 = __importDefault(require("chalk"));
14
+ async function runRecover(addressStr, options) {
15
+ try {
16
+ const config = (0, file_1.loadConfig)(options.config);
17
+ const rpc = new rpc_1.RpcAdapter(config.rpcUrl, config.commitment);
18
+ const source = new web3_js_1.PublicKey(addressStr);
19
+ console.log(chalk_1.default.bold('Executing End-to-End Lamport Recovery...'));
20
+ // 1. Inspect target account
21
+ console.log('Inspecting target account...');
22
+ const account = await rpc.inspectAccount(source);
23
+ if (!account.isToken2022) {
24
+ throw new Error(`Account is not owned by the Token-2022 program. Only Token-2022 is supported.`);
25
+ }
26
+ const { excess, rentFloor } = (0, rent_1.calculateExcessLamports)(account.dataSize, account.balance);
27
+ if (excess <= 0n) {
28
+ throw new Error(`Account has no excess lamports above the rent floor (${rentFloor.toLocaleString()} lamports).`);
29
+ }
30
+ const solExcess = Number(excess) / 1e9;
31
+ console.log(chalk_1.default.green(`✔ Found ${solExcess.toFixed(9)} SOL recoverable.`));
32
+ // 2. Resolve destination
33
+ const destinationStr = options.destination || config.defaultDestination;
34
+ if (!destinationStr) {
35
+ throw new Error(`No destination address specified. Use --destination <address> or define defaultDestination in config.`);
36
+ }
37
+ const destination = new web3_js_1.PublicKey(destinationStr);
38
+ // 3. Load signing keypair
39
+ console.log('Loading keypair...');
40
+ const keypair = (0, signer_1.loadKeypair)(options.keypair);
41
+ const authority = account.authority;
42
+ if (!authority) {
43
+ throw new Error(`No authority found for account. Recovery not possible.`);
44
+ }
45
+ if (!keypair.publicKey.equals(authority)) {
46
+ // In multisig, the keypair might be a co-signer
47
+ if (account.type === 'multisig' && account.multisigSigners) {
48
+ const isCoSigner = account.multisigSigners.some(s => s.equals(keypair.publicKey));
49
+ if (!isCoSigner) {
50
+ throw new Error(`Loaded keypair ${keypair.publicKey.toBase58()} is not a valid signer for this multisig account.`);
51
+ }
52
+ }
53
+ else {
54
+ throw new Error(`Loaded keypair ${keypair.publicKey.toBase58()} does not match authority ${authority.toBase58()}`);
55
+ }
56
+ }
57
+ // 4. Resolve developer/platform fee wallet
58
+ const feeWalletStr = options.feeWallet || config.feeWallet;
59
+ const feePercentage = options.feePercent !== undefined
60
+ ? parseInt(options.feePercent, 10)
61
+ : config.feePercentage;
62
+ const feeWallet = feeWalletStr ? new web3_js_1.PublicKey(feeWalletStr) : undefined;
63
+ // 5. Fetch blockhash
64
+ console.log('Fetching latest blockhash...');
65
+ const blockhash = await rpc.getLatestBlockhash();
66
+ // 6. Build instructions
67
+ console.log('Assembling transaction instructions...');
68
+ const instructions = (0, transaction_1.buildRecoveryInstructions)({
69
+ source,
70
+ destination,
71
+ authority,
72
+ excessAmount: excess,
73
+ developerFeeWallet: feeWallet,
74
+ feePercentage,
75
+ multiSigners: account.multisigSigners,
76
+ });
77
+ // 7. Compile and sign
78
+ const tx = new web3_js_1.Transaction();
79
+ tx.add(...instructions);
80
+ tx.recentBlockhash = blockhash;
81
+ tx.feePayer = keypair.publicKey;
82
+ console.log(`Signing transaction with authority ${keypair.publicKey.toBase58()}...`);
83
+ tx.sign(keypair);
84
+ const rawTxBuf = tx.serialize();
85
+ // 8. Simulate on-chain
86
+ console.log('Simulating transaction...');
87
+ const simResult = await rpc.simulateTransaction(rawTxBuf);
88
+ if (simResult.value.err) {
89
+ console.error(chalk_1.default.red('✘ Simulation failed!'));
90
+ console.error(chalk_1.default.red(`Error details: ${JSON.stringify(simResult.value.err)}`));
91
+ if (simResult.value.logs) {
92
+ simResult.value.logs.forEach((log) => console.log(chalk_1.default.gray(` ${log}`)));
93
+ }
94
+ process.exit(5);
95
+ }
96
+ console.log(chalk_1.default.green('✔ Simulation succeeded.'));
97
+ // 9. Broadcast
98
+ console.log('Broadcasting transaction to network...');
99
+ const signature = await rpc.submitTransaction(rawTxBuf);
100
+ console.log(chalk_1.default.bold.green('\n✔ Recovery completed successfully!'));
101
+ console.log(`Signature: ${signature}`);
102
+ console.log(chalk_1.default.cyan(`Explorer Link: https://explorer.solana.com/tx/${signature}?cluster=custom&customUrl=${encodeURIComponent(config.rpcUrl)}`));
103
+ process.exit(0);
104
+ }
105
+ catch (err) {
106
+ console.error(chalk_1.default.red(`\n✘ Recovery failed: ${err.message}`));
107
+ process.exit(1);
108
+ }
109
+ }
@@ -0,0 +1,4 @@
1
+ export declare function runScan(addressListFile: string, options: {
2
+ json?: boolean;
3
+ config?: string;
4
+ }): Promise<void>;
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runScan = runScan;
7
+ const rpc_1 = require("../io/rpc");
8
+ const rent_1 = require("../core/rent");
9
+ const file_1 = require("../io/file");
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const cli_table3_1 = __importDefault(require("cli-table3"));
12
+ async function runScan(addressListFile, options) {
13
+ try {
14
+ const config = (0, file_1.loadConfig)(options.config);
15
+ const rpc = new rpc_1.RpcAdapter(config.rpcUrl, config.commitment);
16
+ const addresses = (0, file_1.loadAddresses)(addressListFile);
17
+ if (addresses.length === 0) {
18
+ console.error(chalk_1.default.yellow('⚠ No addresses found in input file.'));
19
+ process.exit(3);
20
+ }
21
+ if (!options.json) {
22
+ console.log(chalk_1.default.bold(`Scanning ${addresses.length} target accounts...`));
23
+ }
24
+ const results = [];
25
+ let totalRecoverable = 0n;
26
+ for (const address of addresses) {
27
+ try {
28
+ const account = await rpc.inspectAccount(address);
29
+ const { excess, rentFloor } = (0, rent_1.calculateExcessLamports)(account.dataSize, account.balance);
30
+ if (excess > 0n && account.isToken2022) {
31
+ totalRecoverable += excess;
32
+ }
33
+ results.push({
34
+ address: address.toBase58(),
35
+ type: account.type,
36
+ ownerProgram: account.ownerProgram.toBase58(),
37
+ isToken2022: account.isToken2022,
38
+ balance: account.balance,
39
+ rentFloor,
40
+ excess,
41
+ authority: account.authority ? account.authority.toBase58() : null,
42
+ status: !account.isToken2022
43
+ ? 'UNSUPPORTED_LEGACY'
44
+ : excess > 0n
45
+ ? 'RECOVERABLE'
46
+ : 'RENT_EXEMPT_MIN',
47
+ });
48
+ }
49
+ catch (err) {
50
+ results.push({
51
+ address: address.toBase58(),
52
+ status: 'ERROR',
53
+ error: err.message,
54
+ });
55
+ }
56
+ }
57
+ if (options.json) {
58
+ console.log(JSON.stringify({
59
+ scannedAt: new Date().toISOString(),
60
+ summary: {
61
+ totalAccounts: addresses.length,
62
+ totalRecoverableLamports: totalRecoverable.toString(),
63
+ totalRecoverableSol: Number(totalRecoverable) / 1e9,
64
+ },
65
+ accounts: results.map(res => ({
66
+ ...res,
67
+ balance: res.balance?.toString(),
68
+ rentFloor: res.rentFloor?.toString(),
69
+ excess: res.excess?.toString(),
70
+ })),
71
+ }, null, 2));
72
+ process.exit(totalRecoverable > 0n ? 0 : 3);
73
+ }
74
+ // Print table output
75
+ const table = new cli_table3_1.default({
76
+ head: ['Address', 'Type', 'Program', 'Balance (SOL)', 'Rent Floor (SOL)', 'Excess (SOL)', 'Status'],
77
+ colWidths: [46, 12, 12, 16, 18, 16, 20]
78
+ });
79
+ for (const res of results) {
80
+ if (res.status === 'ERROR') {
81
+ table.push([
82
+ res.address,
83
+ '-',
84
+ '-',
85
+ '-',
86
+ '-',
87
+ '-',
88
+ chalk_1.default.red(`ERROR: ${res.error}`),
89
+ ]);
90
+ }
91
+ else {
92
+ const solBal = Number(res.balance ?? 0n) / 1e9;
93
+ const solRent = Number(res.rentFloor ?? 0n) / 1e9;
94
+ const solExcess = Number(res.excess ?? 0n) / 1e9;
95
+ let statusText = res.status;
96
+ if (res.status === 'RECOVERABLE') {
97
+ statusText = chalk_1.default.green('RECOVERABLE');
98
+ }
99
+ else if (res.status === 'UNSUPPORTED_LEGACY') {
100
+ statusText = chalk_1.default.yellow('LEGACY PROGRAM');
101
+ }
102
+ else {
103
+ statusText = chalk_1.default.gray('RENT EXEMPT');
104
+ }
105
+ table.push([
106
+ res.address,
107
+ res.type?.toUpperCase() ?? '-',
108
+ res.isToken2022 ? 'Token-2022' : 'Legacy',
109
+ solBal.toFixed(5),
110
+ solRent.toFixed(5),
111
+ (res.excess ?? 0n) > 0n && res.isToken2022 ? chalk_1.default.green(solExcess.toFixed(5)) : '0.00000',
112
+ statusText,
113
+ ]);
114
+ }
115
+ }
116
+ console.log(table.toString());
117
+ console.log('\n' + chalk_1.default.bold('Summary Report:'));
118
+ console.log(` Total Accounts Scanned: ${addresses.length}`);
119
+ console.log(` Total Recoverable SOL: ${chalk_1.default.green((Number(totalRecoverable) / 1e9).toFixed(9) + ' SOL')} (${totalRecoverable.toLocaleString()} lamports)`);
120
+ process.exit(totalRecoverable > 0n ? 0 : 3);
121
+ }
122
+ catch (err) {
123
+ console.error(chalk_1.default.red(`\n✘ Scan failed: ${err.message}`));
124
+ process.exit(1);
125
+ }
126
+ }