first commit
This commit is contained in:
81
tests/unit/bot.test.ts
Normal file
81
tests/unit/bot.test.ts
Normal 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
132
tests/unit/meteora.test.ts
Normal 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
13
tests/unit/sanity.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user