Semantics improvements

This commit is contained in:
Aleksandr Kraiz
2023-05-16 23:21:45 +04:00
parent 188e7bb317
commit cd4eff76d3
88 changed files with 419 additions and 478 deletions

View 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 };

View 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(),
});

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,6 @@
import { z } from 'zod';
import pairConfigSchema from './pairConfigSchema.js';
const exchangeInfoSchema = z.array(pairConfigSchema);
export default exchangeInfoSchema;

View 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';

View File

@@ -0,0 +1,8 @@
import { z } from 'zod';
const orderBenefitsSchema = z.record(z.object({
benefitBtc: z.string(),
benefitPct: z.string(),
}));
export default orderBenefitsSchema;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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,
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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

View 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;

View 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;

View 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;

View 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';

View 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;

View 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),
}),
});

View File

@@ -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;

View 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;

View File

@@ -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;