first commit

This commit is contained in:
Louis-Sinan
2025-12-21 12:22:16 +01:00
commit c9daaddb77
34 changed files with 1735 additions and 0 deletions

81
tests/unit/bot.test.ts Normal file
View File

@@ -0,0 +1,81 @@
import { DLMMBot } from "../../src/bot";
import { MeteoraWrapper } from "../../src/meteora";
import logger from "../../src/utils/logger";
import { BN } from "@coral-xyz/anchor";
jest.mock("../../src/utils/config", () => ({
connection: { getActiveBin: jest.fn() },
wallet: { publicKey: { toBase58: () => "wallet1" } },
REBALANCE_THRESHOLD_PERCENT: 0.15,
poolAddress: { toBase58: () => "pool1" },
DRY_RUN: false,
SLIPPAGE_BPS: 100,
MAX_RETRIES: 3,
JLP_USD_ESTIMATE: 3.0,
SOL_USD_ESTIMATE: 100.0,
COMPOUND_THRESHOLD_USD: 1.0,
}));
jest.mock("../../src/utils/logger", () => ({
info: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
}));
jest.mock("../../src/meteora");
describe("DLMMBot Rebalance Logic", () => {
let bot: DLMMBot;
let mockMeteora: jest.Mocked<MeteoraWrapper>;
beforeEach(() => {
jest.clearAllMocks();
bot = new DLMMBot();
mockMeteora = (bot as any).meteora;
mockMeteora.getBalances.mockResolvedValue({ balanceX: new BN(0), balanceY: new BN(0) });
mockMeteora.getTokenPrice.mockResolvedValue(1.0);
mockMeteora.dlmmPool = {
tokenX: { mint: { decimals: 9, address: { toBase58: () => "So11111111111111111111111111111111111111112" } } },
tokenY: { mint: { decimals: 6, address: { toBase58: () => "JLP" } } }
} as any;
});
it("should not rebalance if within range", async () => {
mockMeteora.getActiveBin.mockResolvedValue({ binId: 100 } as any);
mockMeteora.getPositions.mockResolvedValue([
{
publicKey: { toBase58: () => "pos1" },
positionData: { lowerBinId: 90, upperBinId: 110 },
},
] as any);
await bot.rebalance();
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining("Portfolio healthy")
);
expect(mockMeteora.withdrawAll).not.toHaveBeenCalled();
});
it("should trigger rebalance if out of range", async () => {
mockMeteora.getActiveBin.mockResolvedValue({ binId: 120 } as any);
mockMeteora.getPositions.mockResolvedValue([
{
publicKey: { toBase58: () => "pos1" },
positionData: { lowerBinId: 90, upperBinId: 110 },
},
] as any);
mockMeteora.getBalances.mockResolvedValue({ balanceX: new BN(100), balanceY: new BN(100) });
mockMeteora.getTokenPrice.mockResolvedValue(1.0);
// dlmmPool already mocked in beforeEach
await bot.rebalance();
expect(mockMeteora.withdrawAll).toHaveBeenCalled();
expect(mockMeteora.getBalances).toHaveBeenCalled();
expect(mockMeteora.deposit).toHaveBeenCalledWith(
new BN(100),
new BN(100),
120
);
});
});

132
tests/unit/meteora.test.ts Normal file
View File

@@ -0,0 +1,132 @@
import { PublicKey } from "@solana/web3.js";
import { BN } from "@coral-xyz/anchor";
// Objects prefixed with 'mock' are hoisted and allowed in jest.mock factory
const mockInternalConfig = {
connection: {},
wallet: { publicKey: new PublicKey("LbVRzDTvBDEcrthxfZ4RL6yiq3uZw8bS6MwtdY6UhFQ") },
poolAddress: new PublicKey("LbVRzDTvBDEcrthxfZ4RL6yiq3uZw8bS6MwtdY6UhFQ"),
DRY_RUN: false,
SLIPPAGE_BPS: 100,
MAX_RETRIES: 3,
MIN_SOL_FOR_GAS: 0.1,
};
jest.mock("../../src/utils/config", () => mockInternalConfig);
jest.mock("@meteora-ag/dlmm");
jest.mock("@solana/web3.js", () => ({
...jest.requireActual("@solana/web3.js"),
sendAndConfirmTransaction: jest.fn(),
}));
jest.mock("../../src/utils/logger", () => ({
info: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
}));
import { MeteoraWrapper } from "../../src/meteora";
import DLMM from "@meteora-ag/dlmm";
import { sendAndConfirmTransaction } from "@solana/web3.js";
import logger from "../../src/utils/logger";
describe("MeteoraWrapper", () => {
let wrapper: MeteoraWrapper;
let mockDlmm: any;
beforeEach(async () => {
jest.clearAllMocks();
wrapper = new MeteoraWrapper();
mockDlmm = {
getActiveBin: jest.fn(),
getPositionsByUserAndLbPair: jest.fn(),
removeLiquidity: jest.fn(),
initializePositionAndAddLiquidityByStrategy: jest.fn(),
claimAllRewardsByPosition: jest.fn(),
tokenX: { mint: { address: new PublicKey("LbVRzDTvBDEcrthxfZ4RL6yiq3uZw8bS6MwtdY6UhFQ") } },
tokenY: { mint: { address: new PublicKey("LbVRzDTvBDEcrthxfZ4RL6yiq3uZw8bS6MwtdY6UhFQ") } },
};
(DLMM.create as jest.Mock).mockResolvedValue(mockDlmm);
await wrapper.init();
});
it("should get active bin", async () => {
mockDlmm.getActiveBin.mockResolvedValue({ binId: 100 });
const bin = await wrapper.getActiveBin();
expect(bin.binId).toBe(100);
});
it("should withdraw all liquidity from positions", async () => {
const mockPosition = {
publicKey: new PublicKey("LbVRzDTvBDEcrthxfZ4RL6yiq3uZw8bS6MwtdY6UhFQ"),
positionData: { lowerBinId: 90, upperBinId: 110 },
};
mockDlmm.getPositionsByUserAndLbPair.mockResolvedValue({
userPositions: [mockPosition],
});
mockDlmm.removeLiquidity.mockResolvedValue([{}]);
(sendAndConfirmTransaction as jest.Mock).mockResolvedValue("txHash");
await wrapper.withdrawAll();
expect(mockDlmm.removeLiquidity).toHaveBeenCalled();
expect(sendAndConfirmTransaction).toHaveBeenCalled();
});
it("should deposit liquidity", async () => {
mockDlmm.initializePositionAndAddLiquidityByStrategy.mockResolvedValue({});
(sendAndConfirmTransaction as jest.Mock).mockResolvedValue("txHash");
await wrapper.deposit(
new BN(100),
new BN(100),
100
);
expect(
mockDlmm.initializePositionAndAddLiquidityByStrategy
).toHaveBeenCalled();
expect(sendAndConfirmTransaction).toHaveBeenCalled();
});
it("should skip transactions in DRY_RUN mode", async () => {
mockInternalConfig.DRY_RUN = true;
mockDlmm.initializePositionAndAddLiquidityByStrategy.mockResolvedValue({});
await wrapper.deposit(
new BN(100),
new BN(100),
100
);
expect(sendAndConfirmTransaction).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining("[DRY RUN]")
);
mockInternalConfig.DRY_RUN = false; // Reset
});
it("should get token balances", async () => {
mockDlmm.tokenX = { mint: { address: new PublicKey("LbVRzDTvBDEcrthxfZ4RL6yiq3uZw8bS6MwtdY6UhFQ") } };
mockDlmm.tokenY = { mint: { address: new PublicKey("LbVRzDTvBDEcrthxfZ4RL6yiq3uZw8bS6MwtdY6UhFQ") } };
const balances = await wrapper.getBalances();
expect(balances).toBeDefined();
});
it("should log deposit details", async () => {
mockDlmm.initializePositionAndAddLiquidityByStrategy.mockResolvedValue({});
mockDlmm.tokenX = { mint: { address: new PublicKey("LbVRzDTvBDEcrthxfZ4RL6yiq3uZw8bS6MwtdY6UhFQ") } };
mockDlmm.tokenY = { mint: { address: new PublicKey("LbVRzDTvBDEcrthxfZ4RL6yiq3uZw8bS6MwtdY6UhFQ") } };
(sendAndConfirmTransaction as jest.Mock).mockResolvedValue("txHash");
await wrapper.deposit(new BN(100), new BN(100), 100);
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining("Depositing liquidity")
);
});
});

13
tests/unit/sanity.test.ts Normal file
View File

@@ -0,0 +1,13 @@
import { BN } from "@coral-xyz/anchor";
describe("Basic Setup Test", () => {
it("should handle BN correctly", () => {
const amount = new BN(100);
expect(amount.toString()).toBe("100");
});
it("should be able to import from src (sanity check)", () => {
// We'll add more complex tests once we mock external dependencies
expect(true).toBe(true);
});
});