edwin-sdk
Version:
SDK for integrating AI agents with DeFi protocols
250 lines (215 loc) • 8.75 kB
text/typescript
import {
Connection,
ParsedTransactionWithMeta,
Transaction,
TransactionMessage,
VersionedTransaction,
ParsedInstruction as SolanaParsedInstruction,
} from '@solana/web3.js';
import { BN } from '@coral-xyz/anchor';
import DLMM, { TokenReserve } from '@meteora-ag/dlmm';
import edwinLogger from '../../utils/logger';
import { SolanaWalletClient } from '../../core/wallets';
interface TokenAmount {
amount: string;
decimals: number;
uiAmount: number;
uiAmountString: string;
}
interface TransferInfo {
amount: string;
authority: string;
destination: string;
mint: string;
source: string;
tokenAmount: TokenAmount;
}
interface ParsedInstruction extends Omit<SolanaParsedInstruction, 'program' | 'parsed'> {
program: string | undefined;
parsed?: {
type: string;
info: TransferInfo;
};
}
interface InnerInstruction {
index: number;
instructions: ParsedInstruction[];
}
interface BalanceChanges {
liquidityRemoved: [number, number];
feesClaimed: [number, number];
}
export async function calculateAmounts(
amount: string,
amountB: string,
activeBinPricePerToken: string,
dlmmPool: DLMM
): Promise<[BN, BN]> {
let totalXAmount;
let totalYAmount;
// Helper function to safely get decimals
const getDecimals = (token: TokenReserve): number => {
if (token.mint) {
return token.mint.decimals;
} else if ('decimal' in token) {
if (typeof token.decimal === 'number') {
return token.decimal;
}
return 0;
} else {
return 0;
}
};
const tokenXDecimals = getDecimals(dlmmPool.tokenX);
const tokenYDecimals = getDecimals(dlmmPool.tokenY);
if (amount === 'auto' && amountB === 'auto') {
throw new TypeError(
"Amount for both first asset and second asset cannot be 'auto' for Meteora liquidity provision"
);
} else if (!amount || !amountB) {
throw new TypeError('Both amounts must be specified for Meteora liquidity provision');
}
if (amount === 'auto') {
// Calculate amount based on amountB
if (!isNaN(Number(amountB))) {
totalXAmount = new BN((Number(amountB) / Number(activeBinPricePerToken)) * 10 ** tokenXDecimals);
totalYAmount = new BN(Number(amountB) * 10 ** tokenYDecimals);
} else {
throw new TypeError('Invalid amountB value for second token for Meteora liquidity provision');
}
} else if (amountB === 'auto') {
// Calculate amountB based on amount
if (!isNaN(Number(amount))) {
totalXAmount = new BN(Number(amount) * 10 ** tokenXDecimals);
totalYAmount = new BN(Number(amount) * Number(activeBinPricePerToken) * 10 ** tokenYDecimals);
} else {
throw new TypeError('Invalid amount value for first token for Meteora liquidity provision');
}
} else if (!isNaN(Number(amount)) && !isNaN(Number(amountB))) {
// Both are numbers
totalXAmount = new BN(Number(amount) * 10 ** tokenXDecimals);
totalYAmount = new BN(Number(amountB) * 10 ** tokenYDecimals);
} else {
throw new TypeError("Both amounts must be numbers or 'auto' for Meteora liquidity provision");
}
return [totalXAmount, totalYAmount];
}
export async function getParsedTransactionWithRetries(
connection: Connection,
signature: string
): Promise<ParsedTransactionWithMeta> {
for (let i = 0; i < 5; i++) {
const txInfo = await connection.getParsedTransaction(signature, { maxSupportedTransactionVersion: 0 });
if (txInfo) {
return txInfo;
}
if (i < 2) {
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
throw new Error('Failed to get parsed transaction after 3 attempts');
}
export async function extractBalanceChanges(
connection: Connection,
signature: string,
tokenXAddress: string,
tokenYAddress: string
): Promise<BalanceChanges> {
const METEORA_DLMM_PROGRAM_ID = 'LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo';
const txInfo = await getParsedTransactionWithRetries(connection, signature);
if (!txInfo || !txInfo.meta) {
throw new Error('Transaction details not found or not parsed');
}
const outerInstructions = txInfo.transaction.message.instructions as ParsedInstruction[];
const innerInstructions = txInfo.meta.innerInstructions || [];
const innerMap: Record<number, ParsedInstruction[]> = {};
for (const inner of innerInstructions) {
innerMap[inner.index] = inner.instructions as ParsedInstruction[];
}
const meteoraInstructionIndices: number[] = [];
outerInstructions.forEach((ix, index) => {
if (ix.programId?.toString() === METEORA_DLMM_PROGRAM_ID) {
meteoraInstructionIndices.push(index);
}
});
if (meteoraInstructionIndices.length < 2) {
throw new Error('Expected at least two Meteora instructions in the transaction');
}
const removeLiquidityIndex = meteoraInstructionIndices[0];
const claimFeeIndex = meteoraInstructionIndices[1];
const decodeTokenTransfers = (instructions: ParsedInstruction[]): TransferInfo[] => {
const transfers: TransferInfo[] = [];
for (const ix of instructions) {
if (ix.program === 'spl-token' && ix.parsed?.type === 'transferChecked') {
transfers.push(ix.parsed.info);
}
}
return transfers;
};
const removeLiquidityTransfers = innerMap[removeLiquidityIndex]
? decodeTokenTransfers(innerMap[removeLiquidityIndex])
: [];
const claimFeeTransfers = innerMap[claimFeeIndex] ? decodeTokenTransfers(innerMap[claimFeeIndex]) : [];
const liquidityRemovedA =
removeLiquidityTransfers.find(transfer => transfer.mint === tokenXAddress)?.tokenAmount.uiAmount || 0;
const liquidityRemovedB =
removeLiquidityTransfers.find(transfer => transfer.mint === tokenYAddress)?.tokenAmount.uiAmount || 0;
const feesClaimedA = claimFeeTransfers.find(transfer => transfer.mint === tokenXAddress)?.tokenAmount.uiAmount || 0;
const feesClaimedB = claimFeeTransfers.find(transfer => transfer.mint === tokenYAddress)?.tokenAmount.uiAmount || 0;
return {
liquidityRemoved: [liquidityRemovedA, liquidityRemovedB],
feesClaimed: [feesClaimedA, feesClaimedB],
};
}
export async function extractAddLiquidityTokenAmounts(innerInstructions: InnerInstruction[]): Promise<TokenAmount[]> {
const tokenAmounts: TokenAmount[] = [];
for (const innerInstruction of innerInstructions) {
if (innerInstruction.instructions) {
for (const instruction of innerInstruction.instructions) {
if (instruction.parsed?.type === 'transferChecked') {
edwinLogger.debug(`Transfer info amounts: ${JSON.stringify(instruction.parsed.info.tokenAmount)}`);
tokenAmounts.push(instruction.parsed.info.tokenAmount);
}
}
}
}
return tokenAmounts;
}
interface SimulationInnerInstructions {
innerInstructions?: InnerInstruction[];
}
interface SimulationResult {
value: SimulationInnerInstructions;
}
export async function simulateAddLiquidityTransaction(
connection: Connection,
tx: Transaction,
wallet: SolanaWalletClient
): Promise<TokenAmount[]> {
const latestBlockhash = await connection.getLatestBlockhash();
const messageV0 = new TransactionMessage({
payerKey: wallet.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: tx.instructions,
}).compileToV0Message();
const versionedTx = new VersionedTransaction(messageV0);
const simulationResult = (await connection.simulateTransaction(versionedTx, {
innerInstructions: true,
})) as SimulationResult;
const innerInstructions = simulationResult.value.innerInstructions;
if (!innerInstructions) {
throw new Error('Inner instructions not found in simulation result');
}
return extractAddLiquidityTokenAmounts(innerInstructions);
}
export async function verifyAddLiquidityTokenAmounts(
connection: Connection,
signature: string
): Promise<TokenAmount[]> {
const txInfo = await getParsedTransactionWithRetries(connection, signature);
if (!txInfo || !txInfo.meta) {
throw new Error('Transaction details not found or not parsed');
}
const innerInstructions = txInfo.meta.innerInstructions || [];
return extractAddLiquidityTokenAmounts(innerInstructions as InnerInstruction[]);
}