import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { createTxnBot, type TxnBotDependencies } from './service.js'; import type { BotConfigService } from './services/BotConfigService.js'; import type { BlockchainService } from './services/BlockchainService.js'; import type { GraphQLService } from './services/GraphQLService.js'; const BASE_CONFIG = { PROVIDER_URL: 'http://localhost:8545', PRIVATE_KEY: '0xdeadbeef000000000000000000000000000000000000000000000000deadbeef', LM_CONTRACT_ADDRESS: '0x0000000000000000000000000000000000000001', STAKE_CONTRACT_ADDRESS: '0x0000000000000000000000000000000000000002', GRAPHQL_ENDPOINT: 'http://localhost:42069/graphql', ENVIRONMENT: 'test', PORT: '3000', }; function makeTx(hash: string) { return { hash, wait: (): Promise => Promise.resolve(null) }; } function makeDeps( blockchain?: Partial<{ estimateRecenterGas: () => Promise; recenter: () => Promise>; }>, ): TxnBotDependencies { return { configService: { getConfig: () => ({ ...BASE_CONFIG }), getPort: () => '3000', } as unknown as BotConfigService, blockchainService: { estimateRecenterGas: blockchain?.estimateRecenterGas ?? (() => Promise.resolve()), recenter: blockchain?.recenter ?? (() => Promise.resolve(makeTx('0xabc'))), checkFunds: () => Promise.resolve('1.0'), payTax: () => Promise.resolve(makeTx('0xpay')), } as unknown as BlockchainService, graphQLService: { fetchActivePositions: () => Promise.resolve([]), } as unknown as GraphQLService, }; } describe('evaluateRecenterOpportunity', () => { it('returns canRecenter: true when gas estimation succeeds', async () => { const bot = createTxnBot(makeDeps()); const result = await bot.evaluateRecenterOpportunity(); assert.equal(result.canRecenter, true); assert.equal(result.reason, null); assert.equal(result.error, null); assert.ok(result.checkedAtMs > 0); }); it('returns canRecenter: false with extracted revert reason', async () => { const bot = createTxnBot( makeDeps({ estimateRecenterGas: () => Promise.reject({ shortMessage: 'execution reverted: recenter not needed', message: 'execution reverted: recenter not needed', }), }), ); const result = await bot.evaluateRecenterOpportunity(); assert.equal(result.canRecenter, false); assert.equal(result.reason, 'recenter not needed'); assert.equal(result.error, 'execution reverted: recenter not needed'); }); it('returns canRecenter: false with generic message when no revert prefix', async () => { const bot = createTxnBot( makeDeps({ estimateRecenterGas: () => Promise.reject({ message: 'connection refused' }), }), ); const result = await bot.evaluateRecenterOpportunity(); assert.equal(result.canRecenter, false); assert.equal(result.reason, 'connection refused'); assert.ok(result.checkedAtMs > 0); }); }); describe('attemptRecenter', () => { it('calls recenter() and returns executed: true when eligible', async () => { let recenterCalled = false; const bot = createTxnBot( makeDeps({ recenter: () => { recenterCalled = true; return Promise.resolve(makeTx('0xdeadbeef')); }, }), ); const result = await bot.attemptRecenter(); assert.equal(result.executed, true); assert.equal(result.txHash, '0xdeadbeef'); assert.ok(recenterCalled, 'recenter() should have been called'); }); it('skips recenter() and returns executed: false when not eligible', async () => { let recenterCalled = false; const bot = createTxnBot( makeDeps({ estimateRecenterGas: () => Promise.reject({ message: 'recenter not needed' }), recenter: () => { recenterCalled = true; return Promise.resolve(makeTx('0xabc')); }, }), ); const result = await bot.attemptRecenter(); assert.equal(result.executed, false); assert.equal(recenterCalled, false, 'recenter() must not be called when not eligible'); assert.ok(result.message); }); it('propagates error thrown by recenter() — caught by liquidityLoop', async () => { const bot = createTxnBot( makeDeps({ recenter: () => Promise.reject(new Error('tx submission failed')), }), ); await assert.rejects(bot.attemptRecenter(), /tx submission failed/); }); });