mirror of
https://github.com/orionprotocol/sdk.git
synced 2026-03-30 09:38:03 +03:00
Semantics improvements
This commit is contained in:
367
src/services/Aggregator/index.ts
Normal file
367
src/services/Aggregator/index.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
import type { BigNumber } from 'bignumber.js';
|
||||
import { z } from 'zod';
|
||||
import swapInfoSchema from './schemas/swapInfoSchema.js';
|
||||
import exchangeInfoSchema from './schemas/exchangeInfoSchema.js';
|
||||
import cancelOrderSchema from './schemas/cancelOrderSchema.js';
|
||||
import orderBenefitsSchema from './schemas/orderBenefitsSchema.js';
|
||||
import errorSchema from './schemas/errorSchema.js';
|
||||
import placeAtomicSwapSchema from './schemas/placeAtomicSwapSchema.js';
|
||||
import { AggregatorWS } from './ws/index.js';
|
||||
import { atomicSwapHistorySchema } from './schemas/atomicSwapHistorySchema.js';
|
||||
import type { Exchange, SignedCancelOrderRequest, SignedCFDOrder, SignedOrder } from '../../types.js';
|
||||
import { pairConfigSchema } from './schemas/index.js';
|
||||
import {
|
||||
aggregatedOrderbookSchema, exchangeOrderbookSchema, poolReservesSchema,
|
||||
} from './schemas/aggregatedOrderbookSchema.js';
|
||||
import type networkCodes from '../../constants/networkCodes.js';
|
||||
import toUpperCase from '../../utils/toUpperCase.js';
|
||||
import httpToWS from '../../utils/httpToWS.js';
|
||||
import { ethers } from 'ethers';
|
||||
import orderSchema from './schemas/orderSchema.js';
|
||||
import { exchanges } from '../../constants/index.js';
|
||||
import { fetchWithValidation } from 'simple-typed-fetch';
|
||||
|
||||
class Aggregator {
|
||||
private readonly apiUrl: string;
|
||||
|
||||
readonly ws: AggregatorWS;
|
||||
|
||||
get api() {
|
||||
return this.apiUrl;
|
||||
}
|
||||
|
||||
constructor(
|
||||
httpAPIUrl: string,
|
||||
wsAPIUrl: string,
|
||||
) {
|
||||
// const oaUrl = new URL(apiUrl);
|
||||
// const oaWsProtocol = oaUrl.protocol === 'https:' ? 'wss' : 'ws';
|
||||
// const aggregatorWsUrl = `${oaWsProtocol}://${oaUrl.host + (oaUrl.pathname === '/'
|
||||
// ? ''
|
||||
// : oaUrl.pathname)}/v1`;
|
||||
|
||||
this.apiUrl = httpAPIUrl;
|
||||
this.ws = new AggregatorWS(httpToWS(wsAPIUrl));
|
||||
|
||||
this.getHistoryAtomicSwaps = this.getHistoryAtomicSwaps.bind(this);
|
||||
this.getPairConfig = this.getPairConfig.bind(this);
|
||||
this.getPairConfigs = this.getPairConfigs.bind(this);
|
||||
this.getPairsList = this.getPairsList.bind(this);
|
||||
this.getSwapInfo = this.getSwapInfo.bind(this);
|
||||
this.getTradeProfits = this.getTradeProfits.bind(this);
|
||||
this.placeAtomicSwap = this.placeAtomicSwap.bind(this);
|
||||
this.placeOrder = this.placeOrder.bind(this);
|
||||
this.placeCFDOrder = this.placeCFDOrder.bind(this);
|
||||
this.cancelOrder = this.cancelOrder.bind(this);
|
||||
this.checkWhitelisted = this.checkWhitelisted.bind(this);
|
||||
this.getLockedBalance = this.getLockedBalance.bind(this);
|
||||
this.getAggregatedOrderbook = this.getAggregatedOrderbook.bind(this);
|
||||
this.getExchangeOrderbook = this.getExchangeOrderbook.bind(this);
|
||||
this.getPoolReserves = this.getPoolReserves.bind(this);
|
||||
this.getVersion = this.getVersion.bind(this);
|
||||
}
|
||||
|
||||
getOrder = (orderId: string, owner?: string) => {
|
||||
if (!ethers.utils.isHexString(orderId)) {
|
||||
throw new Error(`Invalid order id: ${orderId}. Must be a hex string`);
|
||||
}
|
||||
const url = new URL(`${this.apiUrl}/api/v1/order`);
|
||||
url.searchParams.append('orderId', orderId);
|
||||
if (owner !== undefined) {
|
||||
if (!ethers.utils.isAddress(owner)) {
|
||||
throw new Error(`Invalid owner address: ${owner}`);
|
||||
}
|
||||
url.searchParams.append('owner', owner);
|
||||
}
|
||||
return fetchWithValidation(
|
||||
url.toString(),
|
||||
orderSchema,
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
}
|
||||
|
||||
getPairsList = (market: 'spot' | 'futures') => {
|
||||
const url = new URL(`${this.apiUrl}/api/v1/pairs/list`);
|
||||
url.searchParams.append('market', toUpperCase(market));
|
||||
|
||||
return fetchWithValidation(
|
||||
url.toString(),
|
||||
z.array(z.string().toUpperCase()),
|
||||
);
|
||||
};
|
||||
|
||||
getAggregatedOrderbook = (pair: string, depth = 20) => {
|
||||
const url = new URL(`${this.apiUrl}/api/v1/orderbook`);
|
||||
url.searchParams.append('pair', pair);
|
||||
url.searchParams.append('depth', depth.toString());
|
||||
return fetchWithValidation(
|
||||
url.toString(),
|
||||
aggregatedOrderbookSchema,
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
};
|
||||
|
||||
getAvailableExchanges = () => fetchWithValidation(
|
||||
`${this.apiUrl}/api/v1/exchange/list`,
|
||||
z.enum(exchanges).array(),
|
||||
);
|
||||
|
||||
getExchangeOrderbook = (
|
||||
pair: string,
|
||||
exchange: Exchange,
|
||||
depth = 20,
|
||||
filterByBrokerBalances: boolean | null = null,
|
||||
) => {
|
||||
const url = new URL(`${this.apiUrl}/api/v1/orderbook/${exchange}/${pair}`);
|
||||
url.searchParams.append('pair', pair);
|
||||
url.searchParams.append('depth', depth.toString());
|
||||
if (filterByBrokerBalances !== null) {
|
||||
url.searchParams.append('filterByBrokerBalances', filterByBrokerBalances.toString());
|
||||
}
|
||||
return fetchWithValidation(
|
||||
url.toString(),
|
||||
exchangeOrderbookSchema,
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
};
|
||||
|
||||
getPairConfigs = (market: 'spot' | 'futures') => {
|
||||
const url = new URL(`${this.apiUrl}/api/v1/pairs/exchangeInfo`);
|
||||
url.searchParams.append('market', toUpperCase(market));
|
||||
|
||||
return fetchWithValidation(
|
||||
url.toString(),
|
||||
exchangeInfoSchema,
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
}
|
||||
|
||||
getPoolReserves = (
|
||||
pair: string,
|
||||
exchange: Exchange,
|
||||
) => {
|
||||
const url = new URL(`${this.apiUrl}/api/v1/pools/reserves/${exchange}/${pair}`);
|
||||
return fetchWithValidation(
|
||||
url.toString(),
|
||||
poolReservesSchema,
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
};
|
||||
|
||||
getVersion = () => fetchWithValidation(
|
||||
`${this.apiUrl}/api/v1/version`,
|
||||
z.object({
|
||||
serviceName: z.string(),
|
||||
version: z.string(),
|
||||
apiVersion: z.string(),
|
||||
}),
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
|
||||
getPairConfig = (assetPair: string) => fetchWithValidation(
|
||||
`${this.apiUrl}/api/v1/pairs/exchangeInfo/${assetPair}`,
|
||||
pairConfigSchema,
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
|
||||
checkWhitelisted = (address: string) => fetchWithValidation(
|
||||
`${this.apiUrl}/api/v1/whitelist/check?address=${address}`,
|
||||
z.boolean(),
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
|
||||
placeOrder = (
|
||||
signedOrder: SignedOrder,
|
||||
isCreateInternalOrder: boolean,
|
||||
partnerId?: string,
|
||||
) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...(partnerId !== undefined) && { 'X-Partner-Id': partnerId },
|
||||
};
|
||||
|
||||
return fetchWithValidation(
|
||||
`${this.apiUrl}/api/v1/order/${isCreateInternalOrder ? 'internal' : ''}`,
|
||||
z.object({
|
||||
orderId: z.string(),
|
||||
placementRequests: z.array(
|
||||
z.object({
|
||||
amount: z.number(),
|
||||
brokerAddress: z.string(),
|
||||
exchange: z.string(),
|
||||
}),
|
||||
).optional(),
|
||||
}),
|
||||
{
|
||||
headers,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(signedOrder),
|
||||
},
|
||||
errorSchema,
|
||||
);
|
||||
};
|
||||
|
||||
cancelOrder = (signedCancelOrderRequest: SignedCancelOrderRequest) => fetchWithValidation(
|
||||
`${this.apiUrl}/api/v1/order`,
|
||||
cancelOrderSchema,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...signedCancelOrderRequest,
|
||||
sender: signedCancelOrderRequest.senderAddress,
|
||||
}),
|
||||
},
|
||||
errorSchema,
|
||||
);
|
||||
|
||||
placeCFDOrder = (
|
||||
signedOrder: SignedCFDOrder
|
||||
) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
};
|
||||
|
||||
return fetchWithValidation(
|
||||
`${this.apiUrl}/api/v1/order/futures`,
|
||||
z.object({
|
||||
orderId: z.string(),
|
||||
placementRequests: z.array(
|
||||
z.object({
|
||||
amount: z.number(),
|
||||
brokerAddress: z.string(),
|
||||
exchange: z.string(),
|
||||
}),
|
||||
).optional(),
|
||||
}),
|
||||
{
|
||||
headers,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(signedOrder),
|
||||
},
|
||||
errorSchema,
|
||||
);
|
||||
};
|
||||
|
||||
getSwapInfo = (
|
||||
type: 'exactSpend' | 'exactReceive',
|
||||
assetIn: string,
|
||||
assetOut: string,
|
||||
amount: string,
|
||||
instantSettlement?: boolean,
|
||||
exchanges?: Exchange[] | 'cex' | 'pools',
|
||||
) => {
|
||||
const url = new URL(`${this.apiUrl}/api/v1/swap`);
|
||||
url.searchParams.append('assetIn', assetIn);
|
||||
url.searchParams.append('assetOut', assetOut);
|
||||
if (type === 'exactSpend') {
|
||||
url.searchParams.append('amountIn', amount);
|
||||
} else {
|
||||
url.searchParams.append('amountOut', amount);
|
||||
}
|
||||
if (exchanges !== undefined) {
|
||||
if (Array.isArray(exchanges)) {
|
||||
exchanges.forEach((exchange) => {
|
||||
url.searchParams.append('exchanges', exchange);
|
||||
});
|
||||
} else {
|
||||
url.searchParams.append('exchanges', exchanges);
|
||||
}
|
||||
}
|
||||
if (instantSettlement !== undefined && instantSettlement) {
|
||||
url.searchParams.append('instantSettlement', 'true');
|
||||
}
|
||||
|
||||
return fetchWithValidation(
|
||||
url.toString(),
|
||||
swapInfoSchema,
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
};
|
||||
|
||||
getLockedBalance = (address: string, currency: string) => {
|
||||
const url = new URL(`${this.apiUrl}/api/v1/address/balance/reserved/${currency}`);
|
||||
url.searchParams.append('address', address);
|
||||
return fetchWithValidation(
|
||||
url.toString(),
|
||||
z.object({
|
||||
[currency]: z.number(),
|
||||
}).partial(),
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
};
|
||||
|
||||
getTradeProfits = (
|
||||
symbol: string,
|
||||
amount: BigNumber,
|
||||
isBuy: boolean,
|
||||
) => {
|
||||
const url = new URL(`${this.apiUrl}/api/v1/orderBenefits`);
|
||||
url.searchParams.append('symbol', symbol);
|
||||
url.searchParams.append('amount', amount.toString());
|
||||
url.searchParams.append('side', isBuy ? 'buy' : 'sell');
|
||||
|
||||
return fetchWithValidation(
|
||||
url.toString(),
|
||||
orderBenefitsSchema,
|
||||
undefined,
|
||||
errorSchema,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Placing atomic swap. Placement must take place on the target chain.
|
||||
* @param secretHash Secret hash
|
||||
* @param sourceNetworkCode uppercase, e.g. BSC, ETH
|
||||
* @returns Fetch promise
|
||||
*/
|
||||
placeAtomicSwap = (
|
||||
secretHash: string,
|
||||
sourceNetworkCode: Uppercase<typeof networkCodes[number]>,
|
||||
) => fetchWithValidation(
|
||||
`${this.apiUrl}/api/v1/atomic-swap`,
|
||||
placeAtomicSwapSchema,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
secretHash,
|
||||
sourceNetworkCode,
|
||||
}),
|
||||
},
|
||||
errorSchema,
|
||||
);
|
||||
|
||||
/**
|
||||
* Get placed atomic swaps. Each atomic swap received from this list has a target chain corresponding to this Aggregator
|
||||
* @param sender Sender address
|
||||
* @returns Fetch promise
|
||||
*/
|
||||
getHistoryAtomicSwaps = (sender: string, limit = 1000) => {
|
||||
const url = new URL(`${this.apiUrl}/api/v1/atomic-swap/history/all`);
|
||||
url.searchParams.append('sender', sender);
|
||||
url.searchParams.append('limit', limit.toString());
|
||||
return fetchWithValidation(url.toString(), atomicSwapHistorySchema);
|
||||
};
|
||||
}
|
||||
export * as schemas from './schemas/index.js';
|
||||
export * as ws from './ws/index.js';
|
||||
export { Aggregator };
|
||||
32
src/services/Aggregator/schemas/aggregatedOrderbookSchema.ts
Normal file
32
src/services/Aggregator/schemas/aggregatedOrderbookSchema.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
import { exchanges } from '../../../constants/index.js';
|
||||
|
||||
const orderbookElementSchema = z.object({
|
||||
price: z.number(),
|
||||
amount: z.number(),
|
||||
path: z.array(z.object({
|
||||
assetPair: z.string().toUpperCase(),
|
||||
action: z.enum(['BUY', 'SELL']),
|
||||
})),
|
||||
});
|
||||
|
||||
const aggregatedOrderbookElementSchema = orderbookElementSchema
|
||||
.extend({
|
||||
exchanges: z.enum(exchanges).array(),
|
||||
});
|
||||
|
||||
export const aggregatedOrderbookSchema = z.object({
|
||||
asks: z.array(aggregatedOrderbookElementSchema),
|
||||
bids: z.array(aggregatedOrderbookElementSchema),
|
||||
});
|
||||
|
||||
export const exchangeOrderbookSchema = z.object({
|
||||
asks: z.array(orderbookElementSchema),
|
||||
bids: z.array(orderbookElementSchema),
|
||||
});
|
||||
|
||||
export const poolReservesSchema = z.object({
|
||||
a: z.number(), // amount asset
|
||||
p: z.number(), // price asset
|
||||
indicativePrice: z.number(),
|
||||
});
|
||||
22
src/services/Aggregator/schemas/atomicSwapHistorySchema.ts
Normal file
22
src/services/Aggregator/schemas/atomicSwapHistorySchema.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { z } from 'zod';
|
||||
import uppercasedNetworkCodes from '../../../constants/uppercasedNetworkCodes.js';
|
||||
import redeemOrderSchema from './redeemOrderSchema.js';
|
||||
|
||||
export const atomicSwapHistorySchema = z.array(z.object({
|
||||
id: z.string(),
|
||||
sender: z.string(),
|
||||
lockOrder: z.object({
|
||||
sender: z.string(),
|
||||
asset: z.string().toUpperCase(),
|
||||
amount: z.number(),
|
||||
expiration: z.number(),
|
||||
secretHash: z.string(),
|
||||
used: z.boolean(),
|
||||
sourceNetworkCode: z.enum(uppercasedNetworkCodes),
|
||||
}),
|
||||
redeemOrder: redeemOrderSchema,
|
||||
status: z.enum(['SETTLED', 'EXPIRED', 'ACTIVE']),
|
||||
creationTime: z.number(),
|
||||
}));
|
||||
|
||||
export default atomicSwapHistorySchema;
|
||||
13
src/services/Aggregator/schemas/cancelOrderSchema.ts
Normal file
13
src/services/Aggregator/schemas/cancelOrderSchema.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const cancelOrderSchema = z.object({
|
||||
orderId: z.union([z.number(), z.string()]),
|
||||
cancellationRequests: z.array(z.object({
|
||||
amount: z.number(),
|
||||
brokerAddress: z.string(),
|
||||
exchange: z.string(),
|
||||
})).optional(),
|
||||
remainingAmount: z.number().optional(),
|
||||
});
|
||||
|
||||
export default cancelOrderSchema;
|
||||
11
src/services/Aggregator/schemas/errorSchema.ts
Normal file
11
src/services/Aggregator/schemas/errorSchema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const errorSchema = z.object({
|
||||
error: z.object({
|
||||
code: z.number(),
|
||||
reason: z.string(),
|
||||
}),
|
||||
timestamp: z.string(),
|
||||
});
|
||||
|
||||
export default errorSchema;
|
||||
6
src/services/Aggregator/schemas/exchangeInfoSchema.ts
Normal file
6
src/services/Aggregator/schemas/exchangeInfoSchema.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import pairConfigSchema from './pairConfigSchema.js';
|
||||
|
||||
const exchangeInfoSchema = z.array(pairConfigSchema);
|
||||
|
||||
export default exchangeInfoSchema;
|
||||
11
src/services/Aggregator/schemas/index.ts
Normal file
11
src/services/Aggregator/schemas/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export { default as atomicSwapHistorySchema } from './atomicSwapHistorySchema.js';
|
||||
export { default as cancelOrderSchema } from './cancelOrderSchema.js';
|
||||
export { default as exchangeInfoSchema } from './exchangeInfoSchema.js';
|
||||
export { default as orderBenefitsSchema } from './orderBenefitsSchema.js';
|
||||
export { default as pairConfigSchema } from './pairConfigSchema.js';
|
||||
export { default as placeAtomicSwapSchema } from './placeAtomicSwapSchema.js';
|
||||
export { default as redeemOrderSchema } from './redeemOrderSchema.js';
|
||||
export { default as swapInfoSchema } from './swapInfoSchema.js';
|
||||
export { default as orderSchema } from './orderSchema.js';
|
||||
export * from './aggregatedOrderbookSchema.js'
|
||||
export { default as errorSchema } from './errorSchema.js';
|
||||
8
src/services/Aggregator/schemas/orderBenefitsSchema.ts
Normal file
8
src/services/Aggregator/schemas/orderBenefitsSchema.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const orderBenefitsSchema = z.record(z.object({
|
||||
benefitBtc: z.string(),
|
||||
benefitPct: z.string(),
|
||||
}));
|
||||
|
||||
export default orderBenefitsSchema;
|
||||
111
src/services/Aggregator/schemas/orderSchema.ts
Normal file
111
src/services/Aggregator/schemas/orderSchema.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { ethers } from 'ethers';
|
||||
import { z } from 'zod';
|
||||
import { exchanges, orderStatuses, subOrderStatuses } from '../../../constants/index.js';
|
||||
|
||||
const blockchainOrderSchema = z.object({
|
||||
id: z.string().refine(ethers.utils.isHexString, (value) => ({
|
||||
message: `blockchainOrder.id must be a hex string, got ${value}`,
|
||||
})),
|
||||
senderAddress: z.string().refine(ethers.utils.isAddress, (value) => ({
|
||||
message: `blockchainOrder.senderAddress must be an address, got ${value}`,
|
||||
})),
|
||||
matcherAddress: z.string().refine(ethers.utils.isAddress, (value) => ({
|
||||
message: `blockchainOrder.matcherAddress must be an address, got ${value}`,
|
||||
})),
|
||||
baseAsset: z.string().refine(ethers.utils.isAddress, (value) => ({
|
||||
message: `blockchainOrder.baseAsset must be an address, got ${value}`,
|
||||
})),
|
||||
quoteAsset: z.string().refine(ethers.utils.isAddress, (value) => ({
|
||||
message: `blockchainOrder.quoteAsset must be an address, got ${value}`,
|
||||
})),
|
||||
matcherFeeAsset: z.string().refine(ethers.utils.isAddress, (value) => ({
|
||||
message: `blockchainOrder.matcherFeeAsset must be an address, got ${value}`,
|
||||
})),
|
||||
amount: z.number().int().nonnegative(),
|
||||
price: z.number().int().nonnegative(),
|
||||
matcherFee: z.number().int().nonnegative(),
|
||||
nonce: z.number(),
|
||||
expiration: z.number(),
|
||||
buySide: z.union([z.literal(1), z.literal(0)]),
|
||||
signature: z.string().refine(ethers.utils.isHexString, (value) => ({
|
||||
message: `blockchainOrder.signature must be a hex string, got ${value}`,
|
||||
})).nullable(),
|
||||
isPersonalSign: z.boolean(),
|
||||
needWithdraw: z.boolean(),
|
||||
});
|
||||
|
||||
const tradeInfoSchema = z.object({
|
||||
tradeId: z.string().uuid(),
|
||||
tradeStatus: z.enum(['NEW', 'PENDING', 'OK', 'FAIL', 'TEMP_ERROR', 'REJECTED']),
|
||||
filledAmount: z.number().nonnegative(),
|
||||
price: z.number().nonnegative(),
|
||||
creationTime: z.number(),
|
||||
updateTime: z.number(),
|
||||
matchedBlockchainOrder: blockchainOrderSchema.optional(),
|
||||
matchedSubOrderId: z.number().int().nonnegative().optional(),
|
||||
exchangeTradeInfo: z.boolean(),
|
||||
poolTradeInfo: z.boolean(),
|
||||
});
|
||||
|
||||
const baseOrderSchema = z.object({
|
||||
assetPair: z.string().toUpperCase(),
|
||||
side: z.enum(['BUY', 'SELL']),
|
||||
amount: z.number().nonnegative(),
|
||||
remainingAmount: z.number().nonnegative(),
|
||||
price: z.number().nonnegative(),
|
||||
sender: z.string().refine(ethers.utils.isAddress, (value) => ({
|
||||
message: `order.sender must be an address, got ${value}`,
|
||||
})),
|
||||
filledAmount: z.number().nonnegative(),
|
||||
internalOnly: z.boolean(),
|
||||
})
|
||||
|
||||
const selfBrokers = exchanges.map((exchange) => `SELF_BROKER_${exchange}` as const);
|
||||
type SelfBroker = typeof selfBrokers[number];
|
||||
const isSelfBroker = (value: string): value is SelfBroker => selfBrokers.some((broker) => broker === value);
|
||||
const subOrderSchema = baseOrderSchema.extend({
|
||||
price: z.number(),
|
||||
id: z.number(),
|
||||
parentOrderId: z.string().refine(ethers.utils.isHexString, (value) => ({
|
||||
message: `subOrder.parentOrderId must be a hex string, got ${value}`,
|
||||
})),
|
||||
exchange: z.enum(exchanges),
|
||||
brokerAddress:
|
||||
z.enum(['ORION_BROKER', 'SELF_BROKER'])
|
||||
.or(z.custom<SelfBroker>((value) => {
|
||||
if (typeof value === 'string' && isSelfBroker(value)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}))
|
||||
.or(z.string().refine(ethers.utils.isAddress, (value) => ({
|
||||
message: `subOrder.subOrders.[n].brokerAddress must be an address, got ${value}`,
|
||||
}))),
|
||||
tradesInfo: z.record(
|
||||
z.string().uuid(),
|
||||
tradeInfoSchema
|
||||
),
|
||||
status: z.enum(subOrderStatuses),
|
||||
complexSwap: z.boolean(),
|
||||
});
|
||||
|
||||
const orderSchema = z.object({
|
||||
orderId: z.string().refine(ethers.utils.isHexString, (value) => ({
|
||||
message: `orderId must be a hex string, got ${value}`,
|
||||
})),
|
||||
order: baseOrderSchema.extend({
|
||||
id: z.string().refine(ethers.utils.isHexString, (value) => ({
|
||||
message: `order.id must be a hex string, got ${value}`,
|
||||
})),
|
||||
fee: z.number().nonnegative(),
|
||||
feeAsset: z.string().toUpperCase(),
|
||||
creationTime: z.number(),
|
||||
blockchainOrder: blockchainOrderSchema,
|
||||
subOrders: z.record(subOrderSchema),
|
||||
updateTime: z.number(),
|
||||
status: z.enum(orderStatuses),
|
||||
settledAmount: z.number().nonnegative(),
|
||||
})
|
||||
});
|
||||
|
||||
export default orderSchema;
|
||||
16
src/services/Aggregator/schemas/pairConfigSchema.ts
Normal file
16
src/services/Aggregator/schemas/pairConfigSchema.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const pairConfigSchema = z.object({
|
||||
// baseAssetPrecision: z.number().int(), // Deprecated. DO NOT USE
|
||||
// executableOnBrokersPriceDeviation: z.number().nullable(), // Deprecated. DO NOT USE
|
||||
maxPrice: z.number(),
|
||||
maxQty: z.number(),
|
||||
minPrice: z.number(),
|
||||
minQty: z.number(),
|
||||
name: z.string().toUpperCase(),
|
||||
pricePrecision: z.number().int(),
|
||||
qtyPrecision: z.number().int(),
|
||||
// quoteAssetPrecision: z.number().int(), // Deprecated. DO NOT USE
|
||||
});
|
||||
|
||||
export default pairConfigSchema;
|
||||
18
src/services/Aggregator/schemas/placeAtomicSwapSchema.ts
Normal file
18
src/services/Aggregator/schemas/placeAtomicSwapSchema.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const placeAtomicSwapSchema = z.object({
|
||||
redeemOrder: z.object({
|
||||
amount: z.number(),
|
||||
asset: z.string().toUpperCase(),
|
||||
expiration: z.number(),
|
||||
receiver: z.string(),
|
||||
secretHash: z.string(),
|
||||
sender: z.string(),
|
||||
signature: z.string(),
|
||||
claimReceiver: z.string(),
|
||||
}),
|
||||
secretHash: z.string(),
|
||||
sender: z.string(),
|
||||
});
|
||||
|
||||
export default placeAtomicSwapSchema;
|
||||
14
src/services/Aggregator/schemas/redeemOrderSchema.ts
Normal file
14
src/services/Aggregator/schemas/redeemOrderSchema.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const redeemOrderSchema = z.object({
|
||||
asset: z.string().toUpperCase(),
|
||||
amount: z.number(),
|
||||
secretHash: z.string(),
|
||||
sender: z.string(),
|
||||
receiver: z.string(),
|
||||
expiration: z.number(),
|
||||
signature: z.string(),
|
||||
claimReceiver: z.string(),
|
||||
});
|
||||
|
||||
export default redeemOrderSchema;
|
||||
66
src/services/Aggregator/schemas/swapInfoSchema.ts
Normal file
66
src/services/Aggregator/schemas/swapInfoSchema.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { z } from 'zod';
|
||||
import { exchanges } from '../../../constants/index.js';
|
||||
|
||||
const orderInfoSchema = z.object({
|
||||
assetPair: z.string().toUpperCase(),
|
||||
side: z.enum(['BUY', 'SELL']),
|
||||
amount: z.number(),
|
||||
safePrice: z.number(),
|
||||
}).nullable();
|
||||
|
||||
const swapInfoBase = z.object({
|
||||
id: z.string(),
|
||||
amountIn: z.number(),
|
||||
amountOut: z.number(),
|
||||
assetIn: z.string().toUpperCase(),
|
||||
assetOut: z.string().toUpperCase(),
|
||||
path: z.array(z.string()),
|
||||
// isThroughPoolOptimal: z.boolean(), // deprecated
|
||||
executionInfo: z.string(),
|
||||
orderInfo: orderInfoSchema,
|
||||
exchanges: z.array(z.enum(exchanges)),
|
||||
price: z.number().nullable(), // spending asset price
|
||||
minAmountOut: z.number(),
|
||||
minAmountIn: z.number(),
|
||||
marketPrice: z.number().nullable(), // spending asset market price
|
||||
alternatives: z.object({ // execution alternatives
|
||||
exchanges: z.array(z.enum(exchanges)),
|
||||
path: z.object({
|
||||
units: z.object({
|
||||
assetPair: z.string().toUpperCase(),
|
||||
action: z.string(),
|
||||
}).array(),
|
||||
}),
|
||||
marketAmountOut: z.number().nullable(),
|
||||
marketAmountIn: z.number().nullable(),
|
||||
marketPrice: z.number(),
|
||||
availableAmountIn: z.number().nullable(),
|
||||
availableAmountOut: z.number().nullable(),
|
||||
orderInfo: orderInfoSchema,
|
||||
isThroughPoolOrCurve: z.boolean(),
|
||||
}).array(),
|
||||
});
|
||||
|
||||
const swapInfoByAmountIn = swapInfoBase.extend({
|
||||
availableAmountOut: z.null(),
|
||||
availableAmountIn: z.number(),
|
||||
marketAmountOut: z.number().nullable(),
|
||||
marketAmountIn: z.null(),
|
||||
}).transform((val) => ({
|
||||
...val,
|
||||
type: 'exactSpend' as const,
|
||||
}));
|
||||
|
||||
const swapInfoByAmountOut = swapInfoBase.extend({
|
||||
availableAmountOut: z.number(),
|
||||
availableAmountIn: z.null(),
|
||||
marketAmountOut: z.null(),
|
||||
marketAmountIn: z.number().nullable(),
|
||||
}).transform((val) => ({
|
||||
...val,
|
||||
type: 'exactReceive' as const,
|
||||
}));
|
||||
|
||||
const swapInfoSchema = swapInfoByAmountIn.or(swapInfoByAmountOut);
|
||||
|
||||
export default swapInfoSchema;
|
||||
16
src/services/Aggregator/ws/MessageType.ts
Normal file
16
src/services/Aggregator/ws/MessageType.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
const MessageType = {
|
||||
ERROR: 'e',
|
||||
PING_PONG: 'pp',
|
||||
SWAP_INFO: 'si',
|
||||
INITIALIZATION: 'i',
|
||||
AGGREGATED_ORDER_BOOK_UPDATE: 'aobu',
|
||||
ASSET_PAIRS_CONFIG_UPDATE: 'apcu',
|
||||
ASSET_PAIR_CONFIG_UPDATE: 'apiu',
|
||||
ADDRESS_UPDATE: 'au',
|
||||
CFD_ADDRESS_UPDATE: 'auf',
|
||||
FUTURES_TRADE_INFO_UPDATE: 'fti',
|
||||
BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATE: 'btasabu',
|
||||
UNSUBSCRIPTION_DONE: 'ud',
|
||||
} as const;
|
||||
|
||||
export default MessageType;
|
||||
12
src/services/Aggregator/ws/SubscriptionType.ts
Normal file
12
src/services/Aggregator/ws/SubscriptionType.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
const SubscriptionType = {
|
||||
ASSET_PAIRS_CONFIG_UPDATES_SUBSCRIBE: 'apcus',
|
||||
ASSET_PAIR_CONFIG_UPDATES_SUBSCRIBE: 'apius',
|
||||
AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE: 'aobus',
|
||||
ADDRESS_UPDATES_SUBSCRIBE: 'aus',
|
||||
CFD_ADDRESS_UPDATES_SUBSCRIBE: 'ausf',
|
||||
BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATES_SUBSCRIBE: 'btasabus',
|
||||
SWAP_SUBSCRIBE: 'ss',
|
||||
FUTURES_TRADE_INFO_SUBSCRIBE: 'fts',
|
||||
} as const;
|
||||
|
||||
export default SubscriptionType;
|
||||
5
src/services/Aggregator/ws/UnsubscriptionType.ts
Normal file
5
src/services/Aggregator/ws/UnsubscriptionType.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const UnsubscriptionType = {
|
||||
ASSET_PAIRS_CONFIG_UPDATES_UNSUBSCRIBE: 'apcu',
|
||||
BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATES_UNSUBSCRIBE: 'btasabu',
|
||||
} as const;
|
||||
export default UnsubscriptionType;
|
||||
664
src/services/Aggregator/ws/index.ts
Normal file
664
src/services/Aggregator/ws/index.ts
Normal file
@@ -0,0 +1,664 @@
|
||||
import { z } from 'zod';
|
||||
import WebSocket from 'isomorphic-ws';
|
||||
import { validate as uuidValidate, v4 as uuidv4 } from 'uuid';
|
||||
import MessageType from './MessageType.js';
|
||||
import SubscriptionType from './SubscriptionType.js';
|
||||
import {
|
||||
pingPongMessageSchema, initMessageSchema,
|
||||
errorSchema, brokerMessageSchema, orderBookSchema,
|
||||
assetPairsConfigSchema, addressUpdateSchema, swapInfoSchema,
|
||||
} from './schemas/index.js';
|
||||
import UnsubscriptionType from './UnsubscriptionType.js';
|
||||
import type {
|
||||
SwapInfoBase, AssetPairUpdate, OrderbookItem,
|
||||
Balance, Exchange, CFDBalance, FuturesTradeInfo, SwapInfo, Json,
|
||||
} from '../../../types.js';
|
||||
import unsubscriptionDoneSchema from './schemas/unsubscriptionDoneSchema.js';
|
||||
import assetPairConfigSchema from './schemas/assetPairConfigSchema.js';
|
||||
import type { fullOrderSchema, orderUpdateSchema } from './schemas/addressUpdateSchema.js';
|
||||
import cfdAddressUpdateSchema from './schemas/cfdAddressUpdateSchema.js';
|
||||
import futuresTradeInfoSchema from './schemas/futuresTradeInfoSchema.js';
|
||||
import { objectKeys } from '../../../utils/objectKeys.js';
|
||||
// import errorSchema from './schemas/errorSchema';
|
||||
|
||||
const UNSUBSCRIBE = 'u';
|
||||
|
||||
type SwapSubscriptionRequest = {
|
||||
// d: string, // swap request UUID, set by client side
|
||||
i: string // asset in
|
||||
o: string // asset out
|
||||
a: number // amount IN/OUT
|
||||
es?: Exchange[] | 'cex' | 'pools' // exchange list of all cex or all pools (ORION_POOL, UNISWAP, PANCAKESWAP etc)
|
||||
e?: boolean // is amount IN? Value `false` means a = amount OUT, `true` if omitted
|
||||
is?: boolean // instant settlement
|
||||
}
|
||||
|
||||
type BrokerTradableAtomicSwapBalanceSubscription = {
|
||||
callback: (balances: Partial<Record<string, number>>) => void
|
||||
}
|
||||
|
||||
type PairsConfigSubscription = {
|
||||
callback: ({ kind, data }: {
|
||||
kind: 'initial' | 'update'
|
||||
data: Partial<Record<string, AssetPairUpdate>>
|
||||
}) => void
|
||||
}
|
||||
|
||||
type PairConfigSubscription = {
|
||||
payload: string
|
||||
callback: ({ kind, data }: {
|
||||
kind: 'initial' | 'update'
|
||||
data: AssetPairUpdate
|
||||
}) => void
|
||||
}
|
||||
|
||||
type AggregatedOrderbookSubscription = {
|
||||
payload: string
|
||||
callback: (
|
||||
asks: OrderbookItem[],
|
||||
bids: OrderbookItem[],
|
||||
pair: string
|
||||
) => void
|
||||
errorCb?: (message: string) => void
|
||||
}
|
||||
|
||||
type SwapInfoSubscription = {
|
||||
payload: SwapSubscriptionRequest
|
||||
callback: (swapInfo: SwapInfo) => void
|
||||
}
|
||||
|
||||
type FuturesTradeInfoSubscription = {
|
||||
payload: {
|
||||
s: string
|
||||
i: string
|
||||
a: number
|
||||
p?: number
|
||||
}
|
||||
callback: (futuresTradeInfo: FuturesTradeInfo) => void
|
||||
errorCb?: (message: string) => void
|
||||
}
|
||||
|
||||
type AddressUpdateUpdate = {
|
||||
kind: 'update'
|
||||
balances: Partial<
|
||||
Record<
|
||||
string,
|
||||
Balance
|
||||
>
|
||||
>
|
||||
order?: z.infer<typeof orderUpdateSchema> | z.infer<typeof fullOrderSchema> | undefined
|
||||
}
|
||||
|
||||
type AddressUpdateInitial = {
|
||||
kind: 'initial'
|
||||
balances: Partial<
|
||||
Record<
|
||||
string,
|
||||
Balance
|
||||
>
|
||||
>
|
||||
orders?: Array<z.infer<typeof fullOrderSchema>> | undefined // The field is not defined if the user has no orders
|
||||
}
|
||||
|
||||
type CfdAddressUpdateUpdate = {
|
||||
kind: 'update'
|
||||
balances?: CFDBalance[] | undefined
|
||||
order?: z.infer<typeof orderUpdateSchema> | z.infer<typeof fullOrderSchema> | undefined
|
||||
}
|
||||
|
||||
type CfdAddressUpdateInitial = {
|
||||
kind: 'initial'
|
||||
balances: CFDBalance[]
|
||||
orders?: Array<z.infer<typeof fullOrderSchema>> | undefined // The field is not defined if the user has no orders
|
||||
}
|
||||
|
||||
type AddressUpdateSubscription = {
|
||||
payload: string
|
||||
callback: (data: AddressUpdateUpdate | AddressUpdateInitial) => void
|
||||
errorCb?: (message: string) => void
|
||||
}
|
||||
|
||||
type CfdAddressUpdateSubscription = {
|
||||
payload: string
|
||||
callback: (data: CfdAddressUpdateUpdate | CfdAddressUpdateInitial) => void
|
||||
}
|
||||
|
||||
type Subscription = {
|
||||
[SubscriptionType.ADDRESS_UPDATES_SUBSCRIBE]: AddressUpdateSubscription
|
||||
[SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE]: CfdAddressUpdateSubscription
|
||||
[SubscriptionType.AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE]: AggregatedOrderbookSubscription
|
||||
[SubscriptionType.ASSET_PAIRS_CONFIG_UPDATES_SUBSCRIBE]: PairsConfigSubscription
|
||||
[SubscriptionType.ASSET_PAIR_CONFIG_UPDATES_SUBSCRIBE]: PairConfigSubscription
|
||||
[SubscriptionType.BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATES_SUBSCRIBE]: BrokerTradableAtomicSwapBalanceSubscription
|
||||
[SubscriptionType.SWAP_SUBSCRIBE]: SwapInfoSubscription
|
||||
[SubscriptionType.FUTURES_TRADE_INFO_SUBSCRIBE]: FuturesTradeInfoSubscription
|
||||
}
|
||||
|
||||
const exclusiveSubscriptions = [
|
||||
SubscriptionType.BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATES_SUBSCRIBE,
|
||||
SubscriptionType.ASSET_PAIRS_CONFIG_UPDATES_SUBSCRIBE,
|
||||
] as const;
|
||||
|
||||
type BufferLike =
|
||||
| string
|
||||
| Buffer
|
||||
| DataView
|
||||
| number
|
||||
| ArrayBufferView
|
||||
| Uint8Array
|
||||
| ArrayBuffer
|
||||
| SharedArrayBuffer
|
||||
| readonly unknown[]
|
||||
| readonly number[]
|
||||
| { valueOf: () => ArrayBuffer }
|
||||
| { valueOf: () => SharedArrayBuffer }
|
||||
| { valueOf: () => Uint8Array }
|
||||
| { valueOf: () => readonly number[] }
|
||||
| { valueOf: () => string }
|
||||
| { [Symbol.toPrimitive]: (hint: string) => string };
|
||||
|
||||
const isSubType = (subType: string): subType is keyof Subscription => Object.values(SubscriptionType).some((t) => t === subType);
|
||||
|
||||
const unknownMessageTypeRegex = /An unknown message type: '(.*)', json: (.*)/;
|
||||
const nonExistentMessageRegex = /Could not cancel nonexistent subscription: (.*)/;
|
||||
class AggregatorWS {
|
||||
private ws?: WebSocket | undefined;
|
||||
|
||||
// is used to make sure we do not need to renew ws subscription
|
||||
// we can not be sure that onclose event will recieve our code when we do `ws.close(4000)`
|
||||
// since sometimes it can be replaced with system one.
|
||||
// https://stackoverflow.com/questions/19304157/getting-the-reason-why-websockets-closed-with-close-code-1006
|
||||
private isClosedIntentionally = false;
|
||||
|
||||
private subscriptions: Partial<{
|
||||
[K in keyof Subscription]: Partial<Record<string, Subscription[K]>>
|
||||
}> = {};
|
||||
|
||||
public onInit: (() => void) | undefined
|
||||
|
||||
public onError: ((err: string) => void) | undefined
|
||||
|
||||
public logger: ((message: string) => void) | undefined
|
||||
|
||||
private readonly wsUrl: string;
|
||||
|
||||
get api() {
|
||||
return this.wsUrl;
|
||||
}
|
||||
|
||||
constructor(
|
||||
wsUrl: string,
|
||||
logger?: (msg: string) => void,
|
||||
onInit?: () => void,
|
||||
onError?: (err: string) => void
|
||||
) {
|
||||
this.wsUrl = wsUrl;
|
||||
this.logger = logger;
|
||||
this.onInit = onInit;
|
||||
this.onError = onError;
|
||||
}
|
||||
|
||||
private sendRaw(data: BufferLike) {
|
||||
if (this.ws?.readyState === 1) {
|
||||
this.ws.send(data);
|
||||
} else if (this.ws?.readyState === 0) {
|
||||
setTimeout(() => {
|
||||
this.sendRaw(data);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
private send(jsonObject: Json) {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
const jsonData = JSON.stringify(jsonObject);
|
||||
this.ws.send(jsonData);
|
||||
this.logger?.(`Sent: ${jsonData}`);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.send(jsonObject);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
subscribe<T extends typeof SubscriptionType[keyof typeof SubscriptionType]>(
|
||||
type: T,
|
||||
subscription: Subscription[T],
|
||||
) {
|
||||
if (!this.ws) this.init();
|
||||
const isExclusive = exclusiveSubscriptions.some((t) => t === type);
|
||||
const subs = this.subscriptions[type];
|
||||
if (isExclusive && subs && Object.keys(subs).length > 0) {
|
||||
throw new Error(`Subscription '${type}' already exists. Please unsubscribe first.`);
|
||||
}
|
||||
|
||||
const id = type === 'aobus'
|
||||
? ((subscription as any).payload as string) // TODO: Refactor!!!
|
||||
: uuidv4();
|
||||
const subRequest: Json = {};
|
||||
subRequest['T'] = type;
|
||||
subRequest['id'] = id;
|
||||
|
||||
// TODO Refactor this
|
||||
if ('payload' in subscription) {
|
||||
if (typeof subscription.payload === 'string') {
|
||||
subRequest['S'] = subscription.payload;
|
||||
} else {
|
||||
subRequest['S'] = {
|
||||
d: id,
|
||||
...subscription.payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.send(subRequest);
|
||||
|
||||
const subKey = isExclusive ? 'default' : id;
|
||||
this.subscriptions[type] = {
|
||||
...this.subscriptions[type],
|
||||
[subKey]: subscription,
|
||||
};
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
unsubscribe(subscription: keyof typeof UnsubscriptionType | string, details?: string) {
|
||||
this.send({
|
||||
T: UNSUBSCRIBE,
|
||||
S: subscription,
|
||||
...(details !== undefined) && { d: details },
|
||||
});
|
||||
|
||||
if (subscription.includes('0x')) { // is wallet address (ADDRESS_UPDATE)
|
||||
const auSubscriptions = this.subscriptions[SubscriptionType.ADDRESS_UPDATES_SUBSCRIBE];
|
||||
if (auSubscriptions) {
|
||||
const targetAuSub = Object.entries(auSubscriptions).find(([, value]) => value?.payload === subscription);
|
||||
if (targetAuSub) {
|
||||
const [key] = targetAuSub;
|
||||
delete this.subscriptions[SubscriptionType.ADDRESS_UPDATES_SUBSCRIBE]?.[key];
|
||||
}
|
||||
}
|
||||
|
||||
const aufSubscriptions = this.subscriptions[SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE];
|
||||
if (aufSubscriptions) {
|
||||
const targetAufSub = Object.entries(aufSubscriptions).find(([, value]) => value?.payload === subscription);
|
||||
if (targetAufSub) {
|
||||
const [key] = targetAufSub;
|
||||
delete this.subscriptions[SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE]?.[key];
|
||||
}
|
||||
}
|
||||
} else if (uuidValidate(subscription)) {
|
||||
// is swap info subscription (contains hyphen)
|
||||
delete this.subscriptions[SubscriptionType.SWAP_SUBSCRIBE]?.[subscription];
|
||||
delete this.subscriptions[SubscriptionType.ASSET_PAIR_CONFIG_UPDATES_SUBSCRIBE]?.[subscription];
|
||||
delete this.subscriptions[SubscriptionType.FUTURES_TRADE_INFO_SUBSCRIBE]?.[subscription];
|
||||
// !!! swap info subscription is uuid that contains hyphen
|
||||
} else if (subscription.includes('-') && subscription.split('-').length === 2) { // is pair name(AGGREGATED_ORDER_BOOK_UPDATE)
|
||||
const aobSubscriptions = this.subscriptions[SubscriptionType.AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE];
|
||||
if (aobSubscriptions) {
|
||||
const targetAobSub = Object.entries(aobSubscriptions).find(([, value]) => value?.payload === subscription);
|
||||
if (targetAobSub) {
|
||||
const [key] = targetAobSub;
|
||||
delete this.subscriptions[SubscriptionType.AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE]?.[key];
|
||||
}
|
||||
}
|
||||
} else if (subscription === UnsubscriptionType.ASSET_PAIRS_CONFIG_UPDATES_UNSUBSCRIBE) {
|
||||
delete this.subscriptions[SubscriptionType.ASSET_PAIRS_CONFIG_UPDATES_SUBSCRIBE]?.['default'];
|
||||
} else if (subscription === UnsubscriptionType.BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATES_UNSUBSCRIBE) {
|
||||
delete this.subscriptions[SubscriptionType.BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATES_SUBSCRIBE]?.['default'];
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.isClosedIntentionally = true;
|
||||
this.ws?.close();
|
||||
delete this.ws;
|
||||
}
|
||||
|
||||
private init(isReconnect = false) {
|
||||
this.isClosedIntentionally = false;
|
||||
this.ws = new WebSocket(this.wsUrl);
|
||||
this.ws.onerror = (err) => {
|
||||
this.logger?.(`AggregatorWS: ${err.message}`);
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
this.logger?.(`AggregatorWS: connection closed ${this.isClosedIntentionally ? 'intentionally' : ''}`);
|
||||
if (!this.isClosedIntentionally) this.init(true);
|
||||
};
|
||||
this.ws.onopen = () => {
|
||||
// Re-subscribe to all subscriptions
|
||||
if (isReconnect) {
|
||||
const subscriptionsToReconnect = this.subscriptions;
|
||||
this.subscriptions = {};
|
||||
Object.keys(subscriptionsToReconnect)
|
||||
.filter(isSubType)
|
||||
.forEach((subType) => {
|
||||
const subscriptions = subscriptionsToReconnect[subType];
|
||||
if (subscriptions) {
|
||||
Object.keys(subscriptions).forEach((subKey) => {
|
||||
const sub = subscriptions[subKey];
|
||||
if (sub) this.subscribe(subType, sub);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
this.logger?.(`AggregatorWS: connection opened${isReconnect ? ' (reconnect)' : ''}`);
|
||||
};
|
||||
this.ws.onmessage = (e) => {
|
||||
const { data } = e;
|
||||
if (typeof data !== 'string') throw new Error('AggregatorWS: received non-string message');
|
||||
this.logger?.(`AggregatorWS: received message: ${data}`);
|
||||
const rawJson: unknown = JSON.parse(data);
|
||||
|
||||
const messageSchema = z.union([
|
||||
initMessageSchema,
|
||||
pingPongMessageSchema,
|
||||
addressUpdateSchema,
|
||||
cfdAddressUpdateSchema,
|
||||
assetPairsConfigSchema,
|
||||
assetPairConfigSchema,
|
||||
brokerMessageSchema,
|
||||
orderBookSchema,
|
||||
swapInfoSchema,
|
||||
futuresTradeInfoSchema,
|
||||
errorSchema,
|
||||
unsubscriptionDoneSchema,
|
||||
]);
|
||||
|
||||
const json = messageSchema.parse(rawJson);
|
||||
|
||||
switch (json.T) {
|
||||
case MessageType.ERROR: {
|
||||
const err = errorSchema.parse(json);
|
||||
// Get subscription error callback
|
||||
// 2. Find subscription by id
|
||||
// 3. Call onError callback
|
||||
|
||||
const { id, m } = err;
|
||||
if (id !== undefined) {
|
||||
const nonExistentMessageMatch = m.match(nonExistentMessageRegex);
|
||||
const unknownMessageMatch = m.match(unknownMessageTypeRegex);
|
||||
if (nonExistentMessageMatch !== null) {
|
||||
const [, subscription] = nonExistentMessageMatch;
|
||||
if (subscription === undefined) throw new TypeError('Subscription is undefined. This should not happen.')
|
||||
console.warn(`You tried to unsubscribe from non-existent subscription '${subscription}'. This is probably a bug in the code. Please be sure that you are unsubscribing from the subscription that you are subscribed to.`)
|
||||
} else if (unknownMessageMatch !== null) {
|
||||
const [, subscription, jsonPayload] = unknownMessageMatch;
|
||||
if (subscription === undefined) throw new TypeError('Subscription is undefined. This should not happen.')
|
||||
if (jsonPayload === undefined) throw new TypeError('JSON payload is undefined. This should not happen.')
|
||||
console.warn(`You tried to subscribe to '${subscription}' with unknown payload '${jsonPayload}'. This is probably a bug in the code. Please be sure that you are subscribing to the existing subscription with the correct payload.`)
|
||||
} else {
|
||||
const subType = objectKeys(this.subscriptions).find((st) => this.subscriptions[st]?.[id]);
|
||||
if (subType === undefined) throw new Error(`AggregatorWS: cannot find subscription type by id ${id}. Current subscriptions: ${JSON.stringify(this.subscriptions)}`);
|
||||
const sub = this.subscriptions[subType]?.[id];
|
||||
if (sub === undefined) throw new Error(`AggregatorWS: cannot find subscription by id ${id}. Current subscriptions: ${JSON.stringify(this.subscriptions)}`);
|
||||
if ('errorCb' in sub) {
|
||||
sub.errorCb(err.m);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.onError?.(err.m);
|
||||
}
|
||||
break;
|
||||
case MessageType.PING_PONG:
|
||||
this.sendRaw(data.toString());
|
||||
break;
|
||||
case MessageType.UNSUBSCRIPTION_DONE:
|
||||
// To implement
|
||||
break;
|
||||
case MessageType.SWAP_INFO: {
|
||||
const baseSwapInfo: SwapInfoBase = {
|
||||
swapRequestId: json.S,
|
||||
assetIn: json.ai,
|
||||
assetOut: json.ao,
|
||||
amountIn: json.a,
|
||||
amountOut: json.o,
|
||||
price: json.p,
|
||||
marketPrice: json.mp,
|
||||
minAmountOut: json.mao,
|
||||
minAmountIn: json.ma,
|
||||
path: json.ps,
|
||||
exchanges: json.e,
|
||||
poolOptimal: json.po,
|
||||
...(json.oi) && {
|
||||
orderInfo: {
|
||||
pair: json.oi.p,
|
||||
side: json.oi.s,
|
||||
amount: json.oi.a,
|
||||
safePrice: json.oi.sp,
|
||||
},
|
||||
},
|
||||
alternatives: json.as.map((item) => ({
|
||||
exchanges: item.e,
|
||||
path: item.ps,
|
||||
marketAmountOut: item.mo,
|
||||
marketAmountIn: item.mi,
|
||||
marketPrice: item.mp,
|
||||
availableAmountIn: item.aa,
|
||||
availableAmountOut: item.aao,
|
||||
})),
|
||||
};
|
||||
|
||||
switch (json.k) { // kind
|
||||
case 'exactSpend':
|
||||
this.subscriptions[SubscriptionType.SWAP_SUBSCRIBE]?.[json.S]?.callback({
|
||||
kind: json.k,
|
||||
marketAmountOut: json.mo,
|
||||
availableAmountIn: json.aa,
|
||||
...baseSwapInfo,
|
||||
});
|
||||
|
||||
break;
|
||||
case 'exactReceive':
|
||||
this.subscriptions[SubscriptionType.SWAP_SUBSCRIBE]?.[json.S]?.callback({
|
||||
kind: json.k,
|
||||
...baseSwapInfo,
|
||||
marketAmountIn: json.mi,
|
||||
availableAmountOut: json.aao,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MessageType.FUTURES_TRADE_INFO_UPDATE:
|
||||
this.subscriptions[SubscriptionType.FUTURES_TRADE_INFO_SUBSCRIBE]?.[json.id]?.callback({
|
||||
futuresTradeRequestId: json.id,
|
||||
sender: json.S,
|
||||
instrument: json.i,
|
||||
buyPrice: json.bp,
|
||||
sellPrice: json.sp,
|
||||
buyPower: json.bpw,
|
||||
sellPower: json.spw,
|
||||
minAmount: json.ma,
|
||||
});
|
||||
break;
|
||||
case MessageType.INITIALIZATION:
|
||||
this.onInit?.();
|
||||
break;
|
||||
case MessageType.AGGREGATED_ORDER_BOOK_UPDATE: {
|
||||
const { ob, S } = json;
|
||||
const mapOrderbookItems = (rawItems: typeof ob.a | typeof ob.b) => rawItems.reduce<OrderbookItem[]>((acc, item) => {
|
||||
const [
|
||||
price,
|
||||
amount,
|
||||
exchanges,
|
||||
vob,
|
||||
] = item;
|
||||
|
||||
acc.push({
|
||||
price,
|
||||
amount,
|
||||
exchanges,
|
||||
vob: vob.map(([side, pairName]) => ({
|
||||
side,
|
||||
pairName,
|
||||
})),
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
this.subscriptions[
|
||||
SubscriptionType.AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE
|
||||
]?.[json.S]?.callback(
|
||||
mapOrderbookItems(ob.a),
|
||||
mapOrderbookItems(ob.b),
|
||||
S,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case MessageType.ASSET_PAIR_CONFIG_UPDATE: {
|
||||
const pair = json.u;
|
||||
const [, minQty, pricePrecision] = pair;
|
||||
|
||||
this.subscriptions[
|
||||
SubscriptionType.ASSET_PAIR_CONFIG_UPDATES_SUBSCRIBE
|
||||
]?.[json.id]?.callback({
|
||||
data: {
|
||||
minQty,
|
||||
pricePrecision,
|
||||
},
|
||||
kind: json.k === 'i' ? 'initial' : 'update',
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case MessageType.ASSET_PAIRS_CONFIG_UPDATE: {
|
||||
const pairs = json;
|
||||
const priceUpdates: Partial<Record<string, AssetPairUpdate>> = {};
|
||||
|
||||
pairs.u.forEach(([pairName, minQty, pricePrecision]) => {
|
||||
priceUpdates[pairName] = {
|
||||
minQty,
|
||||
pricePrecision,
|
||||
};
|
||||
});
|
||||
|
||||
this.subscriptions[
|
||||
SubscriptionType.ASSET_PAIRS_CONFIG_UPDATES_SUBSCRIBE
|
||||
]?.['default']?.callback({
|
||||
kind: json.k === 'i' ? 'initial' : 'update',
|
||||
data: priceUpdates,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case MessageType.CFD_ADDRESS_UPDATE:
|
||||
switch (json.k) { // message kind
|
||||
case 'i': { // initial
|
||||
const fullOrders = (json.o)
|
||||
? json.o.reduce<Array<z.infer<typeof fullOrderSchema>>>((prev, o) => {
|
||||
prev.push(o);
|
||||
|
||||
return prev;
|
||||
}, [])
|
||||
: undefined;
|
||||
|
||||
this.subscriptions[
|
||||
SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE
|
||||
]?.[json.id]?.callback({
|
||||
kind: 'initial',
|
||||
orders: fullOrders,
|
||||
balances: json.b,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'u': { // update
|
||||
let orderUpdate: z.infer<typeof orderUpdateSchema> | z.infer<typeof fullOrderSchema> | undefined;
|
||||
if (json.o) {
|
||||
const firstOrder = json.o[0];
|
||||
orderUpdate = firstOrder;
|
||||
}
|
||||
|
||||
this.subscriptions[
|
||||
SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE
|
||||
]?.[json.id]?.callback({
|
||||
kind: 'update',
|
||||
order: orderUpdate,
|
||||
balances: json.b,
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case MessageType.ADDRESS_UPDATE: {
|
||||
const balances = (json.b)
|
||||
? Object.entries(json.b)
|
||||
.reduce<Partial<Record<string, Balance>>>((prev, [asset, assetBalances]) => {
|
||||
if (!assetBalances) return prev;
|
||||
const [tradable, reserved, contract, wallet, allowance] = assetBalances;
|
||||
|
||||
prev[asset] = {
|
||||
tradable, reserved, contract, wallet, allowance,
|
||||
};
|
||||
|
||||
return prev;
|
||||
}, {})
|
||||
: {};
|
||||
switch (json.k) { // message kind
|
||||
case 'i': { // initial
|
||||
const fullOrders = json.o
|
||||
? json.o.reduce<Array<z.infer<typeof fullOrderSchema>>>((prev, o) => {
|
||||
prev.push(o);
|
||||
|
||||
return prev;
|
||||
}, [])
|
||||
: undefined;
|
||||
|
||||
this.subscriptions[
|
||||
SubscriptionType.ADDRESS_UPDATES_SUBSCRIBE
|
||||
]?.[json.id]?.callback({
|
||||
kind: 'initial',
|
||||
orders: fullOrders,
|
||||
balances,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'u': { // update
|
||||
let orderUpdate: z.infer<typeof orderUpdateSchema> | z.infer<typeof fullOrderSchema> | undefined;
|
||||
if (json.o) {
|
||||
const firstOrder = json.o[0];
|
||||
orderUpdate = firstOrder;
|
||||
}
|
||||
|
||||
this.subscriptions[
|
||||
SubscriptionType.ADDRESS_UPDATES_SUBSCRIBE
|
||||
]?.[json.id]?.callback({
|
||||
kind: 'update',
|
||||
order: orderUpdate,
|
||||
balances,
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MessageType.BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATE: {
|
||||
const brokerBalances: Partial<Record<string, number>> = {};
|
||||
|
||||
json.bb.forEach(([asset, balance]) => {
|
||||
brokerBalances[asset] = balance;
|
||||
});
|
||||
|
||||
this.subscriptions[
|
||||
SubscriptionType.BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATES_SUBSCRIBE
|
||||
]?.['default']?.callback(brokerBalances);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export * as schemas from './schemas/index.js';
|
||||
export {
|
||||
AggregatorWS,
|
||||
SubscriptionType,
|
||||
UnsubscriptionType,
|
||||
MessageType,
|
||||
};
|
||||
137
src/services/Aggregator/ws/schemas/addressUpdateSchema.ts
Normal file
137
src/services/Aggregator/ws/schemas/addressUpdateSchema.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { z } from 'zod';
|
||||
import { exchanges } from '../../../../constants/index.js';
|
||||
import orderStatuses from '../../../../constants/orderStatuses.js';
|
||||
import subOrderStatuses from '../../../../constants/subOrderStatuses.js';
|
||||
import MessageType from '../MessageType.js';
|
||||
import balancesSchema from './balancesSchema.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
import executionTypes from '../../../../constants/cfdExecutionTypes.js';
|
||||
|
||||
const baseAddressUpdate = baseMessageSchema.extend({
|
||||
id: z.string(),
|
||||
T: z.literal(MessageType.ADDRESS_UPDATE),
|
||||
S: z.string(), // subscription
|
||||
uc: z.array(z.enum(['b', 'o'])), // update content
|
||||
});
|
||||
|
||||
const subOrderSchema = z.object({
|
||||
i: z.number(), // id
|
||||
I: z.string(), // parent order id
|
||||
O: z.string(), // sender (owner)
|
||||
P: z.string().toUpperCase(), // asset pair
|
||||
s: z.enum(['BUY', 'SELL']), // side
|
||||
a: z.number(), // amount
|
||||
A: z.number(), // settled amount
|
||||
p: z.number(), // avg weighed settlement price
|
||||
e: z.enum(exchanges), // exchange
|
||||
b: z.string(), // broker address
|
||||
S: z.enum(subOrderStatuses), // status
|
||||
o: z.boolean(), // internal only
|
||||
});
|
||||
|
||||
export const orderUpdateSchema = z.object({
|
||||
I: z.string(), // id
|
||||
A: z.number(), // settled amount
|
||||
S: z.enum(orderStatuses), // status
|
||||
l: z.boolean().optional(), // is liquidation order
|
||||
t: z.number(), // update time
|
||||
E: z.enum(executionTypes).optional(), // execution type
|
||||
C: z.string().optional(), // trigger condition
|
||||
c: subOrderSchema.array(),
|
||||
})
|
||||
.transform((val) => ({
|
||||
...val,
|
||||
k: 'update' as const,
|
||||
})).transform((o) => ({
|
||||
kind: o.k,
|
||||
id: o.I,
|
||||
settledAmount: o.A,
|
||||
status: o.S,
|
||||
liquidated: o.l,
|
||||
executionType: o.E,
|
||||
triggerCondition: o.C,
|
||||
subOrders: o.c.map((so) => ({
|
||||
pair: so.P,
|
||||
exchange: so.e,
|
||||
id: so.i,
|
||||
amount: so.a,
|
||||
settledAmount: so.A,
|
||||
price: so.p,
|
||||
status: so.S,
|
||||
side: so.s,
|
||||
subOrdQty: so.A,
|
||||
})),
|
||||
}));
|
||||
|
||||
export const fullOrderSchema = z.object({
|
||||
I: z.string(), // id
|
||||
O: z.string(), // sender (owner)
|
||||
P: z.string().toUpperCase(), // asset pair
|
||||
s: z.enum(['BUY', 'SELL']), // side
|
||||
a: z.number(), // amount
|
||||
A: z.number(), // settled amount
|
||||
p: z.number(), // price
|
||||
F: z.string().toUpperCase(), // fee asset
|
||||
f: z.number(), // fee
|
||||
l: z.boolean().optional(), // is liquidation order
|
||||
L: z.number().optional(), // stop limit price,
|
||||
o: z.boolean(), // internal only
|
||||
S: z.enum(orderStatuses), // status
|
||||
T: z.number(), // creation time / unix timestamp
|
||||
t: z.number(), // update time
|
||||
c: subOrderSchema.array(),
|
||||
E: z.enum(executionTypes).optional(), // execution type
|
||||
C: z.string().optional(), // trigger condition
|
||||
}).transform((val) => ({
|
||||
...val,
|
||||
k: 'full' as const,
|
||||
})).transform((o) => ({
|
||||
kind: o.k,
|
||||
id: o.I,
|
||||
settledAmount: o.A,
|
||||
feeAsset: o.F,
|
||||
fee: o.f,
|
||||
liquidated: o.l,
|
||||
stopPrice: o.L,
|
||||
status: o.S,
|
||||
date: o.T,
|
||||
clientOrdId: o.O,
|
||||
type: o.s,
|
||||
pair: o.P,
|
||||
amount: o.a,
|
||||
price: o.p,
|
||||
executionType: o.E,
|
||||
triggerCondition: o.C,
|
||||
subOrders: o.c.map((so) => ({
|
||||
pair: so.P,
|
||||
exchange: so.e,
|
||||
id: so.i,
|
||||
amount: so.a,
|
||||
settledAmount: so.A,
|
||||
price: so.p,
|
||||
status: so.S,
|
||||
side: so.s,
|
||||
subOrdQty: so.A,
|
||||
})),
|
||||
}));
|
||||
|
||||
const updateMessageSchema = baseAddressUpdate.extend({
|
||||
k: z.literal('u'), // kind of message: "u" - updates
|
||||
uc: z.array(z.enum(['b', 'o'])), // update content: "o" - orders updates, "b" - balance updates
|
||||
b: balancesSchema.optional(),
|
||||
o: z.tuple([fullOrderSchema.or(orderUpdateSchema)]).optional(),
|
||||
});
|
||||
|
||||
const initialMessageSchema = baseAddressUpdate.extend({
|
||||
k: z.literal('i'), // kind of message: "i" - initial
|
||||
b: balancesSchema,
|
||||
o: z.array(fullOrderSchema)
|
||||
.optional(), // When no orders — no field
|
||||
});
|
||||
|
||||
const addressUpdateSchema = z.union([
|
||||
initialMessageSchema,
|
||||
updateMessageSchema,
|
||||
]);
|
||||
|
||||
export default addressUpdateSchema;
|
||||
16
src/services/Aggregator/ws/schemas/assetPairConfigSchema.ts
Normal file
16
src/services/Aggregator/ws/schemas/assetPairConfigSchema.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
import MessageType from '../MessageType.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
|
||||
const assetPairConfigSchema = baseMessageSchema.extend({
|
||||
id: z.string(),
|
||||
T: z.literal(MessageType.ASSET_PAIR_CONFIG_UPDATE),
|
||||
k: z.enum(['i', 'u']),
|
||||
u: z.tuple([
|
||||
z.string().toUpperCase(), // pairName
|
||||
z.number(), // minQty
|
||||
z.number().int(), // pricePrecision
|
||||
]),
|
||||
});
|
||||
|
||||
export default assetPairConfigSchema;
|
||||
18
src/services/Aggregator/ws/schemas/assetPairsConfigSchema.ts
Normal file
18
src/services/Aggregator/ws/schemas/assetPairsConfigSchema.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
import MessageType from '../MessageType.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
|
||||
const assetPairsConfigSchema = baseMessageSchema.extend({
|
||||
id: z.string(),
|
||||
T: z.literal(MessageType.ASSET_PAIRS_CONFIG_UPDATE),
|
||||
k: z.enum(['i', 'u']),
|
||||
u: z.array(
|
||||
z.tuple([
|
||||
z.string().toUpperCase(), // pairName
|
||||
z.number(), // minQty
|
||||
z.number().int(), // pricePrecision
|
||||
]),
|
||||
),
|
||||
});
|
||||
|
||||
export default assetPairsConfigSchema;
|
||||
14
src/services/Aggregator/ws/schemas/balancesSchema.ts
Normal file
14
src/services/Aggregator/ws/schemas/balancesSchema.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
import { makePartial } from '../../../../utils/index.js';
|
||||
|
||||
const balancesSchema = z.record( // changed balances in format
|
||||
z.string().toUpperCase(), // asset
|
||||
z.tuple([
|
||||
z.string(), // tradable balance
|
||||
z.string(), // reserved balance
|
||||
z.string(), // contract balance
|
||||
z.string(), // wallet balance
|
||||
z.string(), // allowance
|
||||
]),
|
||||
).transform(makePartial);
|
||||
export default balancesSchema;
|
||||
9
src/services/Aggregator/ws/schemas/baseMessageSchema.ts
Normal file
9
src/services/Aggregator/ws/schemas/baseMessageSchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
import MessageType from '../MessageType.js';
|
||||
|
||||
const baseMessageSchema = z.object({
|
||||
T: z.nativeEnum(MessageType),
|
||||
_: z.number(),
|
||||
});
|
||||
|
||||
export default baseMessageSchema;
|
||||
15
src/services/Aggregator/ws/schemas/brokerMessageSchema.ts
Normal file
15
src/services/Aggregator/ws/schemas/brokerMessageSchema.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
import MessageType from '../MessageType.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
|
||||
const brokerMessageSchema = baseMessageSchema.extend({
|
||||
T: z.literal(MessageType.BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATE),
|
||||
bb: z.array(
|
||||
z.tuple([
|
||||
z.string().toUpperCase(), // Asset name
|
||||
z.number(), // limit
|
||||
]),
|
||||
),
|
||||
});
|
||||
|
||||
export default brokerMessageSchema;
|
||||
33
src/services/Aggregator/ws/schemas/cfdAddressUpdateSchema.ts
Normal file
33
src/services/Aggregator/ws/schemas/cfdAddressUpdateSchema.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
import { fullOrderSchema, orderUpdateSchema } from './addressUpdateSchema.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
import MessageType from '../MessageType.js';
|
||||
import cfdBalancesSchema from './cfdBalancesSchema.js';
|
||||
|
||||
const baseCfdAddressUpdate = baseMessageSchema.extend({
|
||||
id: z.string(),
|
||||
T: z.literal(MessageType.CFD_ADDRESS_UPDATE),
|
||||
S: z.string(), // subscription
|
||||
uc: z.array(z.enum(['b', 'o'])), // update content
|
||||
});
|
||||
|
||||
const updateMessageSchema = baseCfdAddressUpdate.extend({
|
||||
k: z.literal('u'), // kind of message: "u" - updates
|
||||
uc: z.array(z.enum(['b', 'o'])), // update content: "o" - orders updates, "b" - balance updates
|
||||
b: cfdBalancesSchema.optional(),
|
||||
o: z.tuple([fullOrderSchema.or(orderUpdateSchema)]).optional(),
|
||||
});
|
||||
|
||||
const initialMessageSchema = baseCfdAddressUpdate.extend({
|
||||
k: z.literal('i'), // kind of message: "i" - initial
|
||||
b: cfdBalancesSchema,
|
||||
o: z.array(fullOrderSchema)
|
||||
.optional(), // When no orders — no field
|
||||
});
|
||||
|
||||
const cfdAddressUpdateSchema = z.union([
|
||||
initialMessageSchema,
|
||||
updateMessageSchema,
|
||||
]);
|
||||
|
||||
export default cfdAddressUpdateSchema
|
||||
52
src/services/Aggregator/ws/schemas/cfdBalancesSchema.ts
Normal file
52
src/services/Aggregator/ws/schemas/cfdBalancesSchema.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { z } from 'zod';
|
||||
import positionStatuses from '../../../../constants/positionStatuses.js';
|
||||
|
||||
const cfdBalanceSchema = z
|
||||
.object({
|
||||
i: z.string(),
|
||||
b: z.string(),
|
||||
pnl: z.string(),
|
||||
fr: z.string(),
|
||||
e: z.string(),
|
||||
p: z.string(),
|
||||
cp: z.string(),
|
||||
pp: z.string(),
|
||||
r: z.string(),
|
||||
m: z.string(),
|
||||
mu: z.string(),
|
||||
fmu: z.string(),
|
||||
awb: z.string(),
|
||||
l: z.string(),
|
||||
s: z.enum(positionStatuses),
|
||||
lfrs: z.string(),
|
||||
lfrd: z.string(),
|
||||
sfrs: z.string(),
|
||||
sfrd: z.string(),
|
||||
sop: z.string().optional(),
|
||||
})
|
||||
.transform((obj) => ({
|
||||
instrument: obj.i,
|
||||
balance: obj.b,
|
||||
profitLoss: obj.pnl,
|
||||
fundingRate: obj.fr,
|
||||
equity: obj.e,
|
||||
position: obj.p,
|
||||
currentPrice: obj.cp,
|
||||
positionPrice: obj.pp,
|
||||
reserves: obj.r,
|
||||
margin: obj.m,
|
||||
marginUSD: obj.mu,
|
||||
freeMarginUSD: obj.fmu,
|
||||
availableWithdrawBalance: obj.awb,
|
||||
leverage: obj.l,
|
||||
status: obj.s,
|
||||
longFundingRatePerSecond: obj.lfrs,
|
||||
longFundingRatePerDay: obj.lfrd,
|
||||
shortFundingRatePerSecond: obj.sfrs,
|
||||
shortFundingRatePerDay: obj.sfrd,
|
||||
stopOutPrice: obj.sop,
|
||||
}));
|
||||
|
||||
const cfdBalancesSchema = z.array(cfdBalanceSchema);
|
||||
|
||||
export default cfdBalancesSchema;
|
||||
12
src/services/Aggregator/ws/schemas/errorSchema.ts
Normal file
12
src/services/Aggregator/ws/schemas/errorSchema.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
import MessageType from '../MessageType.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
|
||||
const errorSchema = baseMessageSchema.extend({
|
||||
T: z.literal(MessageType.ERROR),
|
||||
c: z.number().int(), // code
|
||||
id: z.string().optional(), // subscription id
|
||||
m: z.string(), // error message,
|
||||
});
|
||||
|
||||
export default errorSchema;
|
||||
16
src/services/Aggregator/ws/schemas/futuresTradeInfoSchema.ts
Normal file
16
src/services/Aggregator/ws/schemas/futuresTradeInfoSchema.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
import MessageType from '../MessageType.js';
|
||||
|
||||
const futuresTradeInfoSchema = z.object({
|
||||
T: z.literal(MessageType.FUTURES_TRADE_INFO_UPDATE),
|
||||
id: z.string(), // trade info request UUID, set by client side
|
||||
S: z.string(), // sender
|
||||
i: z.string(), // instrument
|
||||
bp: z.number().optional(), // buy price
|
||||
sp: z.number().optional(), // sell price
|
||||
bpw: z.number(), // buy power
|
||||
spw: z.number(), // sell power
|
||||
ma: z.number(), // min amount
|
||||
});
|
||||
|
||||
export default futuresTradeInfoSchema;
|
||||
12
src/services/Aggregator/ws/schemas/index.ts
Normal file
12
src/services/Aggregator/ws/schemas/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { default as addressUpdateSchema } from './addressUpdateSchema.js';
|
||||
export { default as assetPairsConfigSchema } from './assetPairsConfigSchema.js';
|
||||
export { default as baseMessageSchema } from './baseMessageSchema.js';
|
||||
export { default as brokerMessageSchema } from './brokerMessageSchema.js';
|
||||
export { default as errorSchema } from './errorSchema.js';
|
||||
export { default as initMessageSchema } from './initMessageSchema.js';
|
||||
export { default as pingPongMessageSchema } from './pingPongMessageSchema.js';
|
||||
export { default as swapInfoSchema } from './swapInfoSchema.js';
|
||||
export { default as balancesSchema } from './balancesSchema.js';
|
||||
export { default as cfdBalancesSchema } from './cfdBalancesSchema.js';
|
||||
|
||||
export * from './orderBookSchema.js';
|
||||
10
src/services/Aggregator/ws/schemas/initMessageSchema.ts
Normal file
10
src/services/Aggregator/ws/schemas/initMessageSchema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import MessageType from '../MessageType.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
|
||||
const initMessageSchema = baseMessageSchema.extend({
|
||||
T: z.literal(MessageType.INITIALIZATION),
|
||||
i: z.string(),
|
||||
});
|
||||
|
||||
export default initMessageSchema;
|
||||
26
src/services/Aggregator/ws/schemas/orderBookSchema.ts
Normal file
26
src/services/Aggregator/ws/schemas/orderBookSchema.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
import exchanges from '../../../../constants/exchanges.js';
|
||||
import MessageType from '../MessageType.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
|
||||
export const orderBookItemSchema = z.tuple([
|
||||
z.string(), // price
|
||||
z.string(), // size
|
||||
z.array(
|
||||
z.enum(exchanges),
|
||||
), // exchanges
|
||||
z.array(z.tuple([
|
||||
z.enum(['SELL', 'BUY']), // side
|
||||
z.string().toUpperCase(), // pairname
|
||||
])),
|
||||
]);
|
||||
|
||||
export const orderBookSchema = baseMessageSchema.extend({
|
||||
// id: z.string(),
|
||||
T: z.literal(MessageType.AGGREGATED_ORDER_BOOK_UPDATE),
|
||||
S: z.string(),
|
||||
ob: z.object({
|
||||
a: z.array(orderBookItemSchema),
|
||||
b: z.array(orderBookItemSchema),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
import MessageType from '../MessageType.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
|
||||
const pingPongMessageSchema = baseMessageSchema.extend({
|
||||
T: z.literal(MessageType.PING_PONG),
|
||||
});
|
||||
|
||||
export default pingPongMessageSchema;
|
||||
59
src/services/Aggregator/ws/schemas/swapInfoSchema.ts
Normal file
59
src/services/Aggregator/ws/schemas/swapInfoSchema.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { z } from 'zod';
|
||||
import exchanges from '../../../../constants/exchanges.js';
|
||||
import MessageType from '../MessageType.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
|
||||
const alternativeSchema = z.object({ // execution alternatives
|
||||
e: z.enum(exchanges).array(), // exchanges
|
||||
ps: z.string().array(), // path
|
||||
mo: z.number().optional(), // market amount out
|
||||
mi: z.number().optional(), // market amount in
|
||||
mp: z.number(), // market price
|
||||
aa: z.number().optional(), // available amount in
|
||||
aao: z.number().optional(), // available amount out
|
||||
});
|
||||
const swapInfoSchemaBase = baseMessageSchema.extend({
|
||||
T: z.literal(MessageType.SWAP_INFO),
|
||||
S: z.string(), // swap request id
|
||||
ai: z.string().toUpperCase(), // asset in,
|
||||
ao: z.string().toUpperCase(), // asset out
|
||||
a: z.number(), // amount in
|
||||
o: z.number(), // amount out
|
||||
ma: z.number(), // min amount in
|
||||
mao: z.number(), // min amount out
|
||||
ps: z.string().array(), // path
|
||||
po: z.boolean(), // is swap through pool optimal
|
||||
e: z.enum(exchanges).array().optional(), // Exchanges
|
||||
p: z.number().optional(), // price
|
||||
mp: z.number().optional(), // market price
|
||||
oi: z.object({ // info about order equivalent to this swap
|
||||
p: z.string().toUpperCase(), // asset pair
|
||||
s: z.enum(['SELL', 'BUY']), // side
|
||||
a: z.number(), // amount
|
||||
sp: z.number(), // safe price (with safe deviation but without slippage)
|
||||
}).optional(),
|
||||
as: alternativeSchema.array(),
|
||||
});
|
||||
|
||||
const swapInfoSchemaByAmountIn = swapInfoSchemaBase.extend({
|
||||
mo: z.number().optional(), // market amount out
|
||||
aa: z.number(), // available amount in
|
||||
}).transform((content) => ({
|
||||
...content,
|
||||
k: 'exactSpend' as const,
|
||||
}));
|
||||
|
||||
const swapInfoSchemaByAmountOut = swapInfoSchemaBase.extend({
|
||||
mi: z.number().optional(), // market amount in
|
||||
aao: z.number(), // available amount out
|
||||
}).transform((content) => ({
|
||||
...content,
|
||||
k: 'exactReceive' as const,
|
||||
}));
|
||||
|
||||
const swapInfoSchema = z.union([
|
||||
swapInfoSchemaByAmountIn,
|
||||
swapInfoSchemaByAmountOut,
|
||||
]);
|
||||
|
||||
export default swapInfoSchema;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import MessageType from '../MessageType.js';
|
||||
import baseMessageSchema from './baseMessageSchema.js';
|
||||
|
||||
const unsubscriptionDoneSchema = baseMessageSchema.extend({
|
||||
id: z.string(),
|
||||
T: z.literal(MessageType.UNSUBSCRIPTION_DONE),
|
||||
});
|
||||
|
||||
export default unsubscriptionDoneSchema;
|
||||
Reference in New Issue
Block a user