Created
December 4, 2025 04:36
-
-
Save fengtality/fbfa28597be122ed74d4f25046b3e5a5 to your computer and use it in GitHub Desktop.
HumidiFi Swap Parser - Decodes obfuscated swap data from HumidiFi transactions on Solana
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
| #!/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