Last active
March 6, 2025 00:40
-
-
Save nazreen/2190eaaf34710bfd98d9b2ba33989f5a to your computer and use it in GitHub Desktop.
find deployed programs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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