134 lines
4.4 KiB
TypeScript
134 lines
4.4 KiB
TypeScript
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<null> => Promise.resolve(null) };
|
|
}
|
|
|
|
function makeDeps(
|
|
blockchain?: Partial<{
|
|
estimateRecenterGas: () => Promise<void>;
|
|
recenter: () => Promise<ReturnType<typeof makeTx>>;
|
|
}>,
|
|
): 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/);
|
|
});
|
|
});
|