Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { createMock } from '@golevelup/ts-jest';
import { Equal } from 'typeorm';
import { LiquidityManagementAction } from '../../../entities/liquidity-management-action.entity';
import { LiquidityManagementOrder } from '../../../entities/liquidity-management-order.entity';
import { LiquidityManagementOrderRepository } from '../../../repositories/liquidity-management-order.repository';
import { LiquidityManagementPipelineRepository } from '../../../repositories/liquidity-management-pipeline.repository';
import { LiquidityManagementService } from '../../../services/liquidity-management.service';
import { LiquidityPipelineAdapter } from '../liquidity-pipeline.adapter';

describe('LiquidityPipelineAdapter', () => {
let adapter: LiquidityPipelineAdapter;
let liquidityManagementService: LiquidityManagementService;
let pipelineRepo: LiquidityManagementPipelineRepository;
let orderRepo: LiquidityManagementOrderRepository;

beforeEach(() => {
liquidityManagementService = createMock<LiquidityManagementService>();
pipelineRepo = createMock<LiquidityManagementPipelineRepository>();
orderRepo = createMock<LiquidityManagementOrderRepository>();

adapter = new LiquidityPipelineAdapter(liquidityManagementService, pipelineRepo, orderRepo);
});

describe('buy', () => {
it('parses the structured balance/min/max suffix appended by the exchange adapters and calls buyLiquidity with the correct amounts', async () => {
const previousOrderId = 10;
const order = Object.assign(new LiquidityManagementOrder(), {
action: Object.assign(new LiquidityManagementAction(), {
params: JSON.stringify({ assetId: 1, orderIndex: 1, demandOnly: false }),
}),
previousOrderId,
});

const previousOrder = Object.assign(new LiquidityManagementOrder(), {
id: previousOrderId,
errorMessage: 'Binance: not enough balance for BTC (balance: 0.5, min. requested: 1.2, max. requested: 2.5)',
});

jest.spyOn(orderRepo, 'findOneBy').mockResolvedValue(previousOrder);
const buyLiquidityMock = jest
.spyOn(liquidityManagementService, 'buyLiquidity')
.mockResolvedValue({ id: 42 } as Awaited<ReturnType<LiquidityManagementService['buyLiquidity']>>);

const correlationId = await adapter['buy'](order);

expect(orderRepo.findOneBy).toHaveBeenCalledWith({ id: Equal(previousOrderId) });
expect(buyLiquidityMock).toHaveBeenCalledWith(1, expect.closeTo(0.7), expect.closeTo(2.0), true);
expect(correlationId).toBe('42');
});

it('still extracts the correct amounts when the raw exchange message contains parentheses/JSON before the appended suffix', async () => {
const previousOrderId = 11;
const order = Object.assign(new LiquidityManagementOrder(), {
action: Object.assign(new LiquidityManagementAction(), {
params: JSON.stringify({ assetId: 1, orderIndex: 1, demandOnly: false }),
}),
previousOrderId,
});

const previousOrder = Object.assign(new LiquidityManagementOrder(), {
id: previousOrderId,
errorMessage:
'binance {"code":-2010,"msg":"(balance issue)"} (balance: 0.5, min. requested: 1.2, max. requested: 2.5)',
});

jest.spyOn(orderRepo, 'findOneBy').mockResolvedValue(previousOrder);
const buyLiquidityMock = jest
.spyOn(liquidityManagementService, 'buyLiquidity')
.mockResolvedValue({ id: 43 } as Awaited<ReturnType<LiquidityManagementService['buyLiquidity']>>);

const correlationId = await adapter['buy'](order);

expect(orderRepo.findOneBy).toHaveBeenCalledWith({ id: Equal(previousOrderId) });
expect(buyLiquidityMock).toHaveBeenCalledWith(1, expect.closeTo(0.7), expect.closeTo(2.0), true);
expect(correlationId).toBe('43');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
return response.id;
} catch (e) {
if (this.isBalanceTooLowError(e)) {
throw new OrderNotProcessableException(e.message);
throw new OrderNotProcessableException(
`${e.message} (balance: ${balance}, min. requested: ${order.minAmount}, max. requested: ${order.maxAmount})`,
);
}

throw e;
Expand Down Expand Up @@ -173,7 +175,9 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
return await this.exchangeService.sell(tradeAsset, targetAssetEntity.name, amount);
} catch (e) {
if (this.isBalanceTooLowError(e)) {
throw new OrderNotProcessableException(e.message);
throw new OrderNotProcessableException(
`${e.message} (balance: ${availableBalance}, min. requested: ${minSellAmount}, max. requested: ${maxSellAmount})`,
);
}

if (e.message?.includes('Illegal characters found')) {
Expand Down Expand Up @@ -218,7 +222,9 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
return await this.exchangeService.sell(asset, tradeAsset, amount);
} catch (e) {
if (this.isBalanceTooLowError(e)) {
throw new OrderNotProcessableException(e.message);
throw new OrderNotProcessableException(
`${e.message} (balance: ${availableBalance}, min. requested: ${order.minAmount}, max. requested: ${order.maxAmount})`,
);
}

if (e.message?.includes('Illegal characters found')) {
Expand Down Expand Up @@ -276,7 +282,9 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter {
return response.id;
} catch (e) {
if (this.isBalanceTooLowError(e)) {
throw new OrderNotProcessableException(e.message);
throw new OrderNotProcessableException(
`${e.message} (balance: ${sourceBalance}, min. requested: ${minAmount}, max. requested: ${maxAmount})`,
);
}

throw e;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ export class ScryptAdapter extends LiquidityActionAdapter {
return response.id;
} catch (e) {
if (this.isBalanceTooLowError(e)) {
throw new OrderNotProcessableException(e.message);
throw new OrderNotProcessableException(
`${e.message} (balance: ${balance}, min. requested: ${order.minAmount}, max. requested: ${order.maxAmount})`,
);
}

throw e;
Expand Down Expand Up @@ -176,7 +178,9 @@ export class ScryptAdapter extends LiquidityActionAdapter {
return await this.scryptService.sell(tradeAsset, targetAssetEntity.dexName, amount);
} catch (e) {
if (this.isBalanceTooLowError(e)) {
throw new OrderNotProcessableException(e.message);
throw new OrderNotProcessableException(
`${e.message} (balance: ${availableBalance}, min. requested: ${minSellAmount}, max. requested: ${maxSellAmount})`,
);
}

throw e;
Expand Down Expand Up @@ -355,6 +359,8 @@ export class ScryptAdapter extends LiquidityActionAdapter {
try {
return await this.scryptService.sell(fromAsset, toAsset, amount);
} catch (e) {
// No "(balance: ..., min. requested: ..., max. requested: ...)" suffix: balance/min/max are not in scope here.
// The only production Scrypt 'sell' action has no onFail/onSuccess chain, so its error never reaches the liquidity-pipeline regex parser.
if (this.isBalanceTooLowError(e)) {
throw new OrderNotProcessableException(e.message);
}
Expand Down
Loading