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.
- package/dist/bin/smre.d.ts +2 -0
- package/dist/bin/smre.js +106 -0
- package/dist/src/commands/build.d.ts +8 -0
- package/dist/src/commands/build.js +139 -0
- package/dist/src/commands/config.d.ts +1 -0
- package/dist/src/commands/config.js +21 -0
- package/dist/src/commands/doctor.d.ts +1 -0
- package/dist/src/commands/doctor.js +44 -0
- package/dist/src/commands/inspect.d.ts +4 -0
- package/dist/src/commands/inspect.js +76 -0
- package/dist/src/commands/recover.d.ts +7 -0
- package/dist/src/commands/recover.js +109 -0
- package/dist/src/commands/scan.d.ts +4 -0
- package/dist/src/commands/scan.js +126 -0
- package/dist/src/commands/sign.d.ts +4 -0
- package/dist/src/commands/sign.js +55 -0
- package/dist/src/commands/simulate.d.ts +3 -0
- package/dist/src/commands/simulate.js +50 -0
- package/dist/src/commands/submit.d.ts +3 -0
- package/dist/src/commands/submit.js +57 -0
- package/dist/src/core/rent.d.ts +19 -0
- package/dist/src/core/rent.js +26 -0
- package/dist/src/core/transaction.d.ts +22 -0
- package/dist/src/core/transaction.js +54 -0
- package/dist/src/core/validation.d.ts +124 -0
- package/dist/src/core/validation.js +35 -0
- package/dist/src/io/file.d.ts +23 -0
- package/dist/src/io/file.js +140 -0
- package/dist/src/io/rpc.d.ts +36 -0
- package/dist/src/io/rpc.js +155 -0
- package/dist/src/signers/signer.d.ts +22 -0
- package/dist/src/signers/signer.js +114 -0
- package/package.json +40 -0
package/dist/bin/smre.js
ADDED
|
@@ -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,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,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,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,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
|
+
}
|