diff --git a/src/integration/exchange/services/__tests__/exchange.service.spec.ts b/src/integration/exchange/services/__tests__/exchange.service.spec.ts index afad4e83a0..ac0492b893 100644 --- a/src/integration/exchange/services/__tests__/exchange.service.spec.ts +++ b/src/integration/exchange/services/__tests__/exchange.service.spec.ts @@ -39,6 +39,30 @@ describe('ExchangeService', () => { expect(service).toBeDefined(); }); + it('should not be configured without credentials', () => { + expect(service.isConfigured).toBe(false); + }); + + it('should not be configured with only an apiKey', () => { + service = new ExchangeTestModule.TestExchangeService( + ExchangeTestModule.TestExchange, + { apiKey: 'key' }, + new QueueHandler(undefined, undefined), + ); + + expect(service.isConfigured).toBe(false); + }); + + it('should be configured with apiKey and secret', () => { + service = new ExchangeTestModule.TestExchangeService( + ExchangeTestModule.TestExchange, + { apiKey: 'key', secret: 'secret' }, + new QueueHandler(undefined, undefined), + ); + + expect(service.isConfigured).toBe(true); + }); + it('should return BTC/EUR and buy', async () => { Setup.Markets(); diff --git a/src/integration/exchange/services/exchange.service.ts b/src/integration/exchange/services/exchange.service.ts index de51c0570b..14d5310004 100644 --- a/src/integration/exchange/services/exchange.service.ts +++ b/src/integration/exchange/services/exchange.service.ts @@ -68,6 +68,11 @@ export abstract class ExchangeService extends PricingProvider implements OnModul return this.exchange.name; } + // true only when both ccxt credentials are present; used to skip exchanges with no API keys (e.g. XT on dev) + get isConfigured(): boolean { + return !!this.config?.apiKey && !!this.config?.secret; + } + async getRawBalances(): Promise { return this.callApi((e) => e.fetchBalance()); } diff --git a/src/integration/exchange/services/scrypt.service.ts b/src/integration/exchange/services/scrypt.service.ts index 26fbac2c85..637caac598 100644 --- a/src/integration/exchange/services/scrypt.service.ts +++ b/src/integration/exchange/services/scrypt.service.ts @@ -41,6 +41,11 @@ export class ScryptService extends PricingProvider { readonly name: string = 'Scrypt'; + get isConfigured(): boolean { + const { apiKey, apiSecret } = GetConfig().scrypt; + return !!apiKey && !!apiSecret; + } + constructor() { super(); 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 cd99d63f98..4764b6cef8 100644 --- a/src/subdomains/core/liquidity-management/adapters/balances/exchange.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/balances/exchange.adapter.ts @@ -15,6 +15,9 @@ export class ExchangeAdapter implements LiquidityBalanceIntegration { private readonly ASSET_MAPPINGS = { BTC: ['XBT'] }; + // exchanges already warned about missing credentials, to warn once instead of every cycle + private readonly unconfiguredWarned = new Set(); + constructor( private readonly exchangeRegistry: ExchangeRegistryService, private readonly orderRepo: LiquidityManagementOrderRepository, @@ -57,8 +60,19 @@ export class ExchangeAdapter implements LiquidityBalanceIntegration { // --- HELPER METHODS --- // async getForExchange(exchange: string, assets: LiquidityManagementAsset[]): Promise { + const exchangeService = this.exchangeRegistry.getExchange(exchange); + + // not configured (no API credentials) -> skip gracefully, warn once + if (!exchangeService.isConfigured) { + if (!this.unconfiguredWarned.has(exchange)) { + this.unconfiguredWarned.add(exchange); + this.logger.warn(`Exchange ${exchange} has no credentials configured — skipping liquidity balance`); + } + + return assets.map((a) => LiquidityBalance.create(a, 0, 0)); + } + try { - const exchangeService = this.exchangeRegistry.getExchange(exchange); const { total: totalBalances, available: availableBalances } = await exchangeService.getBalances(); return assets.map((a) => {