Skip to content
Open
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
36 changes: 36 additions & 0 deletions migration/1774100000000-AddScryptSellIfDeficitAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module.exports = class AddScryptSellIfDeficitAction1774100000000 {
name = 'AddScryptSellIfDeficitAction1774100000000';

async up(queryRunner) {
// Create sell-if-deficit action: sells EUR for BTC only when Bitcoin on-chain has a deficit, falls back to existing EUR→USDT sell (action 233)
await queryRunner.query(`
INSERT INTO "dbo"."liquidity_management_action" ("system", "command", "tag", "params", "onSuccessId", "onFailId")
VALUES ('Scrypt', 'sell-if-deficit', 'SCRYPT EUR->BTC if deficit', '{"tradeAsset":"BTC","checkAssetId":113}', NULL, 233)
`);

// Update rule 313 (Scrypt/EUR) to use new action as redundancy start
await queryRunner.query(`
UPDATE "dbo"."liquidity_management_rule"
SET "redundancyStartActionId" = (
SELECT "id" FROM "dbo"."liquidity_management_action"
WHERE "system" = 'Scrypt' AND "command" = 'sell-if-deficit'
)
WHERE "id" = 313
`);
}

async down(queryRunner) {
// Restore rule 313 to original action (233 = sell EUR→USDT)
await queryRunner.query(`
UPDATE "dbo"."liquidity_management_rule"
SET "redundancyStartActionId" = 233
WHERE "id" = 313
`);

// Remove sell-if-deficit action
await queryRunner.query(`
DELETE FROM "dbo"."liquidity_management_action"
WHERE "system" = 'Scrypt' AND "command" = 'sell-if-deficit'
`);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import { LiquidityManagementSystem } from '../../enums';
import { OrderFailedException } from '../../exceptions/order-failed.exception';
import { OrderNotProcessableException } from '../../exceptions/order-not-processable.exception';
import { Command, CorrelationId } from '../../interfaces';
import { LiquidityBalanceRepository } from '../../repositories/liquidity-balance.repository';
import { LiquidityManagementOrderRepository } from '../../repositories/liquidity-management-order.repository';
import { LiquidityManagementRuleRepository } from '../../repositories/liquidity-management-rule.repository';
import { LiquidityActionAdapter } from './base/liquidity-action.adapter';

export enum ScryptAdapterCommands {
WITHDRAW = 'withdraw',
SELL = 'sell',
BUY = 'buy',
SELL_IF_DEFICIT = 'sell-if-deficit',
}

@Injectable()
Expand All @@ -35,12 +38,15 @@ export class ScryptAdapter extends LiquidityActionAdapter {
private readonly orderRepo: LiquidityManagementOrderRepository,
private readonly pricingService: PricingService,
private readonly assetService: AssetService,
private readonly ruleRepo: LiquidityManagementRuleRepository,
private readonly balanceRepo: LiquidityBalanceRepository,
) {
super(LiquidityManagementSystem.SCRYPT);

this.commands.set(ScryptAdapterCommands.WITHDRAW, this.withdraw.bind(this));
this.commands.set(ScryptAdapterCommands.SELL, this.sell.bind(this));
this.commands.set(ScryptAdapterCommands.BUY, this.buy.bind(this));
this.commands.set(ScryptAdapterCommands.SELL_IF_DEFICIT, this.sellIfDeficit.bind(this));
}

async checkCompletion(order: LiquidityManagementOrder): Promise<boolean> {
Expand All @@ -54,6 +60,9 @@ export class ScryptAdapter extends LiquidityActionAdapter {
case ScryptAdapterCommands.BUY:
return this.checkBuyCompletion(order);

case ScryptAdapterCommands.SELL_IF_DEFICIT:
return this.checkSellCompletion(order);

default:
return false;
}
Expand All @@ -68,6 +77,9 @@ export class ScryptAdapter extends LiquidityActionAdapter {
case ScryptAdapterCommands.BUY:
return this.validateTradeParams(params);

case ScryptAdapterCommands.SELL_IF_DEFICIT:
return this.validateSellIfDeficitParams(params);

default:
throw new Error(`Command ${command} not supported by ScryptAdapter`);
}
Expand Down Expand Up @@ -176,6 +188,61 @@ export class ScryptAdapter extends LiquidityActionAdapter {
}
}

private async sellIfDeficit(order: LiquidityManagementOrder): Promise<CorrelationId> {
const { tradeAsset, checkAssetId, maxPriceDeviation } = this.parseSellIfDeficitParams(order.action.paramMap);

// Check if the referenced asset has a deficit
const checkRule = await this.ruleRepo.findOneBy({ targetAsset: { id: checkAssetId } });
if (!checkRule) {
throw new OrderNotProcessableException(`No rule found for asset ${checkAssetId}`);
}

const checkBalance = await this.balanceRepo.findOneBy({ asset: { id: checkAssetId } });
if (!checkBalance || checkBalance.amount >= (checkRule.minimal ?? 0)) {
throw new OrderNotProcessableException(
`No deficit for asset ${checkAssetId} (balance: ${checkBalance?.amount}, minimal: ${checkRule.minimal})`,
);
}

// Calculate how much of the trade asset is needed to reach optimal
const deficitAmount = (checkRule.optimal ?? 0) - (checkBalance.amount ?? 0);
if (deficitAmount <= 0) {
throw new OrderNotProcessableException(`No deficit to optimal for asset ${checkAssetId}`);
}

// Get price and convert deficit to source asset amount
const targetAssetEntity = order.pipeline.rule.targetAsset;
const tradeAssetEntity = await this.assetService.getAssetByUniqueName(`Scrypt/${tradeAsset}`);

const price = await this.getAndCheckTradePrice(targetAssetEntity, tradeAssetEntity, maxPriceDeviation);
// price = tradeAsset per targetAsset (e.g., BTC per EUR)
const sellAmount = Util.floor(deficitAmount / price, 6);

// Cap by available balance and order limits
const availableBalance = await this.scryptService.getAvailableBalance(targetAssetEntity.dexName);
const amount = Util.floor(Math.min(sellAmount, order.maxAmount, availableBalance), 6);

if (amount <= 0) {
throw new OrderNotProcessableException(
`Scrypt: insufficient amount for sell-if-deficit (needed: ${sellAmount}, available: ${availableBalance})`,
);
}

order.inputAmount = amount;
order.inputAsset = targetAssetEntity.dexName;
order.outputAsset = tradeAsset;

try {
return await this.scryptService.sell(targetAssetEntity.dexName, tradeAsset, amount);
} catch (e) {
if (this.isBalanceTooLowError(e)) {
throw new OrderNotProcessableException(e.message);
}

throw e;
}
}

// --- COMPLETION CHECKS --- //

private async checkWithdrawCompletion(order: LiquidityManagementOrder): Promise<boolean> {
Expand Down Expand Up @@ -325,6 +392,30 @@ export class ScryptAdapter extends LiquidityActionAdapter {
return { tradeAsset, maxPriceDeviation };
}

private validateSellIfDeficitParams(params: Record<string, unknown>): boolean {
try {
this.parseSellIfDeficitParams(params);
return true;
} catch {
return false;
}
}

private parseSellIfDeficitParams(params: Record<string, unknown>): {
tradeAsset: string;
checkAssetId: number;
maxPriceDeviation?: number;
} {
const { tradeAsset, maxPriceDeviation } = this.parseTradeParams(params);
const checkAssetId = params.checkAssetId as number | undefined;

if (!checkAssetId) {
throw new Error('Params provided to ScryptAdapter sell-if-deficit command are missing checkAssetId.');
}

return { tradeAsset, checkAssetId, maxPriceDeviation };
}

// --- HELPER METHODS --- //

private isBalanceTooLowError(e: Error): boolean {
Expand Down
Loading