diff --git a/migration/1774000000000-AddAvailableAmountToLiquidityBalance.js b/migration/1774000000000-AddAvailableAmountToLiquidityBalance.js new file mode 100644 index 0000000000..3e8a3cc100 --- /dev/null +++ b/migration/1774000000000-AddAvailableAmountToLiquidityBalance.js @@ -0,0 +1,11 @@ +module.exports = class AddAvailableAmountToLiquidityBalance1774000000000 { + name = 'AddAvailableAmountToLiquidityBalance1774000000000'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "dbo"."liquidity_balance" ADD "availableAmount" float`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "dbo"."liquidity_balance" DROP COLUMN "availableAmount"`); + } +}; diff --git a/src/integration/exchange/controllers/exchange.controller.ts b/src/integration/exchange/controllers/exchange.controller.ts index 905eaa036b..c78d5bb5b5 100644 --- a/src/integration/exchange/controllers/exchange.controller.ts +++ b/src/integration/exchange/controllers/exchange.controller.ts @@ -50,7 +50,7 @@ export class ExchangeController { @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.ADMIN), UserActiveGuard()) async getBalance(@Param('exchange') exchange: string): Promise { - return this.call(exchange, (e) => e.getBalances()); + return this.call(exchange, (e) => e.getRawBalances()); } @Get(':exchange/price') diff --git a/src/integration/exchange/services/exchange.service.ts b/src/integration/exchange/services/exchange.service.ts index 3e988598a2..de51c0570b 100644 --- a/src/integration/exchange/services/exchange.service.ts +++ b/src/integration/exchange/services/exchange.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Inject, OnModuleInit } from '@nestjs/common'; import BigNumber from 'bignumber.js'; import { + Balance, Balances, ConstructorArgs, Dictionary, @@ -67,24 +68,30 @@ export abstract class ExchangeService extends PricingProvider implements OnModul return this.exchange.name; } - async getBalances(): Promise { + async getRawBalances(): Promise { return this.callApi((e) => e.fetchBalance()); } - async getTotalBalances(): Promise> { - const balances = await this.getBalances().then((b) => b.total); + async getBalances(): Promise<{ total: Dictionary; available: Dictionary }> { + const balances = await this.getRawBalances(); - const totalBalances = {}; + return { + total: this.aggregateBalances(balances.total), + available: this.aggregateBalances(balances.free), + }; + } + + private aggregateBalances(balances: Balance): Dictionary { + const result: Dictionary = {}; for (const [asset, amount] of Object.entries(balances)) { const [base, suffix] = asset.split('.'); - if (!suffix || suffix === 'F') totalBalances[base] = (totalBalances[base] ?? 0) + amount; + if (!suffix || suffix === 'F') result[base] = (result[base] ?? 0) + (amount as number); } - - return totalBalances; + return result; } async getAvailableBalance(currency: string): Promise { - return this.getBalances().then((b) => b.free[currency] ?? 0); + return this.getRawBalances().then((b) => b.free[currency] ?? 0); } async getPrice(from: string, to: string): Promise { diff --git a/src/integration/exchange/services/scrypt.service.ts b/src/integration/exchange/services/scrypt.service.ts index a39e593346..7ad4ad06fa 100644 --- a/src/integration/exchange/services/scrypt.service.ts +++ b/src/integration/exchange/services/scrypt.service.ts @@ -70,22 +70,31 @@ export class ScryptService extends PricingProvider { // --- BALANCES --- // - async getTotalBalances(): Promise> { + async getBalances(): Promise<{ total: Record; available: Record }> { const balances = await this.balances; - const totalBalances: Record = {}; + const total: Record = {}; + const available: Record = {}; + for (const balance of balances.values()) { - totalBalances[balance.Currency] = parseFloat(balance.Amount) || 0; + const amount = parseFloat(balance.Amount) || 0; + const availableAmount = parseFloat(balance.AvailableAmount) || amount; + + total[balance.Currency] = amount; + available[balance.Currency] = availableAmount; } - return totalBalances; + return { total, available }; } async getAvailableBalance(currency: string): Promise { const balances = await this.balances; const balance = balances.get(currency); - return balance ? parseFloat(balance.AvailableAmount) || 0 : 0; + if (!balance) return 0; + + const amount = parseFloat(balance.Amount) || 0; + return parseFloat(balance.AvailableAmount) || amount; } // --- WITHDRAWALS --- // diff --git a/src/subdomains/core/liquidity-management/adapters/balances/exchange.adapter.ts b/src/subdomains/core/liquidity-management/adapters/balances/exchange.adapter.ts index 4495afa0e0..cd99d63f98 100644 --- a/src/subdomains/core/liquidity-management/adapters/balances/exchange.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/balances/exchange.adapter.ts @@ -59,13 +59,14 @@ export class ExchangeAdapter implements LiquidityBalanceIntegration { async getForExchange(exchange: string, assets: LiquidityManagementAsset[]): Promise { try { const exchangeService = this.exchangeRegistry.getExchange(exchange); - const balances = await exchangeService.getTotalBalances(); + const { total: totalBalances, available: availableBalances } = await exchangeService.getBalances(); return assets.map((a) => { const names = [a.dexName, ...(this.ASSET_MAPPINGS[a.dexName] ?? [])]; - const balance = Util.sum(names.map((n) => balances[n] ?? 0)); + const total = Util.sum(names.map((n) => totalBalances[n] ?? 0)); + const available = Util.sum(names.map((n) => availableBalances[n] ?? 0)); - return LiquidityBalance.create(a, balance); + return LiquidityBalance.create(a, total, available); }); } catch (e) { this.logger.error(`Failed to update liquidity management balance for ${exchange}:`, e); diff --git a/src/subdomains/core/liquidity-management/entities/liquidity-balance.entity.ts b/src/subdomains/core/liquidity-management/entities/liquidity-balance.entity.ts index 292ab7d049..6cbd03c33a 100644 --- a/src/subdomains/core/liquidity-management/entities/liquidity-balance.entity.ts +++ b/src/subdomains/core/liquidity-management/entities/liquidity-balance.entity.ts @@ -10,24 +10,29 @@ export class LiquidityBalance extends IEntity { @Column({ type: 'float', nullable: true }) amount?: number; + @Column({ type: 'float', nullable: true }) + availableAmount?: number; + @Column({ default: true }) isDfxOwned: boolean; // --- FACTORY METHODS --- // - static create(target: Asset, amount: number): LiquidityBalance { + static create(target: Asset, amount: number, availableAmount?: number): LiquidityBalance { const balance = new LiquidityBalance(); balance.asset = target; balance.amount = amount; + balance.availableAmount = availableAmount ?? amount; return balance; } // --- PUBLIC API --- // - updateBalance(amount: number): this { + updateBalance(amount: number, availableAmount?: number): this { this.amount = amount; + this.availableAmount = availableAmount ?? amount; return this; } diff --git a/src/subdomains/core/liquidity-management/services/liquidity-management-balance.service.ts b/src/subdomains/core/liquidity-management/services/liquidity-management-balance.service.ts index dbcb75f774..ee6341bac9 100644 --- a/src/subdomains/core/liquidity-management/services/liquidity-management-balance.service.ts +++ b/src/subdomains/core/liquidity-management/services/liquidity-management-balance.service.ts @@ -86,7 +86,7 @@ export class LiquidityManagementBalanceService implements OnModuleInit { if (existingBalance) { if (existingBalance.updated > startDate) continue; - existingBalance.updateBalance(balance.amount ?? 0); + existingBalance.updateBalance(balance.amount ?? 0, balance.availableAmount); await this.balanceRepo.save(existingBalance); continue; diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index 089c5fbe6b..b5ec2bd1ca 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -452,8 +452,8 @@ export class LogJobService { const manualLiqPosition = manualLiqPositions.find((p) => p.assetId === curr.id)?.value ?? 0; - // plus - const liquidity = (curr.balance?.amount ?? 0) + (paymentDepositBalance ?? 0) + (manualLiqPosition ?? 0); + // plus (use availableAmount to avoid double-counting with pending exchange orders) + const liquidity = (curr.balance?.availableAmount ?? 0) + (paymentDepositBalance ?? 0) + (manualLiqPosition ?? 0); const cryptoInput = [Blockchain.MONERO, Blockchain.LIGHTNING, Blockchain.ZANO].includes(curr.blockchain) ? 0