From eb96af20438a88f7554db139562b6e53e02d85da Mon Sep 17 00:00:00 2001 From: David May Date: Thu, 15 Jan 2026 15:58:32 +0100 Subject: [PATCH 1/8] feat: added scrypt exchange tx sync + log integration --- .../exchange/entities/exchange-tx.entity.ts | 1 + .../services/exchange-registry.service.ts | 11 ++- .../exchange/services/exchange-tx.service.ts | 42 ++++++------ .../exchange/services/scrypt.service.ts | 55 ++++++++++++++- .../adapters/balances/exchange.adapter.ts | 6 +- .../bank-tx/entities/bank-tx.entity.ts | 2 + .../bank-tx/services/bank-tx.service.ts | 5 ++ src/subdomains/supporting/log/dto/log.dto.ts | 2 + .../supporting/log/log-job.service.ts | 68 ++++++++++++++++++- .../payment/entities/transaction.entity.ts | 1 + 10 files changed, 165 insertions(+), 28 deletions(-) diff --git a/src/integration/exchange/entities/exchange-tx.entity.ts b/src/integration/exchange/entities/exchange-tx.entity.ts index cc1541c066..2566b6ae8e 100644 --- a/src/integration/exchange/entities/exchange-tx.entity.ts +++ b/src/integration/exchange/entities/exchange-tx.entity.ts @@ -137,4 +137,5 @@ export const ExchangeSyncs: ExchangeSync[] = [ tokenReplacements: [], }, { exchange: ExchangeName.BINANCE, tradeTokens: ['BTC', 'USDT'], tokenReplacements: [['BTCB', 'BTC']] }, + { exchange: ExchangeName.SCRYPT, tokens: ['CHF', 'EUR'], tokenReplacements: [] }, ]; diff --git a/src/integration/exchange/services/exchange-registry.service.ts b/src/integration/exchange/services/exchange-registry.service.ts index e7b2f8c9a3..0d48925e69 100644 --- a/src/integration/exchange/services/exchange-registry.service.ts +++ b/src/integration/exchange/services/exchange-registry.service.ts @@ -1,10 +1,19 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable, forwardRef } from '@nestjs/common'; import { StrategyRegistry } from 'src/subdomains/supporting/common/strategy-registry'; +import { ExchangeName } from '../enums/exchange.enum'; import { ExchangeService } from './exchange.service'; +import { ScryptService } from './scrypt.service'; @Injectable() export class ExchangeRegistryService extends StrategyRegistry { + @Inject(forwardRef(() => ScryptService)) + private readonly scryptService: ScryptService; + protected getKey(key: string): string { return key.toLowerCase(); } + + getExchange(exchange: string): ExchangeService | ScryptService { + return exchange === ExchangeName.SCRYPT ? this.scryptService : this.get(exchange); + } } diff --git a/src/integration/exchange/services/exchange-tx.service.ts b/src/integration/exchange/services/exchange-tx.service.ts index 10212b0ad7..36bf040fc4 100644 --- a/src/integration/exchange/services/exchange-tx.service.ts +++ b/src/integration/exchange/services/exchange-tx.service.ts @@ -121,7 +121,7 @@ export class ExchangeTxService { private async getTransactionsFor(sync: ExchangeSync, since: Date): Promise { try { - const exchangeService = this.registryService.get(sync.exchange); + const exchangeService = this.registryService.getExchange(sync.exchange); const tokens = sync.tokens ?? (await this.assetService.getAssetsUsedOn(sync.exchange)); @@ -149,25 +149,27 @@ export class ExchangeTxService { ); } - // trades - const tradePairs = sync.tradeTokens - ? sync.tradeTokens - .reduce((prev, curr) => { - prev.push(tokens.filter((t) => t !== curr).map((t) => [curr, t])); - return prev; - }, []) - .flat(1) - .filter((p, i, l) => l.findIndex((p1) => p.every((t) => p1.includes(t))) === i) - : [[undefined, undefined]]; - - for (const [from, to] of tradePairs) { - try { - const txs = await exchangeService - .getTrades(from, to, since) - .then((t) => ExchangeTxMapper.mapTrades(t, sync.exchange)); - transactions.push(...txs); - } catch (e) { - if (!e.message?.includes('not supported')) throw e; + // trades (only if exchange service supports it) + if (exchangeService.getTrades) { + const tradePairs = sync.tradeTokens + ? sync.tradeTokens + .reduce((prev, curr) => { + prev.push(tokens.filter((t) => t !== curr).map((t) => [curr, t])); + return prev; + }, []) + .flat(1) + .filter((p, i, l) => l.findIndex((p1) => p.every((t) => p1.includes(t))) === i) + : [[undefined, undefined]]; + + for (const [from, to] of tradePairs) { + try { + const txs = await exchangeService + .getTrades(from, to, since) + .then((t) => ExchangeTxMapper.mapTrades(t, sync.exchange)); + transactions.push(...txs); + } catch (e) { + if (!e.message?.includes('not supported')) throw e; + } } } diff --git a/src/integration/exchange/services/scrypt.service.ts b/src/integration/exchange/services/scrypt.service.ts index cdfaa35d9a..a90327af66 100644 --- a/src/integration/exchange/services/scrypt.service.ts +++ b/src/integration/exchange/services/scrypt.service.ts @@ -1,5 +1,6 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotImplementedException } from '@nestjs/common'; import { randomUUID } from 'crypto'; +import { Trade, Transaction } from 'ccxt'; import { GetConfig } from 'src/config/config'; import { ScryptMessageType, ScryptWebSocketConnection } from './scrypt-websocket-connection'; @@ -152,8 +153,60 @@ export class ScryptService { }; } + // --- TRANSACTION SYNC (for ExchangeSyncs) --- // + + async getDeposits(_token: string, since?: Date, _chain?: string): Promise { + const transactions = await this.fetchBalanceTransactions(); + return transactions + .filter((t) => t.TransactionType === 'Deposit') + .filter((t) => !since || (t.TransactTime && new Date(t.TransactTime) >= since)) + .map((t) => this.mapToCcxtTransaction(t, 'deposit')); + } + + async getWithdrawals(_token: string, since?: Date): Promise { + const transactions = await this.fetchBalanceTransactions(); + return transactions + .filter((t) => t.TransactionType === 'Withdrawal') + .filter((t) => !since || (t.TransactTime && new Date(t.TransactTime) >= since)) + .map((t) => this.mapToCcxtTransaction(t, 'withdrawal')); + } + + private mapToCcxtTransaction(t: ScryptBalanceTransaction, type: 'deposit' | 'withdrawal'): Transaction { + return { + id: t.TransactionID, + txid: t.TxHash, + type, + currency: t.Currency, + amount: parseFloat(t.Quantity) || 0, + status: this.mapScryptStatus(t.Status), + datetime: t.TransactTime, + timestamp: t.TransactTime ? new Date(t.TransactTime).getTime() : undefined, + updated: t.Timestamp ? new Date(t.Timestamp).getTime() : undefined, + fee: t.Fee ? { cost: parseFloat(t.Fee), currency: t.Currency } : undefined, + info: { method: 'Bank Transfer', asset: t.Currency }, + } as Transaction; + } + + private mapScryptStatus(status: ScryptTransactionStatus): string { + switch (status) { + case ScryptTransactionStatus.COMPLETE: + return 'ok'; + case ScryptTransactionStatus.FAILED: + case ScryptTransactionStatus.REJECTED: + return 'failed'; + default: + return 'pending'; + } + } + private async fetchBalanceTransactions(): Promise { const data = await this.connection.fetch(ScryptMessageType.BALANCE_TRANSACTION); return data as ScryptBalanceTransaction[]; } + + // --- NOT IMPLEMENTED (stubs for ExchangeService compatibility) --- // + + async getTrades(_from?: string, _to?: string, _since?: Date): Promise { + throw new NotImplementedException('getTrades is not supported by Scrypt'); + } } 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 18d8768818..4495afa0e0 100644 --- a/src/subdomains/core/liquidity-management/adapters/balances/exchange.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/balances/exchange.adapter.ts @@ -1,7 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { ExchangeName } from 'src/integration/exchange/enums/exchange.enum'; import { ExchangeRegistryService } from 'src/integration/exchange/services/exchange-registry.service'; -import { ScryptService } from 'src/integration/exchange/services/scrypt.service'; import { Active } from 'src/shared/models/active'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { Util } from 'src/shared/utils/util'; @@ -19,7 +17,6 @@ export class ExchangeAdapter implements LiquidityBalanceIntegration { constructor( private readonly exchangeRegistry: ExchangeRegistryService, - private readonly scryptService: ScryptService, private readonly orderRepo: LiquidityManagementOrderRepository, ) {} @@ -61,8 +58,7 @@ export class ExchangeAdapter implements LiquidityBalanceIntegration { async getForExchange(exchange: string, assets: LiquidityManagementAsset[]): Promise { try { - const exchangeService = - exchange === ExchangeName.SCRYPT ? this.scryptService : this.exchangeRegistry.get(exchange); + const exchangeService = this.exchangeRegistry.getExchange(exchange); const balances = await exchangeService.getTotalBalances(); return assets.map((a) => { diff --git a/src/subdomains/supporting/bank-tx/bank-tx/entities/bank-tx.entity.ts b/src/subdomains/supporting/bank-tx/bank-tx/entities/bank-tx.entity.ts index 54994ae65c..e2a31ac705 100644 --- a/src/subdomains/supporting/bank-tx/bank-tx/entities/bank-tx.entity.ts +++ b/src/subdomains/supporting/bank-tx/bank-tx/entities/bank-tx.entity.ts @@ -34,6 +34,7 @@ export enum BankTxType { TEST_FIAT_FIAT = 'TestFiatFiat', GSHEET = 'GSheet', KRAKEN = 'Kraken', + SCRYPT = 'Scrypt', SCB = 'SCB', CHECKOUT_LTD = 'CheckoutLtd', BANK_ACCOUNT_FEE = 'BankAccountFee', @@ -395,6 +396,7 @@ export class BankTx extends IEntity { : 0; case BankTxType.KRAKEN: + case BankTxType.SCRYPT: if ( !BankService.isBankMatching(asset, targetIban ?? this.accountIban) || (targetIban && asset.dexName !== this.instructedCurrency) diff --git a/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts b/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts index e92b81fddc..307ee6dcb9 100644 --- a/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts +++ b/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts @@ -70,6 +70,7 @@ export const TransactionBankTxTypeMapper: { [BankTxType.BANK_TX_REPEAT_CHARGEBACK]: TransactionTypeInternal.BANK_TX_REPEAT_CHARGEBACK, [BankTxType.FIAT_FIAT]: TransactionTypeInternal.FIAT_FIAT, [BankTxType.KRAKEN]: TransactionTypeInternal.KRAKEN, + [BankTxType.SCRYPT]: TransactionTypeInternal.SCRYPT, [BankTxType.SCB]: TransactionTypeInternal.SCB, [BankTxType.CHECKOUT_LTD]: TransactionTypeInternal.CHECKOUT_LTD, [BankTxType.BANK_ACCOUNT_FEE]: TransactionTypeInternal.BANK_ACCOUNT_FEE, @@ -492,6 +493,10 @@ export class BankTxService implements OnModuleInit { return BankTxType.KRAKEN; } + if (tx.name?.includes('Scrypt Digital Trading')) { + return BankTxType.SCRYPT; + } + return null; } diff --git a/src/subdomains/supporting/log/dto/log.dto.ts b/src/subdomains/supporting/log/dto/log.dto.ts index 2046f95f43..c8be3c0b84 100644 --- a/src/subdomains/supporting/log/dto/log.dto.ts +++ b/src/subdomains/supporting/log/dto/log.dto.ts @@ -58,6 +58,7 @@ export type ManualLogPosition = { export type LogPairId = { fromKraken: { eur: PairId; chf: PairId }; toKraken: { eur: PairId; chf: PairId }; + toScrypt?: { eur: PairId; chf: PairId }; }; type PairId = { @@ -108,6 +109,7 @@ type AssetLogPlusPending = { fromOlky?: number; fromKraken?: number; toKraken?: number; + toScrypt?: number; }; type AssetLogMinusPending = { diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index aabb57d1c6..e87745b308 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -298,6 +298,12 @@ export class LogJobService { ExchangeName.KRAKEN, [ExchangeTxType.DEPOSIT, ExchangeTxType.WITHDRAWAL], ); + const recentScryptBankTx = await this.bankTxService.getRecentExchangeTx(undefined, BankTxType.SCRYPT); + const recentScryptExchangeTx = await this.exchangeTxService.getRecentExchangeTx( + undefined, + ExchangeName.SCRYPT, + [ExchangeTxType.DEPOSIT], + ); // fixed sender and receiver data @@ -366,6 +372,32 @@ export class LogJobService { eurReceiverExchangeTx, ); + // CHF: Yapeal -> Scrypt + const chfSenderScryptBankTx = recentScryptBankTx.filter( + (b) => b.accountIban === yapealChfBank.iban && b.creditDebitIndicator === BankTxIndicator.DEBIT, + ); + const chfReceiverScryptExchangeTx = recentScryptExchangeTx.filter( + (k) => k.type === ExchangeTxType.DEPOSIT && k.status === 'ok' && k.currency === 'CHF', + ); + + // EUR: Yapeal -> Scrypt + const eurSenderScryptBankTx = recentScryptBankTx.filter( + (b) => b.accountIban === yapealEurBank.iban && b.creditDebitIndicator === BankTxIndicator.DEBIT, + ); + const eurReceiverScryptExchangeTx = recentScryptExchangeTx.filter( + (k) => k.type === ExchangeTxType.DEPOSIT && k.status === 'ok' && k.currency === 'EUR', + ); + + // sender and receiver data for Scrypt + const { sender: recentChfYapealScryptTx, receiver: recentChfBankTxScrypt } = this.filterSenderPendingList( + chfSenderScryptBankTx, + chfReceiverScryptExchangeTx, + ); + const { sender: recentEurYapealScryptTx, receiver: recentEurBankTxScrypt } = this.filterSenderPendingList( + eurSenderScryptBankTx, + eurReceiverScryptExchangeTx, + ); + // assetLog return assets.reduce((prev, curr) => { if ((curr.balance?.amount == null && !curr.isActive) || (curr.balance && !curr.balance.isDfxOwned)) return prev; @@ -506,6 +538,28 @@ export class LogJobService { let toKraken = pendingYapealKrakenPlusAmount + pendingChfYapealKrakenMinusAmount + pendingEurYapealKrakenMinusAmount; + // Yapeal to Scrypt + const pendingYapealScryptPlusAmount = this.getPendingBankAmount( + [curr], + [...recentChfYapealScryptTx, ...recentEurYapealScryptTx], + BankTxType.SCRYPT, + ); + const pendingChfYapealScryptMinusAmount = this.getPendingBankAmount( + [curr], + recentChfBankTxScrypt, + ExchangeTxType.DEPOSIT, + yapealChfBank.iban, + ); + const pendingEurYapealScryptMinusAmount = this.getPendingBankAmount( + [curr], + recentEurBankTxScrypt, + ExchangeTxType.DEPOSIT, + yapealEurBank.iban, + ); + + let toScrypt = + pendingYapealScryptPlusAmount + pendingChfYapealScryptMinusAmount + pendingEurYapealScryptMinusAmount; + const errors = []; if (fromKraken !== fromKrakenUnfiltered) { @@ -539,6 +593,16 @@ export class LogJobService { toKraken = 0; } + if (toScrypt < 0) { + errors.push(`toScrypt < 0`); + this.logger.verbose( + `Error in financial log, toScrypt balance < 0 for asset: ${curr.id}, pendingPlusAmount: + ${pendingYapealScryptPlusAmount}, pendingChfMinusAmount: ${pendingChfYapealScryptMinusAmount}, + pendingEurMinusAmount: ${pendingEurYapealScryptMinusAmount}`, + ); + toScrypt = 0; + } + // total pending balance const totalPlusPending = cryptoInput + @@ -546,7 +610,8 @@ export class LogJobService { bridgeOrder + pendingOlkyYapealAmount + (useUnfilteredTx ? fromKrakenUnfiltered : fromKraken) + - (useUnfilteredTx ? toKrakenUnfiltered : toKraken); + (useUnfilteredTx ? toKrakenUnfiltered : toKraken) + + toScrypt; const totalPlus = liquidity + totalPlusPending + (totalCustomBalance ?? 0); @@ -631,6 +696,7 @@ export class LogJobService { fromOlky: this.getJsonValue(pendingOlkyYapealAmount, amountType(curr)), fromKraken: this.getJsonValue(useUnfilteredTx ? fromKrakenUnfiltered : fromKraken, amountType(curr)), toKraken: this.getJsonValue(useUnfilteredTx ? toKrakenUnfiltered : toKraken, amountType(curr)), + toScrypt: this.getJsonValue(toScrypt, amountType(curr)), } : undefined, // monitoring: errors.length diff --git a/src/subdomains/supporting/payment/entities/transaction.entity.ts b/src/subdomains/supporting/payment/entities/transaction.entity.ts index d3b34946b8..7bee9def4a 100644 --- a/src/subdomains/supporting/payment/entities/transaction.entity.ts +++ b/src/subdomains/supporting/payment/entities/transaction.entity.ts @@ -26,6 +26,7 @@ export enum TransactionTypeInternal { FIAT_FIAT = 'FiatFiat', INTERNAL = 'Internal', KRAKEN = 'Kraken', + SCRYPT = 'Scrypt', BANK_TX_RETURN = 'BankTxReturn', BANK_TX_REPEAT = 'BankTxRepeat', CRYPTO_INPUT_RETURN = 'CryptoInputReturn', From 9eda128c0efd85542ca38f615e416fa091ec8e90 Mon Sep 17 00:00:00 2001 From: David May Date: Thu, 15 Jan 2026 17:05:02 +0100 Subject: [PATCH 2/8] fix: fix formatting --- .../exchange/services/exchange-registry.service.ts | 5 ++--- src/subdomains/supporting/log/log-job.service.ts | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/integration/exchange/services/exchange-registry.service.ts b/src/integration/exchange/services/exchange-registry.service.ts index 0d48925e69..ccdce3e606 100644 --- a/src/integration/exchange/services/exchange-registry.service.ts +++ b/src/integration/exchange/services/exchange-registry.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, forwardRef } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { StrategyRegistry } from 'src/subdomains/supporting/common/strategy-registry'; import { ExchangeName } from '../enums/exchange.enum'; import { ExchangeService } from './exchange.service'; @@ -6,8 +6,7 @@ import { ScryptService } from './scrypt.service'; @Injectable() export class ExchangeRegistryService extends StrategyRegistry { - @Inject(forwardRef(() => ScryptService)) - private readonly scryptService: ScryptService; + @Inject() private readonly scryptService: ScryptService; protected getKey(key: string): string { return key.toLowerCase(); diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index e87745b308..953b5b1b41 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -299,11 +299,9 @@ export class LogJobService { [ExchangeTxType.DEPOSIT, ExchangeTxType.WITHDRAWAL], ); const recentScryptBankTx = await this.bankTxService.getRecentExchangeTx(undefined, BankTxType.SCRYPT); - const recentScryptExchangeTx = await this.exchangeTxService.getRecentExchangeTx( - undefined, - ExchangeName.SCRYPT, - [ExchangeTxType.DEPOSIT], - ); + const recentScryptExchangeTx = await this.exchangeTxService.getRecentExchangeTx(undefined, ExchangeName.SCRYPT, [ + ExchangeTxType.DEPOSIT, + ]); // fixed sender and receiver data From 46126f18551e678faf401ff8909ce6daa4991201 Mon Sep 17 00:00:00 2001 From: David May Date: Thu, 15 Jan 2026 17:40:20 +0100 Subject: [PATCH 3/8] feat: sync refactoring --- .../exchange/entities/exchange-tx.entity.ts | 2 +- .../exchange/mappers/exchange-tx.mapper.ts | 29 +++++++++ .../exchange/services/exchange-tx.service.ts | 47 +++++++------- .../exchange/services/scrypt.service.ts | 63 +++---------------- 4 files changed, 66 insertions(+), 75 deletions(-) diff --git a/src/integration/exchange/entities/exchange-tx.entity.ts b/src/integration/exchange/entities/exchange-tx.entity.ts index 2566b6ae8e..56ffd49931 100644 --- a/src/integration/exchange/entities/exchange-tx.entity.ts +++ b/src/integration/exchange/entities/exchange-tx.entity.ts @@ -137,5 +137,5 @@ export const ExchangeSyncs: ExchangeSync[] = [ tokenReplacements: [], }, { exchange: ExchangeName.BINANCE, tradeTokens: ['BTC', 'USDT'], tokenReplacements: [['BTCB', 'BTC']] }, - { exchange: ExchangeName.SCRYPT, tokens: ['CHF', 'EUR'], tokenReplacements: [] }, + { exchange: ExchangeName.SCRYPT, tokens: [], tokenReplacements: [] }, ]; diff --git a/src/integration/exchange/mappers/exchange-tx.mapper.ts b/src/integration/exchange/mappers/exchange-tx.mapper.ts index d845ea964a..3f55a71f40 100644 --- a/src/integration/exchange/mappers/exchange-tx.mapper.ts +++ b/src/integration/exchange/mappers/exchange-tx.mapper.ts @@ -2,6 +2,7 @@ import { Trade, Transaction } from 'ccxt'; import { ExchangeTxDto } from '../dto/exchange-tx.dto'; import { ExchangeTxType } from '../entities/exchange-tx.entity'; import { ExchangeName } from '../enums/exchange.enum'; +import { ScryptBalanceTransaction, ScryptTransactionStatus, ScryptTransactionType } from '../services/scrypt.service'; export class ExchangeTxMapper { static mapDeposits(transactions: Transaction[], exchange: ExchangeName): ExchangeTxDto[] { @@ -70,4 +71,32 @@ export class ExchangeTxMapper { side: t.side, })); } + + static mapScryptTransactions(transactions: ScryptBalanceTransaction[], exchange: ExchangeName): ExchangeTxDto[] { + return transactions.map((t) => ({ + exchange, + type: t.TransactionType === ScryptTransactionType.DEPOSIT ? ExchangeTxType.DEPOSIT : ExchangeTxType.WITHDRAWAL, + externalId: t.TransactionID, + externalCreated: t.TransactTime ? new Date(t.TransactTime) : new Date(), + externalUpdated: t.Timestamp ? new Date(t.Timestamp) : new Date(), + status: this.mapScryptStatus(t.Status), + amount: parseFloat(t.Quantity) || 0, + feeAmount: t.Fee ? parseFloat(t.Fee) : 0, + feeCurrency: t.Currency, + currency: t.Currency, + txId: t.TxHash, + })); + } + + private static mapScryptStatus(status: ScryptTransactionStatus): string { + switch (status) { + case ScryptTransactionStatus.COMPLETE: + return 'ok'; + case ScryptTransactionStatus.FAILED: + case ScryptTransactionStatus.REJECTED: + return 'failed'; + default: + return 'pending'; + } + } } diff --git a/src/integration/exchange/services/exchange-tx.service.ts b/src/integration/exchange/services/exchange-tx.service.ts index 36bf040fc4..706939ad15 100644 --- a/src/integration/exchange/services/exchange-tx.service.ts +++ b/src/integration/exchange/services/exchange-tx.service.ts @@ -19,6 +19,7 @@ import { ExchangeName } from '../enums/exchange.enum'; import { ExchangeTxMapper } from '../mappers/exchange-tx.mapper'; import { ExchangeTxRepository } from '../repositories/exchange-tx.repository'; import { ExchangeRegistryService } from './exchange-registry.service'; +import { ScryptService } from './scrypt.service'; @Injectable() export class ExchangeTxService { @@ -123,6 +124,12 @@ export class ExchangeTxService { try { const exchangeService = this.registryService.getExchange(sync.exchange); + // Scrypt special case + if (exchangeService instanceof ScryptService) { + const transactions = await exchangeService.getAllTransactions(since); + return ExchangeTxMapper.mapScryptTransactions(transactions, sync.exchange); + } + const tokens = sync.tokens ?? (await this.assetService.getAssetsUsedOn(sync.exchange)); // replace invalid tokens @@ -149,27 +156,25 @@ export class ExchangeTxService { ); } - // trades (only if exchange service supports it) - if (exchangeService.getTrades) { - const tradePairs = sync.tradeTokens - ? sync.tradeTokens - .reduce((prev, curr) => { - prev.push(tokens.filter((t) => t !== curr).map((t) => [curr, t])); - return prev; - }, []) - .flat(1) - .filter((p, i, l) => l.findIndex((p1) => p.every((t) => p1.includes(t))) === i) - : [[undefined, undefined]]; - - for (const [from, to] of tradePairs) { - try { - const txs = await exchangeService - .getTrades(from, to, since) - .then((t) => ExchangeTxMapper.mapTrades(t, sync.exchange)); - transactions.push(...txs); - } catch (e) { - if (!e.message?.includes('not supported')) throw e; - } + // trades + const tradePairs = sync.tradeTokens + ? sync.tradeTokens + .reduce((prev, curr) => { + prev.push(tokens.filter((t) => t !== curr).map((t) => [curr, t])); + return prev; + }, []) + .flat(1) + .filter((p, i, l) => l.findIndex((p1) => p.every((t) => p1.includes(t))) === i) + : [[undefined, undefined]]; + + for (const [from, to] of tradePairs) { + try { + const txs = await exchangeService + .getTrades(from, to, since) + .then((t) => ExchangeTxMapper.mapTrades(t, sync.exchange)); + transactions.push(...txs); + } catch (e) { + if (!e.message?.includes('not supported')) throw e; } } diff --git a/src/integration/exchange/services/scrypt.service.ts b/src/integration/exchange/services/scrypt.service.ts index a90327af66..e423882871 100644 --- a/src/integration/exchange/services/scrypt.service.ts +++ b/src/integration/exchange/services/scrypt.service.ts @@ -1,9 +1,13 @@ -import { Injectable, NotImplementedException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { randomUUID } from 'crypto'; -import { Trade, Transaction } from 'ccxt'; import { GetConfig } from 'src/config/config'; import { ScryptMessageType, ScryptWebSocketConnection } from './scrypt-websocket-connection'; +export enum ScryptTransactionType { + WITHDRAWAL = 'Withdrawal', + DEPOSIT = 'Deposit', +} + export enum ScryptTransactionStatus { COMPLETE = 'Complete', FAILED = 'Failed', @@ -21,11 +25,11 @@ interface ScryptBalance { }; } -interface ScryptBalanceTransaction { +export interface ScryptBalanceTransaction { TransactionID: string; ClReqID?: string; Currency: string; - TransactionType: string; + TransactionType: ScryptTransactionType; Status: ScryptTransactionStatus; Quantity: string; Fee?: string; @@ -153,60 +157,13 @@ export class ScryptService { }; } - // --- TRANSACTION SYNC (for ExchangeSyncs) --- // - - async getDeposits(_token: string, since?: Date, _chain?: string): Promise { - const transactions = await this.fetchBalanceTransactions(); - return transactions - .filter((t) => t.TransactionType === 'Deposit') - .filter((t) => !since || (t.TransactTime && new Date(t.TransactTime) >= since)) - .map((t) => this.mapToCcxtTransaction(t, 'deposit')); - } - - async getWithdrawals(_token: string, since?: Date): Promise { + async getAllTransactions(since?: Date): Promise { const transactions = await this.fetchBalanceTransactions(); - return transactions - .filter((t) => t.TransactionType === 'Withdrawal') - .filter((t) => !since || (t.TransactTime && new Date(t.TransactTime) >= since)) - .map((t) => this.mapToCcxtTransaction(t, 'withdrawal')); - } - - private mapToCcxtTransaction(t: ScryptBalanceTransaction, type: 'deposit' | 'withdrawal'): Transaction { - return { - id: t.TransactionID, - txid: t.TxHash, - type, - currency: t.Currency, - amount: parseFloat(t.Quantity) || 0, - status: this.mapScryptStatus(t.Status), - datetime: t.TransactTime, - timestamp: t.TransactTime ? new Date(t.TransactTime).getTime() : undefined, - updated: t.Timestamp ? new Date(t.Timestamp).getTime() : undefined, - fee: t.Fee ? { cost: parseFloat(t.Fee), currency: t.Currency } : undefined, - info: { method: 'Bank Transfer', asset: t.Currency }, - } as Transaction; - } - - private mapScryptStatus(status: ScryptTransactionStatus): string { - switch (status) { - case ScryptTransactionStatus.COMPLETE: - return 'ok'; - case ScryptTransactionStatus.FAILED: - case ScryptTransactionStatus.REJECTED: - return 'failed'; - default: - return 'pending'; - } + return transactions.filter((t) => !since || (t.TransactTime && new Date(t.TransactTime) >= since)); } private async fetchBalanceTransactions(): Promise { const data = await this.connection.fetch(ScryptMessageType.BALANCE_TRANSACTION); return data as ScryptBalanceTransaction[]; } - - // --- NOT IMPLEMENTED (stubs for ExchangeService compatibility) --- // - - async getTrades(_from?: string, _to?: string, _since?: Date): Promise { - throw new NotImplementedException('getTrades is not supported by Scrypt'); - } } From 9fc6e1d41642d82e3a5775623afa4713d3c3ae1c Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:14:29 +0100 Subject: [PATCH 4/8] feat(scrypt): add LIMIT orders with price tracking like Binance Implement full trading support for Scrypt exchange with the same rules as Binance/Kraken - using LIMIT orders with dynamic price tracking. ScryptWebSocketConnection: - Add MessageTypes: NEW_ORDER_SINGLE, EXECUTION_REPORT - Add MessageTypes: MARKET_DATA_SNAPSHOT, SECURITY - Add MessageTypes: ORDER_CANCEL_REQUEST, ORDER_CANCEL_REPLACE_REQUEST ScryptService: - Add trading enums: ScryptOrderStatus, ScryptOrderSide, ScryptOrderType - Add fetchOrderBook() - get orderbook with bids/offers - Add getCurrentPrice() - get best bid/ask price - Add getSecurityInfo() / getMinTradeAmount() - dynamic min amounts - Add placeOrder() with price parameter, default to LIMIT - Add sell() - places LIMIT order at current market price - Add cancelOrder() - cancel existing order - Add editOrder() - update order price (OrderCancelReplaceRequest) - Add getOrderStatus() with price field ScryptAdapter: - Add SELL command with tradeAsset parameter - Add checkSellCompletion() with Binance-like price tracking: - NEW/PARTIALLY_FILLED: compare price, editOrder() if changed - CANCELLED: auto-restart with remaining if >= minAmount - FILLED: aggregate output from all correlation IDs - Add aggregateSellOutput() for multi-order fills --- .../services/scrypt-websocket-connection.ts | 9 + .../exchange/services/scrypt.service.ts | 311 ++++++++++++++++++ .../adapters/actions/scrypt.adapter.ts | 174 +++++++++- 3 files changed, 493 insertions(+), 1 deletion(-) diff --git a/src/integration/exchange/services/scrypt-websocket-connection.ts b/src/integration/exchange/services/scrypt-websocket-connection.ts index ef1b220ea4..0f395ad097 100644 --- a/src/integration/exchange/services/scrypt-websocket-connection.ts +++ b/src/integration/exchange/services/scrypt-websocket-connection.ts @@ -23,6 +23,15 @@ export enum ScryptMessageType { BALANCE_TRANSACTION = 'BalanceTransaction', BALANCE = 'Balance', ERROR = 'error', + // Trading + NEW_ORDER_SINGLE = 'NewOrderSingle', + EXECUTION_REPORT = 'ExecutionReport', + // Market Data + MARKET_DATA_SNAPSHOT = 'MarketDataSnapshot', + SECURITY = 'Security', + // Order Management + ORDER_CANCEL_REQUEST = 'OrderCancelRequest', + ORDER_CANCEL_REPLACE_REQUEST = 'OrderCancelReplaceRequest', } enum ScryptRequestType { diff --git a/src/integration/exchange/services/scrypt.service.ts b/src/integration/exchange/services/scrypt.service.ts index cdfaa35d9a..2c45e4d43f 100644 --- a/src/integration/exchange/services/scrypt.service.ts +++ b/src/integration/exchange/services/scrypt.service.ts @@ -49,6 +49,106 @@ export interface ScryptWithdrawStatus { rejectText?: string; } +// --- TRADING TYPES --- // + +export enum ScryptOrderStatus { + NEW = 'New', + PARTIALLY_FILLED = 'PartiallyFilled', + FILLED = 'Filled', + CANCELLED = 'Cancelled', + REJECTED = 'Rejected', +} + +export enum ScryptOrderSide { + BUY = 'Buy', + SELL = 'Sell', +} + +export enum ScryptOrderType { + MARKET = 'Market', + LIMIT = 'Limit', +} + +export enum ScryptTimeInForce { + FILL_AND_KILL = 'FillAndKill', + FILL_OR_KILL = 'FillOrKill', + GOOD_TILL_CANCEL = 'GoodTillCancel', +} + +interface ScryptExecutionReport { + ClOrdID: string; + OrigClOrdID?: string; + OrderID?: string; + Symbol: string; + Side: string; + OrdStatus: ScryptOrderStatus; + ExecType?: string; + OrderQty: string; + CumQty: string; + LeavesQty: string; + AvgPx?: string; + Price?: string; + RejectReason?: string; + Text?: string; +} + +export interface ScryptOrderResponse { + id: string; + status: ScryptOrderStatus; +} + +export interface ScryptOrderInfo { + id: string; + orderId?: string; + symbol: string; + side: string; + status: ScryptOrderStatus; + quantity: number; + filledQuantity: number; + remainingQuantity: number; + avgPrice?: number; + price?: number; + rejectReason?: string; +} + +// --- MARKET DATA TYPES --- // + +interface ScryptPriceLevel { + Price: string; + Size: string; +} + +interface ScryptMarketDataSnapshot { + Timestamp: string; + Symbol: string; + Status: string; + Bids: ScryptPriceLevel[]; + Offers: ScryptPriceLevel[]; +} + +export interface ScryptOrderBook { + bids: Array<{ price: number; size: number }>; + offers: Array<{ price: number; size: number }>; +} + +// --- SECURITY TYPES --- // + +interface ScryptSecurity { + Symbol: string; + MinimumSize?: string; + MaximumSize?: string; + MinPriceIncrement?: string; + MinSizeIncrement?: string; +} + +export interface ScryptSecurityInfo { + symbol: string; + minSize: number; + maxSize: number; + minPriceIncrement: number; + minSizeIncrement: number; +} + @Injectable() export class ScryptService { private readonly connection: ScryptWebSocketConnection; @@ -156,4 +256,215 @@ export class ScryptService { const data = await this.connection.fetch(ScryptMessageType.BALANCE_TRANSACTION); return data as ScryptBalanceTransaction[]; } + + // --- MARKET DATA --- // + + async fetchOrderBook(symbol: string): Promise { + const data = await this.connection.fetch(ScryptMessageType.MARKET_DATA_SNAPSHOT, { Symbols: [symbol] }); + const snapshots = data as ScryptMarketDataSnapshot[]; + const snapshot = snapshots.find((s) => s.Symbol === symbol); + + if (!snapshot) { + throw new Error(`No orderbook data for symbol ${symbol}`); + } + + return { + bids: snapshot.Bids.map((b) => ({ price: parseFloat(b.Price), size: parseFloat(b.Size) })), + offers: snapshot.Offers.map((o) => ({ price: parseFloat(o.Price), size: parseFloat(o.Size) })), + }; + } + + async getCurrentPrice(symbol: string, side: ScryptOrderSide): Promise { + const orderBook = await this.fetchOrderBook(symbol); + + if (side === ScryptOrderSide.BUY) { + if (!orderBook.offers.length) throw new Error(`No offers available for ${symbol}`); + return orderBook.offers[0].price; // Best ask (lowest offer) + } else { + if (!orderBook.bids.length) throw new Error(`No bids available for ${symbol}`); + return orderBook.bids[0].price; // Best bid (highest bid) + } + } + + // --- SECURITY INFO --- // + + async getSecurityInfo(symbol: string): Promise { + const data = await this.connection.fetch(ScryptMessageType.SECURITY, { Symbols: [symbol] }); + const securities = data as ScryptSecurity[]; + const security = securities.find((s) => s.Symbol === symbol); + + if (!security) { + throw new Error(`No security info for symbol ${symbol}`); + } + + return { + symbol: security.Symbol, + minSize: parseFloat(security.MinimumSize ?? '0'), + maxSize: parseFloat(security.MaximumSize ?? '0'), + minPriceIncrement: parseFloat(security.MinPriceIncrement ?? '0'), + minSizeIncrement: parseFloat(security.MinSizeIncrement ?? '0'), + }; + } + + async getMinTradeAmount(symbol: string): Promise { + const info = await this.getSecurityInfo(symbol); + return info.minSize; + } + + // --- TRADING --- // + + async placeOrder( + symbol: string, + side: ScryptOrderSide, + quantity: number, + orderType: ScryptOrderType = ScryptOrderType.LIMIT, + timeInForce: ScryptTimeInForce = ScryptTimeInForce.GOOD_TILL_CANCEL, + price?: number, + ): Promise { + const clOrdId = randomUUID(); + + // Price is required for LIMIT orders + if (orderType === ScryptOrderType.LIMIT && price === undefined) { + throw new Error('Price is required for LIMIT orders'); + } + + const orderData: Record = { + Symbol: symbol, + ClOrdID: clOrdId, + Side: side, + OrderQty: quantity.toString(), + OrdType: orderType, + TimeInForce: timeInForce, + }; + + if (price !== undefined) { + orderData.Price = price.toString(); + } + + const orderRequest = { + type: ScryptMessageType.NEW_ORDER_SINGLE, + data: [orderData], + }; + + const report = await this.connection.requestAndWaitForUpdate( + orderRequest, + ScryptMessageType.EXECUTION_REPORT, + (data) => { + const reports = data as ScryptExecutionReport[]; + return reports.find((r) => r.ClOrdID === clOrdId) ?? null; + }, + 60000, + ); + + if (report.OrdStatus === ScryptOrderStatus.REJECTED) { + throw new Error(`Scrypt order rejected: ${report.Text ?? report.RejectReason ?? 'Unknown reason'}`); + } + + return { + id: clOrdId, + status: report.OrdStatus, + }; + } + + async sell(from: string, to: string, amount: number): Promise { + const symbol = `${from}/${to}`; + const price = await this.getCurrentPrice(symbol, ScryptOrderSide.SELL); + const response = await this.placeOrder( + symbol, + ScryptOrderSide.SELL, + amount, + ScryptOrderType.LIMIT, + ScryptTimeInForce.GOOD_TILL_CANCEL, + price, + ); + return response.id; + } + + async cancelOrder(clOrdId: string, symbol: string): Promise { + const origClOrdId = clOrdId; + const newClOrdId = randomUUID(); + + const cancelRequest = { + type: ScryptMessageType.ORDER_CANCEL_REQUEST, + data: [ + { + OrigClOrdID: origClOrdId, + ClOrdID: newClOrdId, + Symbol: symbol, + }, + ], + }; + + const report = await this.connection.requestAndWaitForUpdate( + cancelRequest, + ScryptMessageType.EXECUTION_REPORT, + (data) => { + const reports = data as ScryptExecutionReport[]; + return reports.find((r) => r.OrigClOrdID === origClOrdId || r.ClOrdID === newClOrdId) ?? null; + }, + 60000, + ); + + return report.OrdStatus === ScryptOrderStatus.CANCELLED; + } + + async editOrder(clOrdId: string, symbol: string, newQuantity: number, newPrice: number): Promise { + const origClOrdId = clOrdId; + const newClOrdId = randomUUID(); + + const replaceRequest = { + type: ScryptMessageType.ORDER_CANCEL_REPLACE_REQUEST, + data: [ + { + OrigClOrdID: origClOrdId, + ClOrdID: newClOrdId, + Symbol: symbol, + OrderQty: newQuantity.toString(), + Price: newPrice.toString(), + }, + ], + }; + + const report = await this.connection.requestAndWaitForUpdate( + replaceRequest, + ScryptMessageType.EXECUTION_REPORT, + (data) => { + const reports = data as ScryptExecutionReport[]; + return reports.find((r) => r.ClOrdID === newClOrdId) ?? null; + }, + 60000, + ); + + if (report.OrdStatus === ScryptOrderStatus.REJECTED) { + throw new Error(`Scrypt order edit rejected: ${report.Text ?? report.RejectReason ?? 'Unknown reason'}`); + } + + return newClOrdId; + } + + async getOrderStatus(clOrdId: string): Promise { + const reports = await this.fetchExecutionReports(); + const report = reports.find((r) => r.ClOrdID === clOrdId); + + if (!report) return null; + + return { + id: report.ClOrdID, + orderId: report.OrderID, + symbol: report.Symbol, + side: report.Side, + status: report.OrdStatus, + quantity: parseFloat(report.OrderQty) || 0, + filledQuantity: parseFloat(report.CumQty) || 0, + remainingQuantity: parseFloat(report.LeavesQty) || 0, + avgPrice: report.AvgPx ? parseFloat(report.AvgPx) : undefined, + price: report.Price ? parseFloat(report.Price) : undefined, + rejectReason: report.RejectReason ?? report.Text, + }; + } + + private async fetchExecutionReports(): Promise { + const data = await this.connection.fetch(ScryptMessageType.EXECUTION_REPORT); + return data as ScryptExecutionReport[]; + } } diff --git a/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts index cc4769df78..d8cd2f1cb8 100644 --- a/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts @@ -1,6 +1,11 @@ import { Injectable } from '@nestjs/common'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; -import { ScryptService, ScryptTransactionStatus } from 'src/integration/exchange/services/scrypt.service'; +import { + ScryptOrderSide, + ScryptOrderStatus, + ScryptService, + ScryptTransactionStatus, +} from 'src/integration/exchange/services/scrypt.service'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { Util } from 'src/shared/utils/util'; import { DexService } from 'src/subdomains/supporting/dex/services/dex.service'; @@ -9,10 +14,12 @@ 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 { LiquidityManagementOrderRepository } from '../../repositories/liquidity-management-order.repository'; import { LiquidityActionAdapter } from './base/liquidity-action.adapter'; export enum ScryptAdapterCommands { WITHDRAW = 'withdraw', + SELL = 'sell', } @Injectable() @@ -24,10 +31,12 @@ export class ScryptAdapter extends LiquidityActionAdapter { constructor( private readonly scryptService: ScryptService, private readonly dexService: DexService, + private readonly orderRepo: LiquidityManagementOrderRepository, ) { super(LiquidityManagementSystem.SCRYPT); this.commands.set(ScryptAdapterCommands.WITHDRAW, this.withdraw.bind(this)); + this.commands.set(ScryptAdapterCommands.SELL, this.sell.bind(this)); } async checkCompletion(order: LiquidityManagementOrder): Promise { @@ -35,6 +44,9 @@ export class ScryptAdapter extends LiquidityActionAdapter { case ScryptAdapterCommands.WITHDRAW: return this.checkWithdrawCompletion(order); + case ScryptAdapterCommands.SELL: + return this.checkSellCompletion(order); + default: return false; } @@ -45,6 +57,9 @@ export class ScryptAdapter extends LiquidityActionAdapter { case ScryptAdapterCommands.WITHDRAW: return this.validateWithdrawParams(params); + case ScryptAdapterCommands.SELL: + return this.validateSellParams(params); + default: throw new Error(`Command ${command} not supported by ScryptAdapter`); } @@ -83,6 +98,37 @@ export class ScryptAdapter extends LiquidityActionAdapter { } } + private async sell(order: LiquidityManagementOrder): Promise { + const { tradeAsset } = this.parseSellParams(order.action.paramMap); + + const asset = order.pipeline.rule.targetAsset.dexName; + + const availableBalance = await this.scryptService.getAvailableBalance(asset); + const effectiveMax = Math.min(order.maxAmount, availableBalance); + + if (effectiveMax < order.minAmount) { + throw new OrderNotProcessableException( + `Scrypt: not enough balance for ${asset} (balance: ${availableBalance}, min. requested: ${order.minAmount}, max. requested: ${order.maxAmount})`, + ); + } + + const amount = Util.floor(effectiveMax, 6); + + order.inputAmount = amount; + order.inputAsset = asset; + order.outputAsset = tradeAsset; + + try { + return await this.scryptService.sell(asset, tradeAsset, amount); + } catch (e) { + if (this.isBalanceTooLowError(e)) { + throw new OrderNotProcessableException(e.message); + } + + throw e; + } + } + // --- COMPLETION CHECKS --- // private async checkWithdrawCompletion(order: LiquidityManagementOrder): Promise { @@ -107,6 +153,111 @@ export class ScryptAdapter extends LiquidityActionAdapter { return this.dexService.checkTransferCompletion(withdrawal.txHash, blockchain); } + private async checkSellCompletion(order: LiquidityManagementOrder): Promise { + const { correlationId } = order; + const { tradeAsset } = this.parseSellParams(order.action.paramMap); + const asset = order.pipeline.rule.targetAsset.dexName; + const symbol = `${asset}/${tradeAsset}`; + + const orderInfo = await this.scryptService.getOrderStatus(correlationId); + if (!orderInfo) { + this.logger.verbose(`No order info for id ${correlationId} at ${this.scryptService.name} found`); + return false; + } + + switch (orderInfo.status) { + case ScryptOrderStatus.NEW: + case ScryptOrderStatus.PARTIALLY_FILLED: { + // Price tracking like Binance - update price if changed + const currentPrice = await this.scryptService.getCurrentPrice(symbol, ScryptOrderSide.SELL); + + if (orderInfo.price && currentPrice !== orderInfo.price) { + this.logger.verbose( + `Order ${correlationId}: price changed ${orderInfo.price} -> ${currentPrice}, updating order`, + ); + + try { + const newId = await this.scryptService.editOrder( + correlationId, + symbol, + orderInfo.remainingQuantity, + currentPrice, + ); + order.updateCorrelationId(newId); + await this.orderRepo.save(order); + this.logger.verbose(`Order ${correlationId} updated to ${newId} with new price ${currentPrice}`); + } catch (e) { + // If edit fails, try to cancel and let it restart + this.logger.verbose(`Could not update order ${correlationId}, attempting cancel: ${e.message}`); + try { + await this.scryptService.cancelOrder(correlationId, symbol); + } catch (cancelError) { + this.logger.verbose(`Cancel also failed: ${cancelError.message}`); + } + } + } + return false; + } + + case ScryptOrderStatus.CANCELLED: { + const minAmount = await this.scryptService.getMinTradeAmount(symbol); + const remaining = orderInfo.remainingQuantity; + + // If remaining amount is below minimum, consider complete + if (remaining < minAmount) { + this.logger.verbose( + `Order ${correlationId} cancelled with remaining ${remaining} < minAmount ${minAmount}, marking complete`, + ); + order.outputAmount = await this.aggregateSellOutput(order); + return true; + } + + // Restart order with remaining amount (like Binance) + this.logger.verbose(`Order ${correlationId} cancelled, restarting with remaining ${remaining} ${asset}`); + + try { + const newId = await this.scryptService.sell(asset, tradeAsset, remaining); + order.updateCorrelationId(newId); + await this.orderRepo.save(order); + this.logger.verbose(`Order ${correlationId} restarted as ${newId}`); + return false; + } catch (e) { + throw new OrderFailedException(`Order ${correlationId} cancelled and restart failed: ${e.message}`); + } + } + + case ScryptOrderStatus.FILLED: { + // Aggregate output from all correlation IDs (in case of restarts) + order.outputAmount = await this.aggregateSellOutput(order); + return true; + } + + case ScryptOrderStatus.REJECTED: + throw new OrderFailedException( + `Order ${correlationId} has been rejected: ${orderInfo.rejectReason ?? 'unknown reason'}`, + ); + + default: + return false; + } + } + + private async aggregateSellOutput(order: LiquidityManagementOrder): Promise { + const correlationIds = order.allCorrelationIds; + let totalOutput = 0; + + for (const id of correlationIds) { + const orderInfo = await this.scryptService.getOrderStatus(id); + if (orderInfo && orderInfo.filledQuantity > 0) { + // For SELL: output is the proceeds (filledQuantity * avgPrice) + const output = orderInfo.avgPrice ? orderInfo.filledQuantity * orderInfo.avgPrice : orderInfo.filledQuantity; + totalOutput += output; + } + } + + return totalOutput; + } + // --- PARAM VALIDATION --- // private validateWithdrawParams(params: Record): boolean { @@ -134,6 +285,27 @@ export class ScryptAdapter extends LiquidityActionAdapter { return { address, asset, blockchain }; } + private validateSellParams(params: Record): boolean { + try { + this.parseSellParams(params); + return true; + } catch { + return false; + } + } + + private parseSellParams(params: Record): { + tradeAsset: string; + } { + const tradeAsset = params.tradeAsset as string | undefined; + + if (!tradeAsset) { + throw new Error(`Params provided to ScryptAdapter.sell(...) command are invalid.`); + } + + return { tradeAsset }; + } + // --- HELPER METHODS --- // private isBalanceTooLowError(_e: Error): boolean { From 8c8c4fdbf9ee1b0a3c335e916bc614657bf7b1be Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:17:50 +0100 Subject: [PATCH 5/8] refactor(scrypt): improve logging and parallelize order fetching - Add verbose log when order price is unchanged (like Binance) - Parallelize aggregateSellOutput() with Promise.allSettled() - Add failure logging and error handling for partial fetch failures - Throw OrderFailedException if no orders can be fetched --- .../adapters/actions/scrypt.adapter.ts | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts index d8cd2f1cb8..eff18c1f7f 100644 --- a/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts @@ -195,6 +195,8 @@ export class ScryptAdapter extends LiquidityActionAdapter { this.logger.verbose(`Cancel also failed: ${cancelError.message}`); } } + } else { + this.logger.verbose(`Order ${correlationId} open, price is still ${currentPrice}`); } return false; } @@ -244,18 +246,38 @@ export class ScryptAdapter extends LiquidityActionAdapter { private async aggregateSellOutput(order: LiquidityManagementOrder): Promise { const correlationIds = order.allCorrelationIds; - let totalOutput = 0; - - for (const id of correlationIds) { - const orderInfo = await this.scryptService.getOrderStatus(id); - if (orderInfo && orderInfo.filledQuantity > 0) { - // For SELL: output is the proceeds (filledQuantity * avgPrice) - const output = orderInfo.avgPrice ? orderInfo.filledQuantity * orderInfo.avgPrice : orderInfo.filledQuantity; - totalOutput += output; - } + + // Fetch all orders in parallel like Binance + const orderResults = await Promise.allSettled( + correlationIds.map((id) => this.scryptService.getOrderStatus(id)), + ); + + const orders = orderResults + .filter((result): result is PromiseFulfilledResult>> => + result.status === 'fulfilled' && result.value !== null) + .map((result) => result.value!); + + // Log failures + const failures = orderResults.filter((result) => result.status === 'rejected'); + if (failures.length > 0) { + this.logger.warn( + `Order ${order.id}: Failed to fetch ${failures.length} of ${correlationIds.length} orders. ` + + `Proceeding with ${orders.length} successful fetches.`, + ); } - return totalOutput; + if (orders.length === 0) { + throw new OrderFailedException(`Failed to fetch any orders for order ${order.id}`); + } + + // For SELL: output is the proceeds (filledQuantity * avgPrice) + return orders.reduce((sum, o) => { + if (o.filledQuantity > 0) { + const output = o.avgPrice ? o.filledQuantity * o.avgPrice : o.filledQuantity; + return sum + output; + } + return sum; + }, 0); } // --- PARAM VALIDATION --- // From 7d1e7e5a6bada5d51f055692ab7ebc69b3a28856 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:23:48 +0100 Subject: [PATCH 6/8] feat(scrypt): add migration for EUR/CHF trading rules - Add ScryptTradingActions migration for rules 312 (CHF) and 313 (EUR) - Set maximal=1000 to trigger automatic USDT conversion --- .../1768494786424-ScryptTradingActions.js | 103 ++++++++++++++++++ .../adapters/actions/scrypt.adapter.ts | 10 +- 2 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 migration/1768494786424-ScryptTradingActions.js diff --git a/migration/1768494786424-ScryptTradingActions.js b/migration/1768494786424-ScryptTradingActions.js new file mode 100644 index 0000000000..5dcf7ff677 --- /dev/null +++ b/migration/1768494786424-ScryptTradingActions.js @@ -0,0 +1,103 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * Add Scrypt SELL actions for CHF->USDT and EUR->USDT trading + * and link them to existing rules 312 (CHF) and 313 (EUR) + * + * @class + * @implements {MigrationInterface} + */ +module.exports = class ScryptTradingActions1768494786424 { + name = 'ScryptTradingActions1768494786424' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + // Create Scrypt SELL action for CHF -> USDT + await queryRunner.query(` + INSERT INTO "dbo"."liquidity_management_action" ("system", "command", "params") + VALUES ('Scrypt', 'sell', '{"tradeAsset":"USDT"}') + `); + + // Get the ID of the newly created CHF action + const chfActionResult = await queryRunner.query(` + SELECT TOP 1 "id" FROM "dbo"."liquidity_management_action" + WHERE "system" = 'Scrypt' AND "command" = 'sell' + ORDER BY "id" DESC + `); + const chfActionId = chfActionResult[0].id; + + // Update Rule 312 (Scrypt CHF) with maximal=1000 and link to SELL action + await queryRunner.query(` + UPDATE "dbo"."liquidity_management_rule" + SET "minimal" = 0, + "optimal" = 0, + "maximal" = 1000, + "redundancyStartActionId" = ${chfActionId} + WHERE "id" = 312 + `); + + // Create Scrypt SELL action for EUR -> USDT (separate action for clarity) + await queryRunner.query(` + INSERT INTO "dbo"."liquidity_management_action" ("system", "command", "params") + VALUES ('Scrypt', 'sell', '{"tradeAsset":"USDT"}') + `); + + // Get the ID of the newly created EUR action + const eurActionResult = await queryRunner.query(` + SELECT TOP 1 "id" FROM "dbo"."liquidity_management_action" + WHERE "system" = 'Scrypt' AND "command" = 'sell' + ORDER BY "id" DESC + `); + const eurActionId = eurActionResult[0].id; + + // Update Rule 313 (Scrypt EUR) with maximal=1000 and link to SELL action + await queryRunner.query(` + UPDATE "dbo"."liquidity_management_rule" + SET "minimal" = 0, + "optimal" = 0, + "maximal" = 1000, + "redundancyStartActionId" = ${eurActionId} + WHERE "id" = 313 + `); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + // Get action IDs linked to rules 312 and 313 + const rule312 = await queryRunner.query(` + SELECT "redundancyStartActionId" FROM "dbo"."liquidity_management_rule" WHERE "id" = 312 + `); + const rule313 = await queryRunner.query(` + SELECT "redundancyStartActionId" FROM "dbo"."liquidity_management_rule" WHERE "id" = 313 + `); + + // Remove action links from rules and reset values + await queryRunner.query(` + UPDATE "dbo"."liquidity_management_rule" + SET "minimal" = NULL, + "optimal" = NULL, + "maximal" = NULL, + "redundancyStartActionId" = NULL + WHERE "id" IN (312, 313) + `); + + // Delete actions if they exist + if (rule312[0]?.redundancyStartActionId) { + await queryRunner.query(` + DELETE FROM "dbo"."liquidity_management_action" WHERE "id" = ${rule312[0].redundancyStartActionId} + `); + } + if (rule313[0]?.redundancyStartActionId && rule313[0].redundancyStartActionId !== rule312[0]?.redundancyStartActionId) { + await queryRunner.query(` + DELETE FROM "dbo"."liquidity_management_action" WHERE "id" = ${rule313[0].redundancyStartActionId} + `); + } + } +} diff --git a/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts index eff18c1f7f..8a87b3e932 100644 --- a/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts @@ -248,13 +248,13 @@ export class ScryptAdapter extends LiquidityActionAdapter { const correlationIds = order.allCorrelationIds; // Fetch all orders in parallel like Binance - const orderResults = await Promise.allSettled( - correlationIds.map((id) => this.scryptService.getOrderStatus(id)), - ); + const orderResults = await Promise.allSettled(correlationIds.map((id) => this.scryptService.getOrderStatus(id))); const orders = orderResults - .filter((result): result is PromiseFulfilledResult>> => - result.status === 'fulfilled' && result.value !== null) + .filter( + (result): result is PromiseFulfilledResult>> => + result.status === 'fulfilled' && result.value !== null, + ) .map((result) => result.value!); // Log failures From 46b1cb9b29f2d75173fa20117ae45657ee173663 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:36:59 +0100 Subject: [PATCH 7/8] fix(scrypt): implement isBalanceTooLowError and fix float comparison - Implement isBalanceTooLowError with common balance error messages - Add tolerance for float comparison in price tracking to avoid unnecessary order updates due to rounding errors --- .../adapters/actions/scrypt.adapter.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts index 8a87b3e932..295babb6ed 100644 --- a/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts @@ -171,7 +171,9 @@ export class ScryptAdapter extends LiquidityActionAdapter { // Price tracking like Binance - update price if changed const currentPrice = await this.scryptService.getCurrentPrice(symbol, ScryptOrderSide.SELL); - if (orderInfo.price && currentPrice !== orderInfo.price) { + // Use tolerance for float comparison to avoid unnecessary updates due to rounding + const priceChanged = orderInfo.price && Math.abs(currentPrice - orderInfo.price) > 0.000001; + if (priceChanged) { this.logger.verbose( `Order ${correlationId}: price changed ${orderInfo.price} -> ${currentPrice}, updating order`, ); @@ -330,7 +332,9 @@ export class ScryptAdapter extends LiquidityActionAdapter { // --- HELPER METHODS --- // - private isBalanceTooLowError(_e: Error): boolean { - return false; // TODO: implement specific error check for Scrypt + private isBalanceTooLowError(e: Error): boolean { + return ['Insufficient funds', 'insufficient balance', 'Insufficient position', 'not enough balance'].some((m) => + e.message?.toLowerCase().includes(m.toLowerCase()), + ); } } From 0b4a4f112f62e3a5af13a252463dbab95a6c2591 Mon Sep 17 00:00:00 2001 From: David May Date: Thu, 15 Jan 2026 22:51:44 +0100 Subject: [PATCH 8/8] feat: implemented trade sync --- src/integration/exchange/dto/scrypt.dto.ts | 188 +++++++++++++++++ .../exchange/mappers/exchange-tx.mapper.ts | 55 ++++- .../exchange/services/exchange-tx.service.ts | 11 +- .../services/scrypt-websocket-connection.ts | 1 + .../exchange/services/scrypt.service.ts | 190 ++++-------------- .../adapters/actions/scrypt.adapter.ts | 8 +- 6 files changed, 289 insertions(+), 164 deletions(-) create mode 100644 src/integration/exchange/dto/scrypt.dto.ts diff --git a/src/integration/exchange/dto/scrypt.dto.ts b/src/integration/exchange/dto/scrypt.dto.ts new file mode 100644 index 0000000000..be4b0e1e41 --- /dev/null +++ b/src/integration/exchange/dto/scrypt.dto.ts @@ -0,0 +1,188 @@ +// --- TRANSACTION TYPES --- // + +export enum ScryptTransactionType { + WITHDRAWAL = 'Withdrawal', + DEPOSIT = 'Deposit', +} + +export enum ScryptTransactionStatus { + COMPLETED = 'Completed', + FAILED = 'Failed', + REJECTED = 'Rejected', +} + +export interface ScryptBalance { + Currency: string; + Amount: string; + AvailableAmount: string; + Equivalent?: { + Currency: string; + Amount: string; + AvailableAmount: string; + }; +} + +export interface ScryptBalanceTransaction { + TransactionID: string; + ClReqID?: string; + Currency: string; + TransactionType: ScryptTransactionType; + Status: ScryptTransactionStatus; + Quantity: string; + Fee?: string; + TxHash?: string; + RejectReason?: string; + RejectText?: string; + Timestamp?: string; + TransactTime?: string; +} + +export interface ScryptWithdrawResponse { + id: string; + status: ScryptTransactionStatus; +} + +export interface ScryptWithdrawStatus { + id: string; + status: ScryptTransactionStatus; + txHash?: string; + amount?: number; + rejectReason?: string; + rejectText?: string; +} + +// --- TRADE TYPES --- // + +export enum ScryptTradeSide { + BUY = 'Buy', + SELL = 'Sell', +} + +export enum ScryptTradeStatus { + PENDING = 'Pending', + CONFIRMED = 'Confirmed', + CANCELED = 'Canceled', +} + +export interface ScryptTrade { + Timestamp: string; + Symbol: string; + OrderID: string; + TradeID: string; + Side: ScryptTradeSide; + TransactTime: string; + ExecType: string; + Currency: string; + Price?: string; + Quantity: string; + Amount: string; + Fee: string; + FeeCurrency?: string; + TradeStatus: ScryptTradeStatus; + AmountCurrency: string; + QuoteID?: string; + RFQID?: string; + CustomerUser?: string; + AggressorSide?: ScryptTradeSide; + DealtCurrency?: string; +} + +// --- ORDER TYPES --- // + +export enum ScryptOrderStatus { + NEW = 'New', + PARTIALLY_FILLED = 'PartiallyFilled', + FILLED = 'Filled', + CANCELLED = 'Cancelled', + REJECTED = 'Rejected', +} + +export enum ScryptOrderSide { + BUY = 'Buy', + SELL = 'Sell', +} + +export enum ScryptOrderType { + MARKET = 'Market', + LIMIT = 'Limit', +} + +export enum ScryptTimeInForce { + FILL_AND_KILL = 'FillAndKill', + FILL_OR_KILL = 'FillOrKill', + GOOD_TILL_CANCEL = 'GoodTillCancel', +} + +export interface ScryptExecutionReport { + ClOrdID: string; + OrigClOrdID?: string; + OrderID?: string; + Symbol: string; + Side: string; + OrdStatus: ScryptOrderStatus; + ExecType?: string; + OrderQty: string; + CumQty: string; + LeavesQty: string; + AvgPx?: string; + Price?: string; + RejectReason?: string; + Text?: string; +} + +export interface ScryptOrderResponse { + id: string; + status: ScryptOrderStatus; +} + +export interface ScryptOrderInfo { + id: string; + orderId?: string; + symbol: string; + side: string; + status: ScryptOrderStatus; + quantity: number; + filledQuantity: number; + remainingQuantity: number; + avgPrice?: number; + price?: number; + rejectReason?: string; +} + +// --- MARKET DATA TYPES --- // + +export interface ScryptPriceLevel { + Price: string; + Size: string; +} + +export interface ScryptMarketDataSnapshot { + Timestamp: string; + Symbol: string; + Status: string; + Bids: ScryptPriceLevel[]; + Offers: ScryptPriceLevel[]; +} + +export interface ScryptOrderBook { + bids: Array<{ price: number; size: number }>; + offers: Array<{ price: number; size: number }>; +} + +// --- SECURITY TYPES --- // + +export interface ScryptSecurity { + Symbol: string; + MinimumSize?: string; + MaximumSize?: string; + MinPriceIncrement?: string; + MinSizeIncrement?: string; +} + +export interface ScryptSecurityInfo { + symbol: string; + minSize: number; + maxSize: number; + minPriceIncrement: number; + minSizeIncrement: number; +} diff --git a/src/integration/exchange/mappers/exchange-tx.mapper.ts b/src/integration/exchange/mappers/exchange-tx.mapper.ts index 3f55a71f40..b8686a0e93 100644 --- a/src/integration/exchange/mappers/exchange-tx.mapper.ts +++ b/src/integration/exchange/mappers/exchange-tx.mapper.ts @@ -1,8 +1,15 @@ import { Trade, Transaction } from 'ccxt'; import { ExchangeTxDto } from '../dto/exchange-tx.dto'; +import { + ScryptBalanceTransaction, + ScryptTrade, + ScryptTradeSide, + ScryptTradeStatus, + ScryptTransactionStatus, + ScryptTransactionType, +} from '../dto/scrypt.dto'; import { ExchangeTxType } from '../entities/exchange-tx.entity'; import { ExchangeName } from '../enums/exchange.enum'; -import { ScryptBalanceTransaction, ScryptTransactionStatus, ScryptTransactionType } from '../services/scrypt.service'; export class ExchangeTxMapper { static mapDeposits(transactions: Transaction[], exchange: ExchangeName): ExchangeTxDto[] { @@ -75,7 +82,7 @@ export class ExchangeTxMapper { static mapScryptTransactions(transactions: ScryptBalanceTransaction[], exchange: ExchangeName): ExchangeTxDto[] { return transactions.map((t) => ({ exchange, - type: t.TransactionType === ScryptTransactionType.DEPOSIT ? ExchangeTxType.DEPOSIT : ExchangeTxType.WITHDRAWAL, + type: this.mapScryptTransactionType(t.TransactionType), externalId: t.TransactionID, externalCreated: t.TransactTime ? new Date(t.TransactTime) : new Date(), externalUpdated: t.Timestamp ? new Date(t.Timestamp) : new Date(), @@ -88,9 +95,20 @@ export class ExchangeTxMapper { })); } + private static mapScryptTransactionType(type: ScryptTransactionType): ExchangeTxType { + switch (type) { + case ScryptTransactionType.DEPOSIT: + return ExchangeTxType.DEPOSIT; + case ScryptTransactionType.WITHDRAWAL: + return ExchangeTxType.WITHDRAWAL; + default: + throw new Error(`Unknown Scrypt transaction type: ${type}`); + } + } + private static mapScryptStatus(status: ScryptTransactionStatus): string { switch (status) { - case ScryptTransactionStatus.COMPLETE: + case ScryptTransactionStatus.COMPLETED: return 'ok'; case ScryptTransactionStatus.FAILED: case ScryptTransactionStatus.REJECTED: @@ -99,4 +117,35 @@ export class ExchangeTxMapper { return 'pending'; } } + + static mapScryptTrades(trades: ScryptTrade[], exchange: ExchangeName): ExchangeTxDto[] { + return trades.map((t) => ({ + exchange, + type: ExchangeTxType.TRADE, + externalId: t.TradeID, + externalCreated: new Date(t.TransactTime), + externalUpdated: new Date(t.Timestamp), + status: this.mapScryptTradeStatus(t.TradeStatus), + amount: parseFloat(t.Quantity) || 0, + feeAmount: parseFloat(t.Fee) || 0, + feeCurrency: t.FeeCurrency ?? t.Currency, + symbol: t.Symbol.replace('-', '/'), + side: t.Side === ScryptTradeSide.BUY ? 'buy' : 'sell', + price: t.Price ? parseFloat(t.Price) : undefined, + cost: parseFloat(t.Amount) || 0, + order: t.OrderID, + })); + } + + private static mapScryptTradeStatus(status: ScryptTradeStatus): string { + switch (status) { + case ScryptTradeStatus.CONFIRMED: + return 'ok'; + case ScryptTradeStatus.CANCELED: + return 'canceled'; + case ScryptTradeStatus.PENDING: + default: + return 'pending'; + } + } } diff --git a/src/integration/exchange/services/exchange-tx.service.ts b/src/integration/exchange/services/exchange-tx.service.ts index 706939ad15..70fa6a1aa4 100644 --- a/src/integration/exchange/services/exchange-tx.service.ts +++ b/src/integration/exchange/services/exchange-tx.service.ts @@ -126,8 +126,15 @@ export class ExchangeTxService { // Scrypt special case if (exchangeService instanceof ScryptService) { - const transactions = await exchangeService.getAllTransactions(since); - return ExchangeTxMapper.mapScryptTransactions(transactions, sync.exchange); + const [transactions, trades] = await Promise.all([ + exchangeService.getAllTransactions(since), + exchangeService.getTrades(since), + ]); + + return [ + ...ExchangeTxMapper.mapScryptTransactions(transactions, sync.exchange), + ...ExchangeTxMapper.mapScryptTrades(trades, sync.exchange), + ]; } const tokens = sync.tokens ?? (await this.assetService.getAssetsUsedOn(sync.exchange)); diff --git a/src/integration/exchange/services/scrypt-websocket-connection.ts b/src/integration/exchange/services/scrypt-websocket-connection.ts index 0f395ad097..fd6e02b8e7 100644 --- a/src/integration/exchange/services/scrypt-websocket-connection.ts +++ b/src/integration/exchange/services/scrypt-websocket-connection.ts @@ -22,6 +22,7 @@ export enum ScryptMessageType { NEW_WITHDRAW_REQUEST = 'NewWithdrawRequest', BALANCE_TRANSACTION = 'BalanceTransaction', BALANCE = 'Balance', + TRADE = 'Trade', ERROR = 'error', // Trading NEW_ORDER_SINGLE = 'NewOrderSingle', diff --git a/src/integration/exchange/services/scrypt.service.ts b/src/integration/exchange/services/scrypt.service.ts index b34a2dab93..464b67fa6d 100644 --- a/src/integration/exchange/services/scrypt.service.ts +++ b/src/integration/exchange/services/scrypt.service.ts @@ -1,159 +1,28 @@ import { Injectable } from '@nestjs/common'; import { randomUUID } from 'crypto'; import { GetConfig } from 'src/config/config'; +import { + ScryptBalance, + ScryptBalanceTransaction, + ScryptExecutionReport, + ScryptMarketDataSnapshot, + ScryptOrderBook, + ScryptOrderInfo, + ScryptOrderResponse, + ScryptOrderSide, + ScryptOrderStatus, + ScryptOrderType, + ScryptSecurity, + ScryptSecurityInfo, + ScryptTimeInForce, + ScryptTrade, + ScryptTransactionStatus, + ScryptTransactionType, + ScryptWithdrawResponse, + ScryptWithdrawStatus, +} from '../dto/scrypt.dto'; import { ScryptMessageType, ScryptWebSocketConnection } from './scrypt-websocket-connection'; -export enum ScryptTransactionType { - WITHDRAWAL = 'Withdrawal', - DEPOSIT = 'Deposit', -} - -export enum ScryptTransactionStatus { - COMPLETE = 'Complete', - FAILED = 'Failed', - REJECTED = 'Rejected', -} - -interface ScryptBalance { - Currency: string; - Amount: string; - AvailableAmount: string; - Equivalent?: { - Currency: string; - Amount: string; - AvailableAmount: string; - }; -} - -export interface ScryptBalanceTransaction { - TransactionID: string; - ClReqID?: string; - Currency: string; - TransactionType: ScryptTransactionType; - Status: ScryptTransactionStatus; - Quantity: string; - Fee?: string; - TxHash?: string; - RejectReason?: string; - RejectText?: string; - Timestamp?: string; - TransactTime?: string; -} - -export interface ScryptWithdrawResponse { - id: string; - status: ScryptTransactionStatus; -} - -export interface ScryptWithdrawStatus { - id: string; - status: ScryptTransactionStatus; - txHash?: string; - amount?: number; - rejectReason?: string; - rejectText?: string; -} - -// --- TRADING TYPES --- // - -export enum ScryptOrderStatus { - NEW = 'New', - PARTIALLY_FILLED = 'PartiallyFilled', - FILLED = 'Filled', - CANCELLED = 'Cancelled', - REJECTED = 'Rejected', -} - -export enum ScryptOrderSide { - BUY = 'Buy', - SELL = 'Sell', -} - -export enum ScryptOrderType { - MARKET = 'Market', - LIMIT = 'Limit', -} - -export enum ScryptTimeInForce { - FILL_AND_KILL = 'FillAndKill', - FILL_OR_KILL = 'FillOrKill', - GOOD_TILL_CANCEL = 'GoodTillCancel', -} - -interface ScryptExecutionReport { - ClOrdID: string; - OrigClOrdID?: string; - OrderID?: string; - Symbol: string; - Side: string; - OrdStatus: ScryptOrderStatus; - ExecType?: string; - OrderQty: string; - CumQty: string; - LeavesQty: string; - AvgPx?: string; - Price?: string; - RejectReason?: string; - Text?: string; -} - -export interface ScryptOrderResponse { - id: string; - status: ScryptOrderStatus; -} - -export interface ScryptOrderInfo { - id: string; - orderId?: string; - symbol: string; - side: string; - status: ScryptOrderStatus; - quantity: number; - filledQuantity: number; - remainingQuantity: number; - avgPrice?: number; - price?: number; - rejectReason?: string; -} - -// --- MARKET DATA TYPES --- // - -interface ScryptPriceLevel { - Price: string; - Size: string; -} - -interface ScryptMarketDataSnapshot { - Timestamp: string; - Symbol: string; - Status: string; - Bids: ScryptPriceLevel[]; - Offers: ScryptPriceLevel[]; -} - -export interface ScryptOrderBook { - bids: Array<{ price: number; size: number }>; - offers: Array<{ price: number; size: number }>; -} - -// --- SECURITY TYPES --- // - -interface ScryptSecurity { - Symbol: string; - MinimumSize?: string; - MaximumSize?: string; - MinPriceIncrement?: string; - MinSizeIncrement?: string; -} - -export interface ScryptSecurityInfo { - symbol: string; - minSize: number; - maxSize: number; - minPriceIncrement: number; - minSizeIncrement: number; -} - @Injectable() export class ScryptService { private readonly connection: ScryptWebSocketConnection; @@ -224,7 +93,10 @@ export class ScryptService { ScryptMessageType.BALANCE_TRANSACTION, (data) => { const transactions = data as ScryptBalanceTransaction[]; - return transactions.find((t) => t.ClReqID === clReqId && t.TransactionType === 'Withdrawal') ?? null; + return ( + transactions.find((t) => t.ClReqID === clReqId && t.TransactionType === ScryptTransactionType.WITHDRAWAL) ?? + null + ); }, 60000, ); @@ -243,7 +115,9 @@ export class ScryptService { async getWithdrawalStatus(clReqId: string): Promise { const transactions = await this.fetchBalanceTransactions(); - const transaction = transactions.find((t) => t.ClReqID === clReqId && t.TransactionType === 'Withdrawal'); + const transaction = transactions.find( + (t) => t.ClReqID === clReqId && t.TransactionType === ScryptTransactionType.WITHDRAWAL, + ); if (!transaction) return null; @@ -257,6 +131,8 @@ export class ScryptService { }; } + // --- TRANSACTIONS / TRADES --- // + async getAllTransactions(since?: Date): Promise { const transactions = await this.fetchBalanceTransactions(); return transactions.filter((t) => !since || (t.TransactTime && new Date(t.TransactTime) >= since)); @@ -267,6 +143,14 @@ export class ScryptService { return data as ScryptBalanceTransaction[]; } + async getTrades(since?: Date): Promise { + const filters: Record = {}; + if (since) filters.StartDate = since.toISOString(); + + const data = await this.connection.fetch(ScryptMessageType.TRADE, filters); + return data as ScryptTrade[]; + } + // --- MARKET DATA --- // async fetchOrderBook(symbol: string): Promise { diff --git a/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts index 295babb6ed..63f6e85d33 100644 --- a/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts @@ -1,11 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; -import { - ScryptOrderSide, - ScryptOrderStatus, - ScryptService, - ScryptTransactionStatus, -} from 'src/integration/exchange/services/scrypt.service'; +import { ScryptOrderSide, ScryptOrderStatus, ScryptTransactionStatus } from 'src/integration/exchange/dto/scrypt.dto'; +import { ScryptService } from 'src/integration/exchange/services/scrypt.service'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { Util } from 'src/shared/utils/util'; import { DexService } from 'src/subdomains/supporting/dex/services/dex.service';