Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 43 additions & 19 deletions src/services/api/helius.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import type { Address } from '@solana/addresses';
import type { Signature } from '@solana/keys';
import { createHelius } from "helius-sdk";
import { RateLimiter } from "../../utils/rateLimiter.js";
import { RateLimiter } from "../../utils/rateLimiter";

export interface HeliusTransaction {
signature: string;
slot: number;
blockTime: number;
error: string;
memo: string;
confirmationStatus: "finalized" | "confirmed" | "processed";
}

export interface HeliusService {
getAccountInfo(address: string, config?: any): Promise<any>;
getBlock(address: string): Promise<any>;
getBlock(slot: bigint): Promise<any>;
getSignaturesForAddress(address: string, config: any): Promise<readonly any[]>;
getAssetsByOwner(config: { ownerAddress: string }): Promise<any>;
getTokenAccounts(config: { owner: string }): Promise<any>;
getBalance(address: string): Promise<any>;
getTransaction(signature: string, config?: any): Promise<any>;
getTransactionsForAddress(address: string, limit: number, before?: string): Promise<{
transactions: any[];
transactions: HeliusTransaction[];
signatures: string[];
paginationToken?: string;
hasMore: boolean;
Expand Down Expand Up @@ -42,19 +53,19 @@ export function createHeliusService(apiKey: string, requestsPerSecond: number =
return {
getAccountInfo: async (address: string, config?: any) => {
return withRateLimit(async () => {
return helius.getAccountInfo(address, config || { encoding: "base64" });
return helius.getAccountInfo(address as Address, config || { encoding: "base64" });
});
},

getBlock: async (address: string) => {
getBlock: async (slot: bigint) => {
return withRateLimit(async () => {
return helius.getBlock(address);
return helius.getBlock(slot);
});
},

getSignaturesForAddress: async (address: string, config: any) => {
return withRateLimit(async () => {
return helius.getSignaturesForAddress(address, config);
return helius.getSignaturesForAddress(address as Address, config);
});
},

Expand All @@ -70,26 +81,36 @@ export function createHeliusService(apiKey: string, requestsPerSecond: number =
});
},

getBalance: async (address: string) => {
getBalance: async (address: string): Promise<any> => {
return withRateLimit(async () => {
return helius.getBalance(address);
return helius.getBalance(address as Address);
});
},

getTransaction: async (signature: string, config?: any) => {
getTransaction: async (signature: Signature, config?: any) => {
return withRateLimit(async () => {
return helius.getTransaction(signature, config || { maxSupportedTransactionVersion: 0 });
});
},

getTransactionsForAddress: async (address: string, limit: number, before?: string) => {
/**
* Fetches transactions for a given address using Helius's optimized RPC method
* This method combines getSignaturesForAddress + getTransaction in a single call,
* significantly reducing API calls and improving performance
*
* @param address - The Solana address to fetch transactions for
* @param limit - Maximum number of transactions to return (max 100 with full details)
* @param before - Optional pagination token from previous request
* @returns Object containing transactions, signatures, pagination token, and hasMore flag
*/
getTransactionsForAddress: async (address: string, limit: number, before?: string) :Promise<{ transactions: HeliusTransaction[]; signatures: string[]; paginationToken?: string; hasMore: boolean }> => {
// Use new Helius getTransactionsForAddress RPC method
// This combines getSignaturesForAddress + getTransaction in 1 call!
// Limit: max 100 transactions with full details

return withRateLimit(async () => {
const params: any = {
transactionDetails: 'full', // Get full transaction data
transactionDetails: 'signatures', // Get full transaction data
limit: Math.min(limit, 100), // Max 100 with full details
};

Expand Down Expand Up @@ -122,24 +143,27 @@ export function createHeliusService(apiKey: string, requestsPerSecond: number =
}

// Filter out failed transactions
const validTransactions = result.data.filter((tx: any) =>
const validTransactions: HeliusTransaction[] = result.data.filter((tx: any) =>
tx.meta?.err === null || tx.meta?.err === undefined
);

return {
transactions: validTransactions,
signatures: validTransactions.map((tx: any) => tx.transaction.signatures[0]),
signatures: validTransactions.map((tx: HeliusTransaction) => Array.isArray(tx.signature) ? tx.signature[0] : tx.signature),
paginationToken: result.paginationToken || undefined,
hasMore: !!result.paginationToken
};
});
},

/**
* Fetches token metadata (symbol and name) for multiple tokens in batch
* Uses Helius DAS API getAssetBatch which is much more efficient than
* calling external APIs (like Birdeye) for each token individually
*
* @param mints - Array of token mint addresses to fetch metadata for
* @returns Map of mint address to metadata object containing symbol and name
*/
getTokenMetadataBatch: async (mints: string[]) => {
// Use Helius DAS API getAssetBatch to fetch metadata for multiple tokens
// This is MUCH more efficient than calling Birdeye token_overview for each token
// Limit: up to 1000 tokens per request

return withRateLimit(async () => {
const metadataMap = new Map<string, { symbol: string | null; name: string | null }>();

Expand Down
187 changes: 113 additions & 74 deletions src/utils/decoder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,41 @@ import { poolKeysSchema } from "./pumpswap/schema.js";
import { poolLayoutSchema } from "./meteora/schema.js";
import { raydiumPoolSchema } from "./raydium/schema.js";
import { createMeteoraTrade, createRaydiumTrade } from "./tradingUtils.js";
import { extractBalances } from "./extractBalances.js";
// import { extractBalances } from "./extractBalances.js";
import { BalanceAnalysis, extractBalances } from "./extractBalances";
import { HeliusTransaction, createHeliusService } from "../../services/api/helius";
import bs58 from "bs58";

export interface TxDecodeResult {
signature: string;
recentBlockhash: string;
blockTime: number;
fee: any;
error: string;
status: any;
accounts: any[];
decodedInstructions: any[];
parsed: any[];
balances: BalanceAnalysis;
totalInstructions: number;
totalAccountsKeys: number;
totalWritableKeys: number;
totalReadonlyKeys: number;
totalInnerInstructions: any;
successfullyDecoded: number;
}

export interface SolanaInstruction {
programIdIndex: number;
accounts: number[];
data: string;
stackHeight: number;
}

export interface InnerInstruction {
index: number;
instructions: SolanaInstruction[];
}

// --------------------------------
// Serializing big ints to strings
// --------------------------------
Expand All @@ -36,7 +67,7 @@ export const decodeBase64Data = (data: any) => {
return Buffer.from(data, 'base64');
}

export const routerDecoderInstructionsData = (type: string, programId: string, instruction: any) => {
export const routerDecoderInstructionsData = (type: string, programId: string, instruction: SolanaInstruction) => {
try {
switch (programId) {
// COMPUTE_BUDGET_PROGRAM_ID
Expand All @@ -62,8 +93,7 @@ export const routerDecoderInstructionsData = (type: string, programId: string, i
return meteoraDecoder(type, programId, instruction);
// PROGRAM_JUPITER_V6
case "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4":
return jupiterDecoder(type, programId, instruction);
// return pumpswapDecoder(programId, instruction);
return jupiterDecoder(instruction);
default:
return null;
}
Expand Down Expand Up @@ -123,7 +153,7 @@ export const extractSwapData = async (programId: string, decoded: any, tx: any)
const dataDecoded = decodeBase64Data(accountInfo.value.data[0]);
const poolKeys = poolKeysSchema.decode(dataDecoded);

return{
return {
programId,
signature: tx.transaction.signatures[0],
timestamp: serializedBigInt(tx.blockTime),
Expand Down Expand Up @@ -173,12 +203,13 @@ export const extractSwapData = async (programId: string, decoded: any, tx: any)
return null;
};

export const decodeTxData = async (tx: any) => {
export const decodeTxData = async (tx: HeliusTransaction): Promise<TxDecodeResult | undefined> => {

// Guard: vérifier que la transaction a la structure minimale requise
if (!tx || !tx.transaction || !tx.transaction.message) {
if (!tx || !tx.signature || !tx.blockTime) {
// Retourner une structure vide pour les transactions invalides (skip silencieusement)
return {
signature: [],
signature: '',
recentBlockhash: '',
blockTime: 0,
fee: 0,
Expand All @@ -204,82 +235,90 @@ export const decodeTxData = async (tx: any) => {
successfullyDecoded: 0
};
}
const heliusService = createHeliusService(process.env.HELIUS_API_KEY || '', 10);
try {
const transactionInfo = await heliusService.getTransaction(tx.signature);

// Extraire les balances avec le nouveau module
const balanceAnalysis = await extractBalances(tx);
const instructions = tx.transaction.message.instructions;
const innerInstructions = tx.meta.innerInstructions;
const accounts = [
...tx.transaction.message.accountKeys,
...(tx.meta?.loadedAddresses?.writable ?? []),
...(tx.meta?.loadedAddresses?.readonly ?? [])
];
const decodedInstructions: any[] = [];
const parsed: any[] = [];
// Extraire les balances avec le nouveau module
const balanceAnalysis = await extractBalances(transactionInfo);
const instructions: SolanaInstruction[] = transactionInfo.transaction.message.instructions;
const innerInstructions: InnerInstruction[] | undefined = transactionInfo.meta.innerInstructions;

const accounts: string[] = [
...transactionInfo.transaction.message.accountKeys,
...(transactionInfo.meta?.loadedAddresses?.writable ?? []),
...(transactionInfo.meta?.loadedAddresses?.readonly ?? [])
];
const decodedInstructions: any[] = [];
const parsed: any[] = [];

// Process main instructions
for (const instruction of instructions) {
try {
const programId = accounts[instruction.programIdIndex];
const decoded = routerDecoderInstructionsData('instruction', programId, instruction);
// Process main instructions
for (const instruction of instructions) {
try {
const programId = accounts[instruction.programIdIndex];
const decoded = routerDecoderInstructionsData('instruction', programId, instruction);

if (decoded) {
decodedInstructions.push({
programId,
type: "main",
instruction,
decoded
});
// Si c’est un swap Jupiter, Pumpfun, Orca, etc.
const swap = await extractSwapData(programId, decoded, tx);
if (swap) parsed.push(swap);
if (decoded) {
decodedInstructions.push({
programId,
type: "main",
instruction,
decoded
});
// Si c’est un swap Jupiter, Pumpfun, Orca, etc.
const swap = await extractSwapData(programId, decoded, transactionInfo);
if (swap) parsed.push(swap);
}
} catch (error) {
console.log("Error processing instruction:", error instanceof Error ? error.message : String(error));
}
} catch (error) {
console.log("Error processing instruction:", error instanceof Error ? error.message : String(error));
}
}

// Process inner instructions
if (innerInstructions) {
for (const innerInst of innerInstructions) {
for (const instruction of innerInst.instructions) {
try {
const programId = accounts[instruction.programIdIndex];
const decoded = routerDecoderInstructionsData('instruction', programId, instruction);
// Process inner instructions
if (innerInstructions) {
for (const innerInst of innerInstructions) {
for (const instruction of innerInst.instructions) {
try {
const programId = accounts[instruction.programIdIndex];
const decoded = routerDecoderInstructionsData('instruction', programId, instruction);

if (decoded) {
decodedInstructions.push({
programId,
type: "inner",
instruction,
decoded
});
const swap = await extractSwapData(programId, decoded, tx);
if (swap) parsed.push(swap);
if (decoded) {
decodedInstructions.push({
programId,
type: "inner",
instruction,
decoded
});
const swap = await extractSwapData(programId, decoded, transactionInfo);
if (swap) parsed.push(swap);
}
} catch (error) {
console.log("Error processing inner instruction:", error instanceof Error ? error.message : String(error));
}
} catch (error) {
console.log("Error processing inner instruction:", error instanceof Error ? error.message : String(error));
}
}
}
}

return {
signature: tx.transaction.signatures,
recentBlockhash: tx.transaction.recentBlockhash,
blockTime: serializedBigInt(tx.blockTime),
fee: tx.transaction.fee,
error: tx.meta.err ? 'FAILED' : 'SUCCESS',
status: tx.meta.status,
accounts: accounts,
decodedInstructions,
parsed,
balances: balanceAnalysis,
totalInstructions: instructions.length,
totalAccountsKeys: tx.transaction.message.accountKeys.length,
totalWritableKeys: tx.meta?.loadedAddresses?.writable.length,
totalReadonlyKeys: tx.meta?.loadedAddresses?.readonly.length,
totalInnerInstructions: innerInstructions ? innerInstructions.reduce((sum: number, inst: any) => sum + inst.instructions.length, 0) : 0,
successfullyDecoded: decodedInstructions.length
return {
signature: transactionInfo.transaction.signatures,
recentBlockhash: transactionInfo.transaction.recentBlockhash,
blockTime: serializedBigInt(transactionInfo.blockTime),
fee: transactionInfo.transaction.fee,
error: transactionInfo.meta.err ? 'FAILED' : 'SUCCESS',
status: transactionInfo.meta.status,
accounts: accounts,
decodedInstructions,
parsed,
balances: balanceAnalysis,
totalInstructions: instructions.length,
totalAccountsKeys: transactionInfo.transaction.message.accountKeys.length,
totalWritableKeys: transactionInfo.meta?.loadedAddresses?.writable.length,
totalReadonlyKeys: transactionInfo.meta?.loadedAddresses?.readonly.length,
totalInnerInstructions: innerInstructions ? innerInstructions.reduce((sum: number, inst: InnerInstruction) => sum + inst.instructions.length, 0) : 0,
successfullyDecoded: decodedInstructions.length
}
} catch (error) {
console.log("Error getting transaction info:", error instanceof Error ? error.message : String(error));
return undefined;
}
}
5 changes: 2 additions & 3 deletions src/utils/decoder/jupiter/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { decodeB58Data, serializedBigInt } from "../index.js";
import { SolanaInstruction, decodeB58Data } from "../";
import { routePlanStepSchema, discriminatorSchema, routePlanLengthSchema, fixedFieldsSchema, swapEventSchema, jupiterSwapFixedSchema } from "./schema.js";
import { getSwapTypeName } from "./swapType.js";

Expand Down Expand Up @@ -30,10 +30,9 @@ const decodeRoutePlan = (buffer: Buffer, offset: number, length: number) => {
return steps;
};

export const jupiterDecoder = (type: string, programId: string, instruction: any) => {
export const jupiterDecoder = (instruction: SolanaInstruction) => {
const dataDecoded = decodeB58Data(instruction.data);
const discriminator = dataDecoded[0];
// console.log("jupiter", discriminator);

try {
switch (discriminator) {
Expand Down
Loading