UNPKG

edwin-sdk

Version:

SDK for integrating AI agents with DeFi protocols

236 lines (198 loc) 9.73 kB
import { config } from 'dotenv'; config(); // Load test environment variables from .env file import { describe, expect, it } from 'vitest'; import { safeJsonStringify } from '../src/utils'; import edwinLogger from '../src/utils/logger'; import { calculateAmounts, extractBalanceChanges } from '../src/plugins/meteora/utils'; import DLMM from '@meteora-ag/dlmm'; import { BN } from '@coral-xyz/anchor'; import { SolanaWalletFactory } from '../src/core/wallets/solana_wallet/factory'; import { MeteoraProtocol } from '../src/plugins/meteora/meteoraProtocol'; const AMOUNT_USDC_TO_ADD = 0.05; // Meteora test describe('Meteora test', () => { if (!process.env.SOLANA_PRIVATE_KEY) { throw new Error('SOLANA_PRIVATE_KEY is not set'); } // Create a wallet using the factory method instead of direct instantiation const wallet = SolanaWalletFactory.fromPrivateKey(process.env.SOLANA_PRIVATE_KEY); const meteora = new MeteoraProtocol(wallet); describe('Meteora Read-only tests', () => { it('test meteora getPools', async () => { const results = await meteora.getPools({ asset: 'sol', assetB: 'usdc', }); expect(results).toBeDefined(); expect(results).toBeInstanceOf(Array); expect(results.length).toBe(10); }, 30000); // 30 second timeout it('test meteora getPositions - note - need to use a paid RPC for this test', async () => { const positions = await meteora.getPositions(); edwinLogger.info('🚀 ~ it ~ getPositions result:', safeJsonStringify(positions)); }, 120000); // 120 second timeout }); describe('Meteora Write (active) tests', () => { it('test meteora create position and add liquidity, then check for new position', async () => { const results = await meteora.getPools({ asset: 'sol', assetB: 'usdc', }); const topPoolAddress = results[0].address; // Add liquidity and get transaction hash const txSignature = await meteora.addLiquidity({ poolAddress: topPoolAddress, amount: 'auto', amountB: AMOUNT_USDC_TO_ADD, }); edwinLogger.info('🚀 ~ it ~ transaction signature:', txSignature); // Get position details from transaction const positionInfo = await meteora.getPositionInfoFromTransaction(txSignature); // Verify liquidity was added correctly expect(positionInfo.liquidityAdded).toBeDefined(); expect(positionInfo.liquidityAdded).toHaveLength(2); expect(positionInfo.liquidityAdded[1]).toBeCloseTo(AMOUNT_USDC_TO_ADD, 1); // Verify USDC amount is approximately correct expect(positionInfo.liquidityAdded[0]).toBeGreaterThan(0); // Verify SOL amount is non-zero edwinLogger.info('🚀 ~ it ~ position info:', positionInfo); const positionAddress = positionInfo.positionAddress; // Get positions after adding liquidity const positions = await meteora.getPositionsFromPool({ poolAddress: topPoolAddress, }); // Check that positions is ok - should be 1 position expect(positions).toBeDefined(); expect(positions.length).toBe(1); expect(positions[0].publicKey.toString()).toBe(positionAddress); }, 120000); // 120 second timeout describe('Remove Liquidity', () => { it('Test Meteora position removal', async () => { // Check if positions exist const positions = await meteora.getPositions(); edwinLogger.info('🚀 ~ positions check:', safeJsonStringify(positions)); // Skip test if no positions if (!positions || positions.size === 0) { edwinLogger.info('No positions found - skipping remove liquidity test'); return; // Simply return early from the test } // Remove liquidity from first position found const poolAddress = Array.from(positions.keys())[0]; const result = await meteora.removeLiquidity({ poolAddress: poolAddress, shouldClosePosition: true, }); edwinLogger.info('🚀 ~ removeLiquidity result:', result); // Check positions after removal const positionsAfter = await meteora.getPositionsFromPool({ poolAddress: poolAddress, }); edwinLogger.info('🚀 ~ positions after removal:', positionsAfter); // Verify position was closed expect(positionsAfter.length).toBe(positions.size - 1); }, 60000); // 60 second timeout }); }); }); describe('Meteora utils', () => { if (!process.env.SOLANA_PRIVATE_KEY) { throw new Error('SOLANA_PRIVATE_KEY is not set'); } // Create a wallet using the factory method instead of direct instantiation const wallet = SolanaWalletFactory.fromPrivateKey(process.env.SOLANA_PRIVATE_KEY); describe('calculateAmounts', () => { // Mock DLMM instance const mockDlmmPool = { tokenX: { decimal: 9 }, tokenY: { decimal: 6 }, } as DLMM; it('should calculate amounts when amount is auto', async () => { const result = await calculateAmounts( 'auto', '100', '2', // price per token mockDlmmPool ); expect(result[0]).toBeInstanceOf(BN); expect(result[1]).toBeInstanceOf(BN); // For amountB = 100 and price = 2 // amountA should be 50 (100/2) in base units expect(result[0].toString()).toBe((50 * 10 ** 9).toString()); // amountB should be 100 in base units expect(result[1].toString()).toBe((100 * 10 ** 6).toString()); }); it('should calculate amounts when amountB is auto', async () => { const result = await calculateAmounts( '50', 'auto', '2', // price per token mockDlmmPool ); expect(result[0]).toBeInstanceOf(BN); expect(result[1]).toBeInstanceOf(BN); // For amountA = 50 and price = 2 // amountA should be 50 in base units expect(result[0].toString()).toBe((50 * 10 ** 9).toString()); // amountB should be 100 (50*2) in base units expect(result[1].toString()).toBe((100 * 10 ** 6).toString()); }); it('should handle direct amounts', async () => { const result = await calculateAmounts( '50', '100', '2', // price per token (not used in this case) mockDlmmPool ); expect(result[0]).toBeInstanceOf(BN); expect(result[1]).toBeInstanceOf(BN); expect(result[0].toString()).toBe((50 * 10 ** 9).toString()); expect(result[1].toString()).toBe((100 * 10 ** 6).toString()); }); it('should throw error when both amounts are auto', async () => { await expect(calculateAmounts('auto', 'auto', '2', mockDlmmPool)).rejects.toThrow(TypeError); }); it('should throw error for invalid number inputs', async () => { await expect(calculateAmounts('invalid', '100', '2', mockDlmmPool)).rejects.toThrow(TypeError); }); }); describe('extractBalanceChanges', () => { it('should correctly extract balance changes from a transaction', async () => { const tokenXMint = await wallet.getTokenAddress('sol'); const tokenYMint = await wallet.getTokenAddress('usdc'); if (!tokenXMint || !tokenYMint) { throw new Error('Token address not found'); } const connection = wallet.getConnection(); const result = await extractBalanceChanges( connection, '31brBmpbZMqduwi3u1Z6Si2Xt4izdkX2TE45jdeeq1oVreiahKyfaHSArMKdyqKWeYFT6GwGWRBxwfnwfbGbPypR', tokenXMint, tokenYMint ); expect(result).toHaveProperty('liquidityRemoved'); expect(result).toHaveProperty('feesClaimed'); expect(result.liquidityRemoved).toHaveLength(2); expect(result.feesClaimed).toHaveLength(2); // Test against known values from the transaction expect(result).toEqual({ liquidityRemoved: [0, 20.274523], feesClaimed: [0.000004094, 0.003779], }); const result2 = await extractBalanceChanges( connection, '57FFqxEZqbyfesEcSiMNGsHUTfSzKRvcreqBzJirFWrHHW37YaRvNGd8EfGPVSEzXuQrdZbxZWM4NjBLkFZ7TmVN', tokenXMint, tokenYMint ); // Test against known values from the transaction expect(result2).toEqual({ liquidityRemoved: [0.051702288, 0], feesClaimed: [0.00000511, 0.000032], }); }, 20000); it('should handle transaction not found', async () => { const connection = wallet.getConnection(); await expect( extractBalanceChanges(connection, 'invalid_signature', 'token_x_address', 'token_y_address') ).rejects.toThrow(Error); }, 40000); }); });