Skip to content
31 changes: 31 additions & 0 deletions migration/1768344518359-AddRefAsset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* @class
* @implements {MigrationInterface}
*/
module.exports = class AddRefAsset1768344518359 {
name = 'AddRefAsset1768344518359'

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "asset" ADD "refEnabled" bit NOT NULL CONSTRAINT "DF_d2c85e8cbdbff07a1dcd8d17797" DEFAULT 0`);
await queryRunner.query(`ALTER TABLE "user" ADD "refAssetId" int`);
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_20e823fee19baff0c5090ab72df" FOREIGN KEY ("refAssetId") REFERENCES "asset"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_20e823fee19baff0c5090ab72df"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "refAssetId"`);
await queryRunner.query(`ALTER TABLE "asset" DROP CONSTRAINT "DF_d2c85e8cbdbff07a1dcd8d17797"`);
await queryRunner.query(`ALTER TABLE "asset" DROP COLUMN "refEnabled"`);
}
}
4 changes: 2 additions & 2 deletions src/integration/exchange/dto/exchange-tx.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export class ExchangeTxDto {
exchange: ExchangeName;
type: ExchangeTxType;
externalId: string;
externalCreated: Date;
externalUpdated: Date;
externalCreated?: Date;
externalUpdated?: Date;
status: string;
amount: number;
feeAmount: number;
Expand Down
186 changes: 186 additions & 0 deletions src/integration/exchange/dto/scrypt.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// --- 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',
CANCELED = 'Canceled',
PENDING_CANCEL = 'PendingCancel',
REJECTED = 'Rejected',
PENDING_NEW = 'PendingNew',
PENDING_REPLACE = 'PendingReplace',
}

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;
OrdRejReason?: string;
CxlRejReason?: 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;
BaseCurrency: string;
QuoteCurrency: string;
MinimumSize?: string;
MaximumSize?: string;
MinPriceIncrement?: string;
MinSizeIncrement?: string;
}
2 changes: 2 additions & 0 deletions src/integration/exchange/entities/exchange-tx.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,6 @@ export const ExchangeSyncs: ExchangeSync[] = [
tokenReplacements: [],
},
{ exchange: ExchangeName.BINANCE, tradeTokens: ['BTC', 'USDT'], tokenReplacements: [['BTCB', 'BTC']] },
{ exchange: ExchangeName.MEXC, tokens: ['ZCHF', 'XMR', 'USDT', 'ZANO', 'fUSD'], tokenReplacements: [] },
{ exchange: ExchangeName.SCRYPT, tokens: [], tokenReplacements: [] },
];
81 changes: 81 additions & 0 deletions src/integration/exchange/mappers/exchange-tx.mapper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
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';

export class ExchangeTxMapper {
// --- CCXT TRANSACTIONS --- //
static mapDeposits(transactions: Transaction[], exchange: ExchangeName): ExchangeTxDto[] {
return transactions
.filter((d) => d.type === 'deposit')
Expand Down Expand Up @@ -70,4 +79,76 @@ export class ExchangeTxMapper {
side: t.side,
}));
}

// --- SCRYPT TRANSACTIONS --- //
static mapScryptTransactions(transactions: ScryptBalanceTransaction[], exchange: ExchangeName): ExchangeTxDto[] {
return transactions.map((t) => ({
exchange,
type: this.mapScryptTransactionType(t.TransactionType),
externalId: t.TransactionID,
externalCreated: t.TransactTime ? new Date(t.TransactTime) : null,
externalUpdated: t.Timestamp ? new Date(t.Timestamp) : null,
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 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.COMPLETED:
return 'ok';
case ScryptTransactionStatus.FAILED:
case ScryptTransactionStatus.REJECTED:
return 'failed';
default:
return 'pending';
}
}

// --- SCRYPT TRADES --- //
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';
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { Injectable } 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';
import { ScryptService } from './scrypt.service';

@Injectable()
export class ExchangeRegistryService extends StrategyRegistry<string, ExchangeService> {
@Inject() 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);
}
}
16 changes: 15 additions & 1 deletion src/integration/exchange/services/exchange-tx.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -121,7 +122,20 @@ export class ExchangeTxService {

private async getTransactionsFor(sync: ExchangeSync, since: Date): Promise<ExchangeTxDto[]> {
try {
const exchangeService = this.registryService.get(sync.exchange);
const exchangeService = this.registryService.getExchange(sync.exchange);

// Scrypt special case
if (exchangeService instanceof ScryptService) {
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));

Expand Down
2 changes: 1 addition & 1 deletion src/integration/exchange/services/mexc.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class MexcService extends ExchangeService {
? 'ok'
: 'pending',
updated: undefined,
fee: undefined,
fee: d.transactionFee ? { cost: parseFloat(d.transactionFee), currency: d.coin.split('-')[0] } : undefined,
network: d.network,
comment: d.memo,
internal: undefined,
Expand Down
Loading
Loading