Skip to content

Instantly share code, notes, and snippets.

@nazreen
Last active March 6, 2025 00:40
Show Gist options
  • Save nazreen/2190eaaf34710bfd98d9b2ba33989f5a to your computer and use it in GitHub Desktop.
Save nazreen/2190eaaf34710bfd98d9b2ba33989f5a to your computer and use it in GitHub Desktop.
find deployed programs
import { Connection, PublicKey } from '@solana/web3.js';
import { promises as fs } from 'fs';
import path from 'path';
import bs58 from 'bs58';
// --- Constants & Types ---
const RPC_URL = 'https://api.devnet.solana.com';
const CACHE_DIR = path.join(__dirname, 'cache');
const INSPECTED_FILE = path.join(CACHE_DIR, 'instructionsInspected.json');
const LAST_CHECKED_FILE = path.join(CACHE_DIR, 'lastChecked.json');
const DEPLOYED_FILE = path.join(CACHE_DIR, 'deployedPrograms.json');
const SKIP_FILE = path.join(CACHE_DIR, 'skipRemaining.json');
const BPF_LOADER_NON_UPGRADEABLE = 'BPFLoader1111111111111111111111111111111111';
const BPF_LOADER_UPGRADEABLE = 'BPFLoaderUpgradeab1e11111111111111111111111';
function getInstructionType(base58Str: string) {
const decoded = bs58.decode(base58Str);
const dataView = new DataView(decoded.buffer, decoded.byteOffset, decoded.byteLength);
return dataView.getUint8(0);
}
function getUint32AtOffset(base58Str: string, offset: number = 4): number {
const decoded = bs58.decode(base58Str);
if (decoded.length < offset + 4) {
throw new Error("Decoded data is too short to read a 32-bit value at the given offset.");
}
const dataView = new DataView(decoded.buffer, decoded.byteOffset, decoded.byteLength);
return dataView.getUint32(offset, true); // little-endian
}
function getOffset(base58Str: string): number {
return getUint32AtOffset(base58Str, 4);
}
interface InspectedTransaction {
hasDeploymentInstruction?: boolean;
deployedProgramId?: string | null;
datetime: string;
skipped?: boolean;
}
// --- Utility Functions ---
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function loadJson(filePath: string): Promise<any> {
try {
const data = await fs.readFile(filePath, 'utf-8');
return JSON.parse(data);
} catch (err) {
return {};
}
}
async function writeJson(filePath: string, data: any): Promise<void> {
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
}
async function ensureCacheDir(): Promise<void> {
try {
await fs.mkdir(CACHE_DIR, { recursive: true });
} catch (err) {
console.error('Error creating cache directory', err);
}
}
// --- Helper Functions ---
/**
* Retrieves the instruction's program ID from either the `programId` property
* or via the `programIdIndex` using the provided account keys.
*/
function getInstructionProgramId(instruction: any, accountKeys: string[]): string | null {
if (instruction.programId) {
return instruction.programId.toString();
} else if (instruction.programIdIndex !== undefined) {
return accountKeys[instruction.programIdIndex];
}
return null;
}
/**
* Extracts deployment info from a transaction.
*
* - For non-upgradeable loader instructions, we assume deployment if present,
* with the deployed program ID at account index 0.
* - For upgradeable loader instructions, we decode the instruction data using bs58.
* Only if the first byte equals 2 (indicating a deployment instruction, e.g. DeployWithMaxDataLen)
* do we mark it as a deployment. In that case the deployed program ID is assumed to be at account index 1.
*/
function extractDeploymentInfo(tx: any): { hasDeployment: boolean; deployedProgramId: string | null } {
let hasDeployment = false;
let deployedProgramId: string | null = null;
if (!tx || !tx.transaction || !tx.transaction.message || !tx.transaction.message.accountKeys) {
return { hasDeployment, deployedProgramId };
}
const message = tx.transaction.message;
const accountKeys = message.accountKeys.map((key: any) => key.toString());
for (const instruction of message.instructions) {
const instrProgramId = getInstructionProgramId(instruction, accountKeys);
if (!instrProgramId) continue;
if (instrProgramId === BPF_LOADER_NON_UPGRADEABLE) {
// Non-upgradeable: assume deployment if the instruction is present.
const deployedIndex = 0;
if (instruction.accounts && instruction.accounts.length > deployedIndex) {
deployedProgramId = accountKeys[deployedIndex];
hasDeployment = true;
break;
}
} else if (instrProgramId === BPF_LOADER_UPGRADEABLE) {
// Upgradeable: check instruction data discriminator.
if (!instruction.data) continue;
let dataBuffer: Buffer;
try {
dataBuffer = bs58.decode(instruction.data);
} catch (e) {
console.error('Error decoding instruction data', e);
continue;
}
// Only treat it as a deployment if the first byte equals 2 (DeployWithMaxDataLen).
if (dataBuffer[0] !== 2) continue;
const deployedIndex = 1;
if (instruction.accounts && instruction.accounts.length > deployedIndex) {
deployedProgramId = accountKeys[deployedIndex];
hasDeployment = true;
break;
}
}
}
return { hasDeployment, deployedProgramId };
}
/**
* Processes a single transaction by fetching it, checking for deployment instructions,
* updating the caches, and checking for a buffer write instruction.
*
* Returns a skip count based on a buffer write instruction: floor(offset / 960).
*/
async function processTransaction(
connection: Connection,
signature: string,
instructionsInspected: { [sig: string]: InspectedTransaction },
deployedPrograms: { [programId: string]: string }
): Promise<number> {
console.log(`Processing signature: ${signature}`);
const tx = await connection.getTransaction(signature, {
commitment: 'confirmed',
maxSupportedTransactionVersion: 0
});
if (!tx) {
throw new Error(`Transaction not found: ${signature}`);
}
const { hasDeployment, deployedProgramId } = extractDeploymentInfo(tx);
const processingTime = new Date().toISOString();
instructionsInspected[signature] = {
hasDeploymentInstruction: hasDeployment,
deployedProgramId,
datetime: processingTime
};
let txnTime = processingTime;
if (tx && tx.blockTime) {
txnTime = new Date(tx.blockTime * 1000).toISOString();
}
if (hasDeployment && deployedProgramId) {
deployedPrograms[deployedProgramId] = txnTime;
}
// Check for a buffer write instruction.
let bufferSkip = 0;
const nOfInstructions = tx.transaction?.message?.compiledInstructions?.length || 1;
// @ts-expect-error ok
const instructions = tx.transaction.message.instructions || tx.transaction.message.compiledInstructions;
if (instructions === undefined) {
console.log(tx.transaction.message);
throw new Error('Instructions not found in transaction');
}
const lastIxn = instructions[nOfInstructions - 1];
let ixnData: string = instructions[nOfInstructions - 1].data;
if (typeof ixnData !== 'string') {
ixnData = bs58.encode(ixnData);
}
const instructionType = getInstructionType(ixnData);
const lastIxnProgramId = tx?.transaction.message.staticAccountKeys[lastIxn?.programIdIndex!].toString();
if (lastIxnProgramId === BPF_LOADER_UPGRADEABLE && instructionType === 1) {
const offset = getOffset(ixnData);
console.log('BPF_LOADER_UPGRADEABLE write instruction found.');
console.log(`Offset: ${offset}`);
const evaluatedBufferSkip = Math.floor(offset / 960);
bufferSkip = evaluatedBufferSkip;
}
return bufferSkip;
}
// --- Main Execution ---
async function main() {
await ensureCacheDir();
const instructionsInspected = await loadJson(INSPECTED_FILE);
const deployedPrograms = await loadJson(DEPLOYED_FILE);
const lastChecked = await loadJson(LAST_CHECKED_FILE);
let skipRemaining: number = await loadJson(SKIP_FILE);
if (typeof skipRemaining !== "number") {
skipRemaining = 0;
}
const connection = new Connection(RPC_URL, 'confirmed');
const solanaAddress = process.argv[2];
if (!solanaAddress) {
console.error('Usage: npx tsx index.ts <solana_address>');
process.exit(1);
}
const publicKey = new PublicKey(solanaAddress);
const options: any = { limit: 1 };
if (lastChecked.signature) {
// options.before = lastChecked.signature;
options.before = "5pmd3ogukUKRaRixzktvf1x1pjFMeZbfohYc72VEo3oWLrWmFRapddLfihRd74enc6uCbWCvZhFXBwFxJmAz5nsP"
}
// Color codes for alternating log messages.
const colors = ["\x1b[34m", "\x1b[32m", "\x1b[33m"]; // blue, green, yellow
let finishedLogCount = 0;
// Loop until no more signatures (i.e. reaching the start).
while (true) {
const signatures = await connection.getSignaturesForAddress(publicKey, options);
if (signatures.length === 0) {
console.log("No more signatures found. Reached the start of history.");
break;
}
for (let i = 0; i < signatures.length; i++) {
const signature = signatures[i].signature;
if (instructionsInspected[signature]) continue;
if (skipRemaining > 0) {
console.log(`Skipping https://solscan.io/tx/${signature}?cluster=devnet due to recent buffer write, skipRemaining: ${skipRemaining}`);
skipRemaining--;
await writeJson(SKIP_FILE, skipRemaining);
const now = new Date().toISOString();
instructionsInspected[signature] = { skipped: true, datetime: now };
await writeJson(LAST_CHECKED_FILE, { signature, datetime: now });
await writeJson(INSPECTED_FILE, instructionsInspected);
continue;
}
const bufferSkip = await processTransaction(connection, signature, instructionsInspected, deployedPrograms);
if (instructionsInspected[signature].hasDeploymentInstruction && instructionsInspected[signature].deployedProgramId) {
await writeJson(SKIP_FILE, skipRemaining);
console.log(`
====================================================
DEPLOYMENT FOUND!
Signature: ${signature}
Deployed Program ID: ${instructionsInspected[signature].deployedProgramId}
====================================================
`);
} else if (bufferSkip > 0) {
const MAX_SKIP = 50;
skipRemaining = Math.min(bufferSkip, MAX_SKIP);
await writeJson(SKIP_FILE, skipRemaining);
console.log(`
====================================================
BUFFER WRITE DETECTED!
Signature: ${signature}
Offset-based skip: ${bufferSkip} transactions, capped at ${MAX_SKIP}
====================================================
`);
}
const now = new Date().toISOString();
await writeJson(LAST_CHECKED_FILE, { signature, datetime: now });
await writeJson(INSPECTED_FILE, instructionsInspected);
await writeJson(DEPLOYED_FILE, deployedPrograms);
const color = colors[finishedLogCount % colors.length];
console.log(`${color}Finished processing https://solscan.io/tx/${signature}?cluster=devnet\x1b[0m`);
finishedLogCount++;
console.log(`Total instructions inspected: ${Object.keys(instructionsInspected).length}`);
// Throttle to 10 requests per minute.
await sleep(6000);
}
// Set the next query's 'before' to the oldest signature from this batch.
options.before = signatures[signatures.length - 1].signature;
}
console.log('Processing complete.');
}
main().catch(err => console.error(err));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment