Strictest / tests / minor improvements

This commit is contained in:
Aleksandr Kraiz
2023-02-17 17:31:35 +04:00
parent 11c8348ee9
commit 2c24f71453
39 changed files with 566 additions and 128 deletions

View File

@@ -9,7 +9,7 @@ import errorSchema from './schemas/errorSchema';
import placeAtomicSwapSchema from './schemas/placeAtomicSwapSchema';
import { OrionAggregatorWS } from './ws';
import { atomicSwapHistorySchema } from './schemas/atomicSwapHistorySchema';
import { type Exchange, type SignedCancelOrderRequest, type SignedCFDOrder, type SignedOrder } from '../../types';
import type { Exchange, SignedCancelOrderRequest, SignedCFDOrder, SignedOrder } from '../../types';
import { pairConfigSchema } from './schemas';
import {
aggregatedOrderbookSchema, exchangeOrderbookSchema, poolReservesSchema,
@@ -17,6 +17,8 @@ import {
import type networkCodes from '../../constants/networkCodes';
import toUpperCase from '../../utils/toUpperCase';
import httpToWS from '../../utils/httpToWS';
import { ethers } from 'ethers';
import orderSchema from './schemas/orderSchema';
class OrionAggregator {
private readonly apiUrl: string;
@@ -55,6 +57,27 @@ class OrionAggregator {
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') => {
@@ -124,6 +147,17 @@ class OrionAggregator {
);
};
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,

View File

@@ -0,0 +1,101 @@
import { ethers } from 'ethers';
import { z } from 'zod';
import { exchanges, orderStatuses, subOrderStatuses } from '../../../constants';
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}`,
})),
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,
matchedSubOrderId: z.number().int().nonnegative(),
exchangeTradeInfo: z.boolean(),
poolTradeInfo: z.boolean(),
});
const baseOrderSchema = z.object({
assetPair: z.string(),
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 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.literal('ORION_BROKER').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(),
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

@@ -1,6 +1,13 @@
import { z } from 'zod';
import { exchanges } from '../../../constants';
const orderInfoSchema = z.object({
assetPair: z.string(),
side: z.enum(['BUY', 'SELL']),
amount: z.number(),
safePrice: z.number(),
}).nullable();
const swapInfoBase = z.object({
id: z.string(),
amountIn: z.number(),
@@ -10,12 +17,7 @@ const swapInfoBase = z.object({
path: z.array(z.string()),
// isThroughPoolOptimal: z.boolean(), // deprecated
executionInfo: z.string(),
orderInfo: z.object({
assetPair: z.string(),
side: z.enum(['BUY', 'SELL']),
amount: z.number(),
safePrice: z.number(),
}).nullable(),
orderInfo: orderInfoSchema,
exchanges: z.array(z.enum(exchanges)),
price: z.number().nullable(), // spending asset price
minAmountOut: z.number(),
@@ -29,11 +31,13 @@ const swapInfoBase = z.object({
action: z.string(),
}).array(),
}),
marketAmountOut: z.number().optional(),
marketAmountIn: z.number().optional(),
marketAmountOut: z.number().nullable(),
marketAmountIn: z.number().nullable(),
marketPrice: z.number(),
availableAmountIn: z.number().optional(),
availableAmountOut: z.number().optional(),
availableAmountIn: z.number().nullable(),
availableAmountOut: z.number().nullable(),
orderInfo: orderInfoSchema,
isThroughPoolOrCurve: z.boolean(),
}).array(),
});

View File

@@ -9,13 +9,13 @@ import {
assetPairsConfigSchema, addressUpdateSchema, swapInfoSchema,
} from './schemas';
import UnsubscriptionType from './UnsubscriptionType';
import {
type SwapInfoBase, type AssetPairUpdate, type OrderbookItem,
type Balance, type Exchange, type CFDBalance, type FuturesTradeInfo, type SwapInfo,
import type {
SwapInfoBase, AssetPairUpdate, OrderbookItem,
Balance, Exchange, CFDBalance, FuturesTradeInfo, SwapInfo,
} from '../../../types';
import unsubscriptionDoneSchema from './schemas/unsubscriptionDoneSchema';
import assetPairConfigSchema from './schemas/assetPairConfigSchema';
import { type fullOrderSchema, type orderUpdateSchema } from './schemas/addressUpdateSchema';
import type { fullOrderSchema, orderUpdateSchema } from './schemas/addressUpdateSchema';
import cfdAddressUpdateSchema from './schemas/cfdAddressUpdateSchema';
import futuresTradeInfoSchema from './schemas/futuresTradeInfoSchema';
// import errorSchema from './schemas/errorSchema';
@@ -79,35 +79,35 @@ type FuturesTradeInfoSubscription = {
type AddressUpdateUpdate = {
kind: 'update'
balances: Partial<
Record<
string,
Balance
Record<
string,
Balance
>
>
>
order?: z.infer<typeof orderUpdateSchema> | z.infer<typeof fullOrderSchema>
order?: z.infer<typeof orderUpdateSchema> | z.infer<typeof fullOrderSchema> | undefined
}
type AddressUpdateInitial = {
kind: 'initial'
balances: Partial<
Record<
string,
Balance
Record<
string,
Balance
>
>
>
orders?: Array<z.infer<typeof fullOrderSchema>> // The field is not defined if the user has no orders
orders?: Array<z.infer<typeof fullOrderSchema>> | undefined // The field is not defined if the user has no orders
}
type CfdAddressUpdateUpdate = {
kind: 'update'
balances?: CFDBalance[]
order?: z.infer<typeof orderUpdateSchema> | z.infer<typeof fullOrderSchema>
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>> // The field is not defined if the user has no orders
orders?: Array<z.infer<typeof fullOrderSchema>> | undefined // The field is not defined if the user has no orders
}
type AddressUpdateSubscription = {
@@ -156,7 +156,7 @@ type BufferLike =
const isSubType = (subType: string): subType is keyof Subscription => Object.values(SubscriptionType).some((t) => t === subType);
class OrionAggregatorWS {
private ws: WebSocket | undefined;
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)`
@@ -168,11 +168,11 @@ class OrionAggregatorWS {
[K in keyof Subscription]: Partial<Record<string, Subscription[K]>>
}> = {};
public onInit?: () => void;
public onInit: (() => void) | undefined
public onError?: (err: string) => void;
public onError: ((err: string) => void) | undefined
public logger?: (message: string) => void;
public logger: ((message: string) => void) | undefined
private readonly wsUrl: string;
@@ -229,15 +229,15 @@ class OrionAggregatorWS {
? ((subscription as any).payload as string) // TODO: Refactor!!!
: uuidv4();
const subRequest: Partial<Record<string, unknown>> = {};
subRequest.T = type;
subRequest.id = id;
subRequest['T'] = type;
subRequest['id'] = id;
// TODO Refactor this
if ('payload' in subscription) {
if (typeof subscription.payload === 'string') {
subRequest.S = subscription.payload;
subRequest['S'] = subscription.payload;
} else {
subRequest.S = {
subRequest['S'] = {
d: id,
...subscription.payload,
};
@@ -296,9 +296,9 @@ class OrionAggregatorWS {
}
}
} else if (subscription === UnsubscriptionType.ASSET_PAIRS_CONFIG_UPDATES_UNSUBSCRIBE) {
delete this.subscriptions[SubscriptionType.ASSET_PAIRS_CONFIG_UPDATES_SUBSCRIBE]?.default;
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;
delete this.subscriptions[SubscriptionType.BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATES_SUBSCRIBE]?.['default'];
}
}
@@ -503,7 +503,7 @@ class OrionAggregatorWS {
this.subscriptions[
SubscriptionType.ASSET_PAIRS_CONFIG_UPDATES_SUBSCRIBE
]?.default?.callback({
]?.['default']?.callback({
kind: json.k === 'i' ? 'initial' : 'update',
data: priceUpdates,
});
@@ -612,7 +612,7 @@ class OrionAggregatorWS {
this.subscriptions[
SubscriptionType.BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATES_SUBSCRIBE
]?.default?.callback(brokerBalances);
]?.['default']?.callback(brokerBalances);
}
break;
default:

View File

@@ -3,6 +3,15 @@ import exchanges from '../../../../constants/exchanges';
import MessageType from '../MessageType';
import baseMessageSchema from './baseMessageSchema';
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
@@ -23,15 +32,7 @@ const swapInfoSchemaBase = baseMessageSchema.extend({
a: z.number(), // amount
sp: z.number(), // safe price (with safe deviation but without slippage)
}).optional(),
as: 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
}).array(),
as: alternativeSchema.array(),
});
const swapInfoSchemaByAmountIn = swapInfoSchemaBase.extend({

View File

@@ -16,7 +16,7 @@ import {
import type redeemOrderSchema from '../OrionAggregator/schemas/redeemOrderSchema';
import { sourceAtomicHistorySchema, targetAtomicHistorySchema } from './schemas/atomicHistorySchema';
import { makePartial } from '../../utils';
import { type networkCodes } from '../../constants';
import type { networkCodes } from '../../constants';
type IAdminAuthHeaders = {
auth: string

View File

@@ -1,5 +1,5 @@
import fetchWithValidation from '../../fetchWithValidation';
import { type Exchange } from '../../types';
import type { Exchange } from '../../types';
import { statisticsOverviewSchema, topPairsStatisticsSchema } from './schemas';
import candlesSchema from './schemas/candlesSchema';
import { PriceFeedWS } from './ws';