Skip to content

Instantly share code, notes, and snippets.

@fengtality
Created December 4, 2025 04:36
Show Gist options
  • Select an option

  • Save fengtality/fbfa28597be122ed74d4f25046b3e5a5 to your computer and use it in GitHub Desktop.

Select an option

Save fengtality/fbfa28597be122ed74d4f25046b3e5a5 to your computer and use it in GitHub Desktop.
HumidiFi Swap Parser - Decodes obfuscated swap data from HumidiFi transactions on Solana
#!/usr/bin/env node
/**
* HumidiFi Swap Parser
* Parses and decodes obfuscated swap data from HumidiFi transactions on Solana.
*
* HumidiFi obfuscates swap parameters using XOR encryption. This script decrypts
* the data and extracts: pool address, tokens involved, and swap amounts.
*
* Usage:
* node humidifi-swap-parser.js <transaction_signature>
* node humidifi-swap-parser.js --hex <hex_data>
*
* Example:
* $ node humidifi-swap-parser.js 486Y3wm6d4MwGjNW31qQnPfHe971KkSVwvHxpnv32dq7BdboH6i8sZJtzF43phC5BAN49dtnDhRJohAN2pUGFtGw
*
* Found 2 outer + 1 inner HumidiFi instruction(s)
*
* === HumidiFi: Swap ===
* Location: Inner Instruction #3.1
* Pool: 3QYYvFWgSuGK8bbxMSAYkCqE8QfSuFtByagnZAuekia2
* Token In: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
* Token Out: So11111111111111111111111111111111111111112
* Amount In: 500
* Amount Out: 3.462293756
*/
// HumidiFi has two programs:
// - Route program: handles swap routing setup (75-byte instructions)
// - Swap program: contains the actual obfuscated swap parameters (25-byte instructions)
const HUMIDIFI_ROUTE_PROGRAM = 'AubCGUt9FfbuzrpFWcFgS4U6FTYRAfuzxrdtttN5TGP';
const HUMIDIFI_SWAP_PROGRAM = '9H6tua7jkLhdm3w8BvgpTn5LZNU7g4ZynDmCiNN3q6Rp';
const HUMIDIFI_PROGRAMS = [HUMIDIFI_ROUTE_PROGRAM, HUMIDIFI_SWAP_PROGRAM];
const HUMIDIFI_KEY = [58, 255, 47, 255, 226, 186, 235, 195];
const RPC_URL = 'https://api.mainnet-beta.solana.com';
function deobfuscate(data) {
const result = Buffer.from(data);
let pos = 0;
for (let i = 0; i < result.length; i += 8) {
const chunkSize = Math.min(8, result.length - i);
// Position mask: pos * 0x0001_0001_0001_0001
const posMask = Buffer.alloc(8);
for (let j = 0; j < 8; j += 2) {
posMask.writeUInt16LE(pos, j);
}
// XOR with key and position mask
for (let j = 0; j < chunkSize; j++) {
result[i + j] ^= HUMIDIFI_KEY[j] ^ posMask[j];
}
pos++;
}
return result;
}
function parseSwapData(data) {
// For 25-byte swap data: decode all bytes, read amountIn at offset 8
const deobfuscated = deobfuscate(data);
return {
header: deobfuscated.slice(0, 8).toString('hex'),
amountIn: deobfuscated.readBigUInt64LE(8),
isBaseToQuote: deobfuscated[16] === 1,
extraBytes: data.length > 17 ? deobfuscated.slice(17).toString('hex') : null,
};
}
async function fetchTransaction(sig) {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getTransaction',
params: [sig, { encoding: 'json', maxSupportedTransactionVersion: 0 }],
}),
});
const json = await res.json();
if (!json.result) throw new Error('Transaction not found');
return json.result;
}
function bs58Decode(str) {
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const bytes = [];
for (let i = 0; i < str.length; i++) {
const idx = ALPHABET.indexOf(str[i]);
if (idx < 0) throw new Error('Invalid base58 character');
let carry = idx;
for (let j = 0; j < bytes.length; j++) {
carry += bytes[j] * 58;
bytes[j] = carry & 0xff;
carry >>= 8;
}
while (carry > 0) {
bytes.push(carry & 0xff);
carry >>= 8;
}
}
for (let i = 0; i < str.length && str[i] === '1'; i++) bytes.push(0);
return Buffer.from(bytes.reverse());
}
function formatAmount(raw, decimals) {
if (decimals === undefined) {
return `${raw} (decimals unknown)`;
}
const divisor = Math.pow(10, decimals);
return `${Number(raw) / divisor}`;
}
function tryDecodeSwap(rawData, location, tokenInfo = null) {
// Try to decode as 25-byte swap instruction (don't skip any bytes)
if (rawData.length === 25) {
try {
const parsed = parseSwapData(rawData);
// Check for reasonable amounts (1 to 1 trillion raw units)
if (parsed.amountIn > 0n && parsed.amountIn < 1000000000000000n) {
console.log(`=== HumidiFi: Swap ===`);
console.log(`Location: ${location}`);
if (tokenInfo?.poolAddress) {
console.log(`Pool: ${tokenInfo.poolAddress}`);
}
if (tokenInfo) {
console.log(`Token In: ${tokenInfo.tokenIn}`);
console.log(`Token Out: ${tokenInfo.tokenOut}`);
}
console.log(`Amount In: ${formatAmount(parsed.amountIn, tokenInfo?.tokenInDecimals)}`);
if (tokenInfo?.amountOut) {
console.log(`Amount Out: ${formatAmount(tokenInfo.amountOut, tokenInfo.tokenOutDecimals)}`);
}
console.log();
return true;
}
} catch (e) {
// Ignore decode errors
}
}
// For other sizes, try to find embedded swap data
// Look for 17-byte or 25-byte chunks that decode to reasonable values
for (let offset = 0; offset <= rawData.length - 17; offset++) {
const chunk = rawData.slice(offset, offset + 17);
try {
const decoded = deobfuscate(chunk);
const amountIn = decoded.readBigUInt64LE(8);
const direction = decoded[16];
if (amountIn > 0n && amountIn < 1000000000000000n && direction <= 1) {
console.log(`=== HumidiFi: swap ===`);
console.log(`Location: ${location} (at offset ${offset})`);
if (tokenInfo?.poolAddress) {
console.log(`Pool: ${tokenInfo.poolAddress}`);
}
if (tokenInfo) {
console.log(`Token In: ${tokenInfo.tokenIn}`);
console.log(`Token Out: ${tokenInfo.tokenOut}`);
}
console.log(`Amount In: ${formatAmount(amountIn, tokenInfo?.tokenInDecimals)}`);
if (tokenInfo?.amountOut) {
console.log(`Amount Out: ${formatAmount(tokenInfo.amountOut, tokenInfo.tokenOutDecimals)}`);
}
console.log();
return true;
}
} catch (e) {
// Ignore decode errors
}
}
return false;
}
// Extract token info and amounts from HumidiFi swap instruction accounts
// Based on Solscan account layout:
// #0 - User Transfer Authority
// #1 - Pool address
// #2 - Base Token Account (Pool's base token account)
// #3 - Quote Token Account (Pool's quote token account)
// #4 - User Base Token Account
// #5 - User Quote Token Account
function extractTokenInfo(ix, accountKeys, tx, isBaseToQuote) {
try {
const accounts = ix.accounts || [];
if (accounts.length < 6) return null;
const preTokenBalances = tx.meta?.preTokenBalances || [];
const postTokenBalances = tx.meta?.postTokenBalances || [];
// Build maps of accountIndex -> mint, balance change, and decimals
const accountToMint = new Map();
const mintToDecimals = new Map();
const preBalances = new Map();
const postBalances = new Map();
for (const bal of preTokenBalances) {
accountToMint.set(bal.accountIndex, bal.mint);
if (bal.uiTokenAmount?.decimals !== undefined) {
mintToDecimals.set(bal.mint, bal.uiTokenAmount.decimals);
}
preBalances.set(bal.accountIndex, BigInt(bal.uiTokenAmount?.amount || '0'));
}
for (const bal of postTokenBalances) {
if (bal.uiTokenAmount?.decimals !== undefined) {
mintToDecimals.set(bal.mint, bal.uiTokenAmount.decimals);
}
postBalances.set(bal.accountIndex, BigInt(bal.uiTokenAmount?.amount || '0'));
}
// Account #2 is Pool Base Token Account, Account #3 is Pool Quote Token Account
const baseAccountIndex = accounts[2];
const quoteAccountIndex = accounts[3];
const baseToken = accountToMint.get(baseAccountIndex);
const quoteToken = accountToMint.get(quoteAccountIndex);
if (baseToken && quoteToken) {
// Calculate amount out from pool balance changes
// Pool receives tokenIn and sends tokenOut
const baseChange = (postBalances.get(baseAccountIndex) || 0n) - (preBalances.get(baseAccountIndex) || 0n);
const quoteChange = (postBalances.get(quoteAccountIndex) || 0n) - (preBalances.get(quoteAccountIndex) || 0n);
// Determine token in/out based on direction
// HumidiFi direction: isBaseToQuote=true means user sells quote for base (buys base)
// Pool receives quote (positive change), sends base (negative change)
let tokenIn, tokenOut, amountOut, tokenInDecimals, tokenOutDecimals;
if (isBaseToQuote) {
tokenIn = quoteToken;
tokenOut = baseToken;
tokenInDecimals = mintToDecimals.get(quoteToken);
tokenOutDecimals = mintToDecimals.get(baseToken);
amountOut = baseChange < 0n ? -baseChange : 0n;
} else {
tokenIn = baseToken;
tokenOut = quoteToken;
tokenInDecimals = mintToDecimals.get(baseToken);
tokenOutDecimals = mintToDecimals.get(quoteToken);
amountOut = quoteChange < 0n ? -quoteChange : 0n;
}
// Pool address is account #1
const poolAddress = accountKeys[accounts[1]];
return { tokenIn, tokenOut, amountOut, tokenInDecimals, tokenOutDecimals, poolAddress };
}
return null;
} catch (e) {
return null;
}
}
async function main() {
const sig = process.argv[2];
if (!sig) {
console.error('Usage: node humidifi-swap-parser.js <transaction_signature>');
console.error(' node humidifi-swap-parser.js --hex <hex_data>');
process.exit(1);
}
// Handle direct hex input
if (sig === '--hex' && process.argv[3]) {
const hexData = process.argv[3];
const rawData = Buffer.from(hexData, 'hex');
console.log(`Decoding ${rawData.length} bytes of hex data\n`);
if (!tryDecodeSwap(rawData, 'Direct hex input')) {
console.log('Could not decode as swap data');
console.log('Raw hex:', hexData);
}
return;
}
const tx = await fetchTransaction(sig);
const msg = tx.transaction.message;
// Resolve account keys (including lookup tables)
const accountKeys = [...msg.accountKeys];
if (msg.addressTableLookups && tx.meta?.loadedAddresses) {
accountKeys.push(...tx.meta.loadedAddresses.writable);
accountKeys.push(...tx.meta.loadedAddresses.readonly);
}
// Find HumidiFi instructions in outer instructions
const humidifiIxs = msg.instructions.filter(ix =>
HUMIDIFI_PROGRAMS.includes(accountKeys[ix.programIdIndex])
);
// Find HumidiFi instructions in inner instructions
const innerHumidifiIxs = [];
if (tx.meta?.innerInstructions) {
for (const inner of tx.meta.innerInstructions) {
for (let i = 0; i < inner.instructions.length; i++) {
const ix = inner.instructions[i];
if (HUMIDIFI_PROGRAMS.includes(accountKeys[ix.programIdIndex])) {
innerHumidifiIxs.push({
ix,
outerIndex: inner.index,
innerIndex: i
});
}
}
}
}
const totalCount = humidifiIxs.length + innerHumidifiIxs.length;
if (totalCount === 0) {
console.error('No HumidiFi instructions found in this transaction');
process.exit(1);
}
console.log(`Found ${humidifiIxs.length} outer + ${innerHumidifiIxs.length} inner HumidiFi instruction(s)\n`);
let foundSwap = false;
// Process outer instructions
for (let i = 0; i < humidifiIxs.length; i++) {
const ix = humidifiIxs[i];
const rawData = bs58Decode(ix.data);
// Parse to get direction first
if (rawData.length === 25) {
try {
const parsed = parseSwapData(rawData);
const ixTokenInfo = extractTokenInfo(ix, accountKeys, tx, parsed.isBaseToQuote);
if (tryDecodeSwap(rawData, `Instruction #${i + 1}`, ixTokenInfo)) {
foundSwap = true;
}
} catch (e) {
// Ignore
}
}
}
// Process inner instructions
for (const { ix, outerIndex, innerIndex } of innerHumidifiIxs) {
const rawData = bs58Decode(ix.data);
// Parse to get direction first
if (rawData.length === 25) {
try {
const parsed = parseSwapData(rawData);
const ixTokenInfo = extractTokenInfo(ix, accountKeys, tx, parsed.isBaseToQuote);
if (tryDecodeSwap(rawData, `Inner Instruction #${outerIndex + 1}.${innerIndex + 1}`, ixTokenInfo)) {
foundSwap = true;
}
} catch (e) {
// Ignore
}
}
}
if (!foundSwap) {
console.log('No decodable swap data found in HumidiFi instructions.');
console.log('The instructions may be route setup or other non-swap types.');
console.log('\nRaw instruction data:');
for (let i = 0; i < humidifiIxs.length; i++) {
const rawData = bs58Decode(humidifiIxs[i].data);
console.log(` Outer #${i + 1}: ${rawData.length} bytes - ${rawData.slice(0, 20).toString('hex')}...`);
}
for (const { ix, outerIndex, innerIndex } of innerHumidifiIxs) {
const rawData = bs58Decode(ix.data);
console.log(` Inner (${outerIndex}:${innerIndex}): ${rawData.length} bytes - ${rawData.slice(0, 20).toString('hex')}...`);
}
}
}
main().catch(e => {
console.error('Error:', e.message);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment