Merge branch 'dev'
This commit is contained in:
commit
905ea647f4
|
@ -1,6 +1,7 @@
|
|||
mango-service-v3/node_modules
|
||||
mango-service-v3/dist
|
||||
mango-service-v3/run-dev-server.sh
|
||||
mango-service-v3/run-dev-server-devnet.sh
|
||||
mango-service-v3/run-build-and-publish-docker-image.sh
|
||||
mango-service-v3/run-build-and-test-docker-locally.sh
|
||||
mango-service-v3/run-update-api-docs.sh
|
||||
|
|
|
@ -26,7 +26,6 @@ See Todos
|
|||
# Todos
|
||||
## Small
|
||||
losely sorted in order of importance/priority
|
||||
- devenet integration tests
|
||||
- retry framework in python
|
||||
- error thrown in endpoints just crashes expressjs, need explicit handling
|
||||
- rpc node related issues
|
||||
|
|
|
@ -11,4 +11,4 @@ RUN yarn build
|
|||
|
||||
ENV PORT=3000
|
||||
|
||||
CMD ["pm2-docker", "start", "dist/server.js", "-i", "1", "-n", "mango-service-v3", "--max-memory-restart", "200M"]
|
||||
CMD ["pm2-docker", "start", "dist/server.js", "-i", "2", "-n", "mango-service-v3", "--max-memory-restart", "200M"]
|
||||
|
|
|
@ -5,8 +5,14 @@ import {
|
|||
} from "@blockworks-foundation/mango-client";
|
||||
import BN from "bn.js";
|
||||
import Controller from "controller.interface";
|
||||
import { RequestErrorCustom } from "dtos";
|
||||
import { NextFunction, Request, Response, Router } from "express";
|
||||
import MangoSimpleClient from "mango.simple.client";
|
||||
import {
|
||||
logger,
|
||||
patchExternalMarketName,
|
||||
patchInternalMarketName,
|
||||
} from "./utils";
|
||||
|
||||
class AccountController implements Controller {
|
||||
public path = "/api/positions";
|
||||
|
@ -26,6 +32,19 @@ class AccountController implements Controller {
|
|||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
this.fetchPerpPositionsInternal()
|
||||
.then((postionDtos) => {
|
||||
response.send({ success: true, result: postionDtos } as PositionsDto);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private async fetchPerpPositionsInternal() {
|
||||
const groupConfig = this.mangoSimpleClient.mangoGroupConfig;
|
||||
const mangoGroup = this.mangoSimpleClient.mangoGroup;
|
||||
|
||||
|
@ -114,7 +133,7 @@ class AccountController implements Controller {
|
|||
cumulativeSellSize: undefined,
|
||||
entryPrice,
|
||||
estimatedLiquidationPrice: undefined,
|
||||
future: marketConfig.name,
|
||||
future: patchInternalMarketName(marketConfig.name),
|
||||
initialMarginRequirement: undefined,
|
||||
longOrderSize: undefined,
|
||||
maintenanceMarginRequirement: undefined,
|
||||
|
@ -133,9 +152,8 @@ class AccountController implements Controller {
|
|||
} as PositionDto;
|
||||
}
|
||||
);
|
||||
|
||||
response.send({ success: true, result: postionDtos } as PositionsDto);
|
||||
};
|
||||
return postionDtos;
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountController;
|
||||
|
@ -182,7 +200,7 @@ interface PositionDto {
|
|||
cumulativeSellSize: number;
|
||||
entryPrice: number;
|
||||
estimatedLiquidationPrice: number;
|
||||
future: "ETH-PERP";
|
||||
future: string;
|
||||
initialMarginRequirement: number;
|
||||
longOrderSize: number;
|
||||
maintenanceMarginRequirement: number;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import bodyParser from "body-parser";
|
||||
import Controller from "controller.interface";
|
||||
import express from "express";
|
||||
import { logger } from "./utils";
|
||||
import AccountController from "./account.controller";
|
||||
import CoinController from "./coin.controller";
|
||||
import MangoSimpleClient from "./mango.simple.client";
|
||||
|
@ -36,7 +37,9 @@ class App {
|
|||
}
|
||||
|
||||
public listen() {
|
||||
this.app.listen(process.env.PORT || 3000);
|
||||
const port = process.env.PORT || 3000;
|
||||
this.app.listen(port);
|
||||
logger.info(`App listening on port ${port}`);
|
||||
}
|
||||
|
||||
public getServer() {
|
||||
|
|
|
@ -5,6 +5,6 @@ export interface BadRequestError {
|
|||
location: string;
|
||||
}
|
||||
|
||||
export interface BadRequestErrorCustom {
|
||||
export interface RequestErrorCustom {
|
||||
msg: string;
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ class MangoSimpleClient {
|
|||
}
|
||||
|
||||
static async create() {
|
||||
const groupName = "mainnet.1";
|
||||
const groupName = process.env.GROUP_NAME || "mainnet.1";
|
||||
const clusterUrl =
|
||||
process.env.CLUSTER_URL || "https://api.mainnet-beta.solana.com";
|
||||
|
||||
|
|
|
@ -5,15 +5,21 @@ import {
|
|||
PerpMarket,
|
||||
} from "@blockworks-foundation/mango-client";
|
||||
import { Market } from "@project-serum/serum";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import Big from "big.js";
|
||||
import { BadRequestError } from "dtos";
|
||||
import { BadRequestError, RequestErrorCustom } from "dtos";
|
||||
import { NextFunction, Request, Response, Router } from "express";
|
||||
import { param, query, validationResult } from "express-validator";
|
||||
import fetch from "node-fetch";
|
||||
import { OrderInfo } from "types";
|
||||
import Controller from "./controller.interface";
|
||||
import MangoSimpleClient from "./mango.simple.client";
|
||||
import { isValidMarket } from "./utils";
|
||||
import {
|
||||
isValidMarket,
|
||||
logger,
|
||||
patchExternalMarketName,
|
||||
patchInternalMarketName,
|
||||
} from "./utils";
|
||||
|
||||
class MarketsController implements Controller {
|
||||
public path = "/api/markets";
|
||||
|
@ -64,10 +70,19 @@ class MarketsController implements Controller {
|
|||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
response.send({
|
||||
success: true,
|
||||
result: await this.fetchMarketsInternal(),
|
||||
} as MarketsDto);
|
||||
this.fetchMarketsInternal()
|
||||
.then((marketsDto) => {
|
||||
response.send({
|
||||
success: true,
|
||||
result: marketsDto,
|
||||
} as MarketsDto);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private fetchMarket = async (
|
||||
|
@ -82,11 +97,21 @@ class MarketsController implements Controller {
|
|||
.json({ errors: errors.array() as BadRequestError[] });
|
||||
}
|
||||
|
||||
const marketName = request.params.market_name;
|
||||
response.send({
|
||||
success: true,
|
||||
result: await this.fetchMarketsInternal(marketName),
|
||||
} as MarketsDto);
|
||||
const marketName = patchExternalMarketName(request.params.market_name);
|
||||
|
||||
this.fetchMarketsInternal(marketName)
|
||||
.then((marketsDto) => {
|
||||
response.send({
|
||||
success: true,
|
||||
result: marketsDto,
|
||||
} as MarketsDto);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private async fetchMarketsInternal(
|
||||
|
@ -191,7 +216,7 @@ class MarketsController implements Controller {
|
|||
}
|
||||
|
||||
return {
|
||||
name: marketConfig.name,
|
||||
name: patchInternalMarketName(marketConfig.name),
|
||||
baseCurrency: marketConfig.baseSymbol,
|
||||
quoteCurrency: "USDC",
|
||||
quoteVolume24h: volume,
|
||||
|
@ -227,34 +252,50 @@ class MarketsController implements Controller {
|
|||
.json({ errors: errors.array() as BadRequestError[] });
|
||||
}
|
||||
|
||||
const marketName = request.params.market_name;
|
||||
const marketName = patchExternalMarketName(request.params.market_name);
|
||||
const depth = Number(request.query.depth) || 20;
|
||||
|
||||
this.getOrderBookInternal(marketName, depth)
|
||||
.then(({ asks, bids }) => {
|
||||
return response.send({
|
||||
success: true,
|
||||
result: {
|
||||
asks: asks,
|
||||
bids: bids,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private async getOrderBookInternal(marketName: string, depth: number) {
|
||||
const ordersInfo = await this.mangoSimpleClient.fetchAllBidsAndAsks(
|
||||
false,
|
||||
marketName
|
||||
);
|
||||
const bids = ordersInfo
|
||||
const bids_ = ordersInfo
|
||||
.flat()
|
||||
.filter((orderInfo) => orderInfo.order.side === "buy")
|
||||
.sort((b1, b2) => b2.order.price - b1.order.price);
|
||||
const asks = ordersInfo
|
||||
const asks_ = ordersInfo
|
||||
.flat()
|
||||
.filter((orderInfo) => orderInfo.order.side === "sell")
|
||||
.sort((a1, a2) => a1.order.price - a2.order.price);
|
||||
|
||||
response.send({
|
||||
success: true,
|
||||
result: {
|
||||
asks: asks
|
||||
.slice(0, depth)
|
||||
.map((ask) => [ask.order.price, ask.order.size]),
|
||||
bids: bids
|
||||
.slice(0, depth)
|
||||
.map((bid) => [bid.order.price, bid.order.size]),
|
||||
},
|
||||
} as OrdersDto);
|
||||
};
|
||||
const asks = asks_
|
||||
.slice(0, depth)
|
||||
.map((ask) => [ask.order.price, ask.order.size]);
|
||||
|
||||
const bids = bids_
|
||||
.slice(0, depth)
|
||||
.map((bid) => [bid.order.price, bid.order.size]);
|
||||
return { asks, bids };
|
||||
}
|
||||
|
||||
private getTrades = async (
|
||||
request: Request,
|
||||
|
@ -271,33 +312,45 @@ class MarketsController implements Controller {
|
|||
const allMarketConfigs = getAllMarkets(
|
||||
this.mangoSimpleClient.mangoGroupConfig
|
||||
);
|
||||
const marketName = request.params.market_name;
|
||||
const marketName = patchExternalMarketName(request.params.market_name);
|
||||
const marketPk = allMarketConfigs.filter(
|
||||
(marketConfig) => marketConfig.name === marketName
|
||||
)[0].publicKey;
|
||||
|
||||
this.getTradesInternal(marketPk)
|
||||
.then((tradeDtos) => {
|
||||
return response.send({
|
||||
success: true,
|
||||
result: tradeDtos,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private async getTradesInternal(marketPk: PublicKey) {
|
||||
const tradesResponse = await fetch(
|
||||
`https://serum-history.herokuapp.com/trades/address/${marketPk.toBase58()}`
|
||||
);
|
||||
const parsedTradesResponse = (await tradesResponse.json()) as any;
|
||||
let tradeDtos;
|
||||
if ("s" in parsedTradesResponse && parsedTradesResponse["s"] === "error") {
|
||||
tradeDtos = [];
|
||||
} else {
|
||||
tradeDtos = parsedTradesResponse["data"].map((trade: any) => {
|
||||
return {
|
||||
id: trade["orderId"],
|
||||
liquidation: undefined,
|
||||
price: trade["price"],
|
||||
side: trade["side"],
|
||||
size: trade["size"],
|
||||
time: new Date(trade["time"]),
|
||||
} as TradeDto;
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
response.send({ success: true, result: tradeDtos } as TradesDto);
|
||||
};
|
||||
return parsedTradesResponse["data"].map((trade: any) => {
|
||||
return {
|
||||
id: trade["orderId"],
|
||||
liquidation: undefined,
|
||||
price: trade["price"],
|
||||
side: trade["side"],
|
||||
size: trade["size"],
|
||||
time: new Date(trade["time"]),
|
||||
} as TradeDto;
|
||||
});
|
||||
}
|
||||
|
||||
private getCandles = async (
|
||||
request: Request,
|
||||
|
@ -311,31 +364,35 @@ class MarketsController implements Controller {
|
|||
.json({ errors: errors.array() as BadRequestError[] });
|
||||
}
|
||||
|
||||
const marketName = request.params.market_name;
|
||||
const marketName = patchExternalMarketName(request.params.market_name);
|
||||
const resolution = String(request.query.resolution);
|
||||
const fromEpochS = Number(request.query.start_time);
|
||||
const toEpochS = Number(request.query.end_time);
|
||||
|
||||
const { t, o, h, l, c, v } = await getOhlcv(
|
||||
marketName,
|
||||
resolution,
|
||||
fromEpochS,
|
||||
toEpochS
|
||||
);
|
||||
|
||||
const ohlcvDtos: OhlcvDto[] = [];
|
||||
for (let i = 0; i < t.length; i++) {
|
||||
ohlcvDtos.push({
|
||||
time: t[i],
|
||||
open: o[i],
|
||||
high: h[i],
|
||||
low: l[i],
|
||||
close: c[i],
|
||||
volume: v[i],
|
||||
} as OhlcvDto);
|
||||
}
|
||||
|
||||
response.send({ success: true, result: ohlcvDtos } as OhlcvsDto);
|
||||
await getOhlcv(marketName, resolution, fromEpochS, toEpochS)
|
||||
.then(({ t, o, h, l, c, v }) => {
|
||||
const ohlcvDtos: OhlcvDto[] = [];
|
||||
for (let i = 0; i < t.length; i++) {
|
||||
ohlcvDtos.push({
|
||||
time: t[i],
|
||||
open: o[i],
|
||||
high: h[i],
|
||||
low: l[i],
|
||||
close: c[i],
|
||||
volume: v[i],
|
||||
} as OhlcvDto);
|
||||
}
|
||||
return response.send({
|
||||
success: true,
|
||||
result: ohlcvDtos,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import { PerpOrder } from "@blockworks-foundation/mango-client";
|
||||
import { Order } from "@project-serum/serum/lib/market";
|
||||
import { BadRequestError, BadRequestErrorCustom } from "dtos";
|
||||
import { BadRequestError, RequestErrorCustom } from "dtos";
|
||||
import { NextFunction, Request, Response, Router } from "express";
|
||||
import { body, query, validationResult } from "express-validator";
|
||||
import Controller from "./controller.interface";
|
||||
import MangoSimpleClient from "./mango.simple.client";
|
||||
import { OrderInfo } from "./types";
|
||||
import { isValidMarket, logger } from "./utils";
|
||||
import {
|
||||
isValidMarket,
|
||||
logger,
|
||||
patchExternalMarketName,
|
||||
patchInternalMarketName,
|
||||
} from "./utils";
|
||||
|
||||
class OrdersController implements Controller {
|
||||
public path = "/api/orders";
|
||||
|
@ -67,61 +72,20 @@ class OrdersController implements Controller {
|
|||
.json({ errors: errors.array() as BadRequestError[] });
|
||||
}
|
||||
|
||||
const openOrders = await this.mangoSimpleClient.fetchAllBidsAndAsks(
|
||||
true,
|
||||
request.query.market ? String(request.query.market) : undefined
|
||||
);
|
||||
const marketName = request.query.market
|
||||
? patchExternalMarketName(String(request.query.market))
|
||||
: undefined;
|
||||
|
||||
const orderDtos = openOrders.flat().map((orderInfo: OrderInfo) => {
|
||||
if ("bestInitial" in orderInfo.order) {
|
||||
const perpOrder = orderInfo.order as PerpOrder;
|
||||
return {
|
||||
createdAt: new Date(perpOrder.timestamp.toNumber() * 1000),
|
||||
filledSize: undefined,
|
||||
future: orderInfo.market.config.name,
|
||||
id: perpOrder.orderId.toString(),
|
||||
market: orderInfo.market.config.name,
|
||||
price: perpOrder.price,
|
||||
avgFillPrice: undefined,
|
||||
remainingSize: undefined,
|
||||
side: perpOrder.side,
|
||||
size: perpOrder.size,
|
||||
status: "open",
|
||||
type: "limit",
|
||||
reduceOnly: undefined,
|
||||
ioc: undefined,
|
||||
postOnly: undefined,
|
||||
clientId:
|
||||
perpOrder.clientId && perpOrder.clientId.toString() !== "0"
|
||||
? perpOrder.clientId.toString()
|
||||
: undefined,
|
||||
} as OrderDto;
|
||||
}
|
||||
|
||||
const spotOrder = orderInfo.order as Order;
|
||||
return {
|
||||
createdAt: undefined,
|
||||
filledSize: undefined,
|
||||
future: orderInfo.market.config.name,
|
||||
id: spotOrder.orderId.toString(),
|
||||
market: orderInfo.market.config.name,
|
||||
price: spotOrder.price,
|
||||
avgFillPrice: undefined,
|
||||
remainingSize: undefined,
|
||||
side: spotOrder.side,
|
||||
size: spotOrder.size,
|
||||
status: "open",
|
||||
type: undefined,
|
||||
reduceOnly: undefined,
|
||||
ioc: undefined,
|
||||
postOnly: undefined,
|
||||
clientId:
|
||||
spotOrder.clientId && spotOrder.clientId.toString() !== "0"
|
||||
? spotOrder.clientId.toString()
|
||||
: undefined,
|
||||
} as OrderDto;
|
||||
});
|
||||
response.send({ success: true, result: orderDtos } as OrdersDto);
|
||||
this.getOpenOrdersInternal(marketName)
|
||||
.then((orderDtos) => {
|
||||
return response.send({ success: true, result: orderDtos } as OrdersDto);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private placeOrder = async (
|
||||
|
@ -139,9 +103,9 @@ class OrdersController implements Controller {
|
|||
const placeOrderDto = request.body as PlaceOrderDto;
|
||||
logger.info(`placing order`);
|
||||
|
||||
try {
|
||||
await this.mangoSimpleClient.placeOrder(
|
||||
placeOrderDto.market,
|
||||
this.mangoSimpleClient
|
||||
.placeOrder(
|
||||
patchExternalMarketName(placeOrderDto.market),
|
||||
placeOrderDto.type,
|
||||
placeOrderDto.side,
|
||||
placeOrderDto.size,
|
||||
|
@ -152,35 +116,16 @@ class OrdersController implements Controller {
|
|||
? "postOnly"
|
||||
: "limit",
|
||||
placeOrderDto.clientId
|
||||
);
|
||||
} catch (error) {
|
||||
return response.status(400).send({
|
||||
errors: [{ msg: error.message } as BadRequestErrorCustom],
|
||||
)
|
||||
.then(() => {
|
||||
return response.status(200).send();
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
response.send({
|
||||
success: true,
|
||||
result: {
|
||||
createdAt: new Date(),
|
||||
filledSize: undefined,
|
||||
future: placeOrderDto.market,
|
||||
id: undefined,
|
||||
market: placeOrderDto.market,
|
||||
price: undefined,
|
||||
remainingSize: undefined,
|
||||
side: placeOrderDto.side,
|
||||
size: placeOrderDto.size,
|
||||
status: undefined,
|
||||
type: placeOrderDto.type,
|
||||
reduceOnly: undefined,
|
||||
ioc: placeOrderDto.ioc,
|
||||
postOnly: placeOrderDto.postOnly,
|
||||
clientId: placeOrderDto.clientId
|
||||
? placeOrderDto.clientId.toString()
|
||||
: null,
|
||||
},
|
||||
} as PlaceOrderResponseDto);
|
||||
};
|
||||
|
||||
private cancelAllOrders = async (
|
||||
|
@ -189,9 +134,17 @@ class OrdersController implements Controller {
|
|||
next: NextFunction
|
||||
) => {
|
||||
logger.info(`cancelling all orders...`);
|
||||
// todo: leads to 429 if too many orders exist, needs optimization
|
||||
await this.mangoSimpleClient.cancelAllOrders();
|
||||
response.send();
|
||||
this.mangoSimpleClient
|
||||
.cancelAllOrders()
|
||||
.then(() => {
|
||||
return response.status(200).send();
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private cancelOrderByOrderId = async (
|
||||
|
@ -212,16 +165,16 @@ class OrdersController implements Controller {
|
|||
this.mangoSimpleClient
|
||||
.cancelOrder(orderInfos[0])
|
||||
.then(() => response.send())
|
||||
.catch(() => {
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response
|
||||
.status(400)
|
||||
.json({ errors: [{ msg: "Unexpected error occured!" }] });
|
||||
.status(500)
|
||||
.json({ errors: [{ msg: error.message }] });
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
return response
|
||||
.status(400)
|
||||
.json({ errors: [{ msg: "Unexpected error occured!" }] });
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).json({ errors: [{ msg: error.message }] });
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -243,18 +196,76 @@ class OrdersController implements Controller {
|
|||
this.mangoSimpleClient
|
||||
.cancelOrder(orderInfos[0])
|
||||
.then(() => response.send())
|
||||
.catch(() => {
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response
|
||||
.status(400)
|
||||
.json({ errors: [{ msg: "Unexpected error occured!" }] });
|
||||
.status(500)
|
||||
.json({ errors: [{ msg: error.message }] });
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
return response
|
||||
.status(400)
|
||||
.json({ errors: [{ msg: "Unexpected error occured!" }] });
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).json({ errors: [{ msg: error.message }] });
|
||||
});
|
||||
};
|
||||
|
||||
private async getOpenOrdersInternal(marketName: string) {
|
||||
const openOrders = await this.mangoSimpleClient.fetchAllBidsAndAsks(
|
||||
true,
|
||||
marketName
|
||||
);
|
||||
|
||||
const orderDtos = openOrders.flat().map((orderInfo: OrderInfo) => {
|
||||
if ("bestInitial" in orderInfo.order) {
|
||||
const perpOrder = orderInfo.order as PerpOrder;
|
||||
return {
|
||||
createdAt: new Date(perpOrder.timestamp.toNumber() * 1000),
|
||||
filledSize: undefined,
|
||||
future: patchInternalMarketName(orderInfo.market.config.name),
|
||||
id: perpOrder.orderId.toString(),
|
||||
market: patchInternalMarketName(orderInfo.market.config.name),
|
||||
price: perpOrder.price,
|
||||
avgFillPrice: undefined,
|
||||
remainingSize: undefined,
|
||||
side: perpOrder.side,
|
||||
size: perpOrder.size,
|
||||
status: "open",
|
||||
type: "limit",
|
||||
reduceOnly: undefined,
|
||||
ioc: undefined,
|
||||
postOnly: undefined,
|
||||
clientId:
|
||||
perpOrder.clientId && perpOrder.clientId.toString() !== "0"
|
||||
? perpOrder.clientId.toString()
|
||||
: undefined,
|
||||
} as OrderDto;
|
||||
}
|
||||
|
||||
const spotOrder = orderInfo.order as Order;
|
||||
return {
|
||||
createdAt: undefined,
|
||||
filledSize: undefined,
|
||||
future: patchInternalMarketName(orderInfo.market.config.name),
|
||||
id: spotOrder.orderId.toString(),
|
||||
market: patchInternalMarketName(orderInfo.market.config.name),
|
||||
price: spotOrder.price,
|
||||
avgFillPrice: undefined,
|
||||
remainingSize: undefined,
|
||||
side: spotOrder.side,
|
||||
size: spotOrder.size,
|
||||
status: "open",
|
||||
type: undefined,
|
||||
reduceOnly: undefined,
|
||||
ioc: undefined,
|
||||
postOnly: undefined,
|
||||
clientId:
|
||||
spotOrder.clientId && spotOrder.clientId.toString() !== "0"
|
||||
? spotOrder.clientId.toString()
|
||||
: undefined,
|
||||
} as OrderDto;
|
||||
});
|
||||
return orderDtos;
|
||||
}
|
||||
}
|
||||
|
||||
export default OrdersController;
|
||||
|
|
|
@ -30,7 +30,7 @@ const mangoGroupConfig: GroupConfig = Config.ids().groups.filter(
|
|||
)[0];
|
||||
|
||||
const allMarketNames = mangoGroupConfig.spotMarkets
|
||||
.map((spotMarketConfig) => spotMarketConfig.name)
|
||||
.map((spotMarketConfig) => spotMarketConfig.baseSymbol + "-SPOT")
|
||||
.concat(
|
||||
mangoGroupConfig.perpMarkets.map(
|
||||
(perpMarketConfig) => perpMarketConfig.name
|
||||
|
@ -41,6 +41,20 @@ const allCoins = mangoGroupConfig.tokens.map(
|
|||
(tokenConfig) => tokenConfig.symbol
|
||||
);
|
||||
|
||||
export function patchExternalMarketName(marketName: string) {
|
||||
if (marketName.includes("-SPOT")) {
|
||||
marketName = marketName.replace("-SPOT", "/USDC");
|
||||
}
|
||||
return marketName;
|
||||
}
|
||||
|
||||
export function patchInternalMarketName(marketName: string) {
|
||||
if (marketName.includes("/USDC")) {
|
||||
marketName = marketName.replace("/USDC", "-SPOT");
|
||||
}
|
||||
return marketName;
|
||||
}
|
||||
|
||||
/// general
|
||||
|
||||
export function zipDict<K extends string | number | symbol, V>(
|
||||
|
|
|
@ -7,13 +7,18 @@ import {
|
|||
} from "@blockworks-foundation/mango-client";
|
||||
import { OpenOrders } from "@project-serum/serum";
|
||||
import Controller from "controller.interface";
|
||||
import { BadRequestErrorCustom } from "dtos";
|
||||
import { RequestErrorCustom } from "dtos";
|
||||
import e, { NextFunction, Request, Response, Router } from "express";
|
||||
import { body } from "express-validator";
|
||||
import { sumBy } from "lodash";
|
||||
import MangoSimpleClient from "mango.simple.client";
|
||||
import { Balances } from "./types";
|
||||
import { i80f48ToPercent, isValidCoin } from "./utils";
|
||||
import {
|
||||
i80f48ToPercent,
|
||||
isValidCoin,
|
||||
logger,
|
||||
patchInternalMarketName,
|
||||
} from "./utils";
|
||||
|
||||
class WalletController implements Controller {
|
||||
public path = "/api/wallet";
|
||||
|
@ -41,6 +46,22 @@ class WalletController implements Controller {
|
|||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
this.fetchBalancesInternal()
|
||||
.then((balanceDtos) => {
|
||||
return response.send({
|
||||
success: true,
|
||||
result: balanceDtos,
|
||||
} as BalancesDto);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private async fetchBalancesInternal() {
|
||||
// local copies of mango objects
|
||||
const mangoGroupConfig = this.mangoSimpleClient.mangoGroupConfig;
|
||||
const mangoGroup = this.mangoSimpleClient.mangoGroup;
|
||||
|
@ -67,7 +88,7 @@ class WalletController implements Controller {
|
|||
name,
|
||||
} of mangoGroupConfig.spotMarkets) {
|
||||
if (!mangoAccount || !mangoGroup) {
|
||||
response.send([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
const openOrders: OpenOrders =
|
||||
|
@ -198,11 +219,10 @@ class WalletController implements Controller {
|
|||
const value = net.mul(mangoGroup.getPrice(tokenIndex, mangoCache));
|
||||
/* tslint:enable */
|
||||
////// end of copy pasta block from mango-ui-v3
|
||||
|
||||
// append balances for base symbols
|
||||
const balanceDtos = baseBalances.map((baseBalance) => {
|
||||
return {
|
||||
coin: baseBalance.key,
|
||||
coin: patchInternalMarketName(baseBalance.key),
|
||||
free: baseBalance.deposits.toNumber(),
|
||||
spotBorrow: baseBalance.borrows.toNumber(),
|
||||
total: baseBalance.net.toNumber(),
|
||||
|
@ -215,16 +235,17 @@ class WalletController implements Controller {
|
|||
|
||||
// append balance for quote symbol
|
||||
balanceDtos.push({
|
||||
coin: this.mangoSimpleClient.mangoGroupConfig.quoteSymbol,
|
||||
coin: patchInternalMarketName(
|
||||
this.mangoSimpleClient.mangoGroupConfig.quoteSymbol
|
||||
),
|
||||
free: quoteMeta.deposits.toNumber(),
|
||||
spotBorrow: quoteMeta.borrows.toNumber(),
|
||||
total: net.toNumber(),
|
||||
usdValue: value.toNumber(),
|
||||
availableWithoutBorrow: net.sub(quoteMeta.borrows).toNumber(),
|
||||
});
|
||||
|
||||
response.send({ success: true, result: balanceDtos } as BalancesDto);
|
||||
};
|
||||
return balanceDtos;
|
||||
}
|
||||
|
||||
private withdraw = async (
|
||||
request: Request,
|
||||
|
@ -238,8 +259,9 @@ class WalletController implements Controller {
|
|||
response.status(200);
|
||||
})
|
||||
.catch((error) => {
|
||||
return response.status(400).send({
|
||||
errors: [{ msg: error.message } as BadRequestErrorCustom],
|
||||
logger.error(`message - ${error.message}, ${error.stack}`);
|
||||
return response.status(500).send({
|
||||
errors: [{ msg: error.message } as RequestErrorCustom],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import time
|
||||
|
||||
from mango_service_v3_py.api import MangoServiceV3Client
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
mango_service_v3_client = MangoServiceV3Client()
|
||||
sleep = 0.5
|
||||
while True:
|
||||
try:
|
||||
resp = mango_service_v3_client.get_balances()
|
||||
print(resp)
|
||||
sleep = sleep * 2
|
||||
time.sleep(sleep)
|
||||
except:
|
||||
pass
|
|
@ -33,26 +33,29 @@ def delayed(seconds):
|
|||
|
||||
|
||||
class MangoServiceV3Client:
|
||||
def __init__(self):
|
||||
if "BASE_URL" in os.environ:
|
||||
self.BASE_URL = f"{os.environ['BASE_URL']}/api"
|
||||
def __init__(self, base_url, timeout):
|
||||
self.timeout = timeout if timeout else 10.0
|
||||
if base_url:
|
||||
self.BASE_URL = base_url
|
||||
else:
|
||||
self.BASE_URL = "http://localhost:3000/api"
|
||||
|
||||
def get_open_positions(self) -> List[Position]:
|
||||
response = httpx.get(f"{self.BASE_URL}/positions", timeout=10.0)
|
||||
response = httpx.get(f"{self.BASE_URL}/positions", timeout=self.timeout)
|
||||
return parse_obj_as(List[Position], json.loads(response.text)["result"])
|
||||
|
||||
def get_balances(self) -> List[Balance]:
|
||||
response = httpx.get(f"{self.BASE_URL}/wallet/balances", timeout=10.0)
|
||||
response = httpx.get(f"{self.BASE_URL}/wallet/balances", timeout=self.timeout)
|
||||
return parse_obj_as(List[Balance], json.loads(response.text)["result"])
|
||||
|
||||
def get_markets(self) -> List[Market]:
|
||||
response = httpx.get(f"{self.BASE_URL}/markets", timeout=10.0)
|
||||
response = httpx.get(f"{self.BASE_URL}/markets", timeout=self.timeout)
|
||||
return parse_obj_as(List[Market], json.loads(response.text)["result"])
|
||||
|
||||
def get_market_by_market_name(self, market_name: str) -> List[Market]:
|
||||
response = httpx.get(f"{self.BASE_URL}/markets/{market_name}", timeout=10.0)
|
||||
response = httpx.get(
|
||||
f"{self.BASE_URL}/markets/{market_name}", timeout=self.timeout
|
||||
)
|
||||
return parse_obj_as(List[Market], json.loads(response.text)["result"])
|
||||
|
||||
def get_orderboook(self, market_name: str, depth: int = 30) -> Orderbook:
|
||||
|
@ -63,7 +66,7 @@ class MangoServiceV3Client:
|
|||
|
||||
def get_trades(self, market_name: str) -> List[Trade]:
|
||||
response = httpx.get(
|
||||
f"{self.BASE_URL}/markets/{market_name}/trades", timeout=10.0
|
||||
f"{self.BASE_URL}/markets/{market_name}/trades", timeout=self.timeout
|
||||
)
|
||||
return parse_obj_as(List[Trade], json.loads(response.text)["result"])
|
||||
|
||||
|
@ -76,34 +79,34 @@ class MangoServiceV3Client:
|
|||
return parse_obj_as(List[Candle], json.loads(response.text)["result"])
|
||||
|
||||
def get_orders(self,) -> List[Order]:
|
||||
response = httpx.get(f"{self.BASE_URL}/orders", timeout=10.0)
|
||||
response = httpx.get(f"{self.BASE_URL}/orders", timeout=self.timeout)
|
||||
return parse_obj_as(List[Order], json.loads(response.text)["result"])
|
||||
|
||||
def get_orders_by_market_name(self, market_name: str) -> List[Order]:
|
||||
response = httpx.get(
|
||||
f"{self.BASE_URL}/orders?market={market_name}", timeout=10.0
|
||||
f"{self.BASE_URL}/orders?market={market_name}", timeout=self.timeout
|
||||
)
|
||||
return parse_obj_as(List[Order], json.loads(response.text)["result"])
|
||||
|
||||
def place_order(self, order: PlaceOrder) -> None:
|
||||
response = httpx.post(
|
||||
f"{self.BASE_URL}/orders", json=order.dict(by_alias=True), timeout=10.0
|
||||
f"{self.BASE_URL}/orders",
|
||||
json=order.dict(by_alias=True),
|
||||
timeout=self.timeout,
|
||||
)
|
||||
# if response.status_code == httpx.codes.BAD_REQUEST:
|
||||
# return parse_obj_as(
|
||||
# List[BadRequestError], json.loads(response.text)["errors"]
|
||||
# )
|
||||
|
||||
def cancel_order_by_client_id(self, client_id):
|
||||
response = httpx.delete(
|
||||
f"{self.BASE_URL}/orders/by_client_id/{client_id}", timeout=10.0
|
||||
f"{self.BASE_URL}/orders/by_client_id/{client_id}", timeout=self.timeout
|
||||
)
|
||||
|
||||
def cancel_order_by_order_id(self, order_id):
|
||||
response = httpx.delete(f"{self.BASE_URL}/orders/{order_id}", timeout=10.0)
|
||||
response = httpx.delete(
|
||||
f"{self.BASE_URL}/orders/{order_id}", timeout=self.timeout
|
||||
)
|
||||
|
||||
def cancel_all_orders(self):
|
||||
response = httpx.delete(f"{self.BASE_URL}/orders", timeout=10.0)
|
||||
response = httpx.delete(f"{self.BASE_URL}/orders", timeout=self.timeout)
|
||||
|
||||
@staticmethod
|
||||
def to_nearest(num, tickDec):
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import pytest
|
||||
|
||||
from mango_service_v3_py.api import MangoServiceV3Client
|
||||
from mango_service_v3_py.dtos import PlaceOrder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mango_service_v3_client():
|
||||
return MangoServiceV3Client("http://localhost:3001/api", timeout=60.0)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def run_around_tests(mango_service_v3_client):
|
||||
# cleanup
|
||||
mango_service_v3_client.cancel_all_orders()
|
||||
orders = mango_service_v3_client.get_orders()
|
||||
assert len(orders) == 0
|
||||
yield
|
||||
# teardown
|
||||
assert True
|
||||
|
||||
|
||||
def place_order(mango_service_v3_client, market):
|
||||
mango_service_v3_client.place_order(
|
||||
PlaceOrder(
|
||||
market=market,
|
||||
side="buy",
|
||||
price=20000,
|
||||
type="limit",
|
||||
size=0.0001,
|
||||
reduce_only=False,
|
||||
ioc=False,
|
||||
post_only=False,
|
||||
client_id=123,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
SPOT_AND_PERP_MARKETS = [("BTC-PERP"), ("BTC-SPOT")]
|
||||
PERP_MARKET = [("BTC-PERP")]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("market", PERP_MARKET)
|
||||
def test_get_positions(mango_service_v3_client, market):
|
||||
positions = mango_service_v3_client.get_open_positions()
|
||||
assert len(positions) > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
|
||||
def test_get_balances(mango_service_v3_client, market):
|
||||
balances = mango_service_v3_client.get_balances()
|
||||
assert len(balances) > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
|
||||
def test_place_order(mango_service_v3_client, market):
|
||||
mango_service_v3_client.cancel_all_orders()
|
||||
place_order(mango_service_v3_client, market)
|
||||
orders = mango_service_v3_client.get_orders()
|
||||
assert len(orders) == 1
|
||||
|
||||
order = orders[0]
|
||||
print(order)
|
||||
assert order.market == market
|
||||
assert order.price == 20000
|
||||
assert order.size == 0.0001
|
||||
|
||||
|
||||
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
|
||||
def test_cancel_order_by_order_id(mango_service_v3_client, market):
|
||||
place_order(mango_service_v3_client, market)
|
||||
|
||||
orders = mango_service_v3_client.get_orders()
|
||||
order = orders[0]
|
||||
mango_service_v3_client.cancel_order_by_order_id(order.id)
|
||||
|
||||
orders = mango_service_v3_client.get_orders()
|
||||
assert len(orders) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
|
||||
def test_cancel_order_by_client_id(mango_service_v3_client, market):
|
||||
place_order(mango_service_v3_client, market)
|
||||
|
||||
orders = mango_service_v3_client.get_orders()
|
||||
order = orders[0]
|
||||
mango_service_v3_client.cancel_order_by_client_id(order.client_id)
|
||||
|
||||
orders = mango_service_v3_client.get_orders()
|
||||
assert len(orders) == 0
|
|
@ -0,0 +1,69 @@
|
|||
import pytest
|
||||
|
||||
from mango_service_v3_py.api import MangoServiceV3Client
|
||||
from mango_service_v3_py.dtos import PlaceOrder
|
||||
|
||||
# Note: some endpoints only return useful data for mainnet, this is because the REST API service rely on
|
||||
# other off chain REST services which only serve data for mainnet
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mango_service_v3_client():
|
||||
return MangoServiceV3Client("http://localhost:3000/api", timeout=60.0)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def run_around_tests(mango_service_v3_client):
|
||||
# cleanup
|
||||
mango_service_v3_client.cancel_all_orders()
|
||||
orders = mango_service_v3_client.get_orders()
|
||||
assert len(orders) == 0
|
||||
yield
|
||||
# teardown
|
||||
assert True
|
||||
|
||||
|
||||
def place_order(mango_service_v3_client, market):
|
||||
mango_service_v3_client.place_order(
|
||||
PlaceOrder(
|
||||
market=market,
|
||||
side="buy",
|
||||
price=20000,
|
||||
type="limit",
|
||||
size=0.0001,
|
||||
reduce_only=False,
|
||||
ioc=False,
|
||||
post_only=False,
|
||||
client_id=123,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
SPOT_AND_PERP_MARKETS = [("BTC-PERP"), ("BTC-SPOT")]
|
||||
PERP_MARKET = [("BTC-PERP")]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
|
||||
def test_get_markets(mango_service_v3_client, market):
|
||||
markets = mango_service_v3_client.get_markets()
|
||||
assert len(markets) > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
|
||||
def test_get_candles(mango_service_v3_client, market):
|
||||
candles = mango_service_v3_client.get_candles(market, 60, 1625922900, 1631214960)
|
||||
print(candles)
|
||||
assert len(candles) > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
|
||||
def test_get_orderbook(mango_service_v3_client, market):
|
||||
ob = mango_service_v3_client.get_orderboook(market)
|
||||
assert len(ob.asks) > 0
|
||||
assert len(ob.bids) > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
|
||||
def test_get_trades(mango_service_v3_client, market):
|
||||
trades = mango_service_v3_client.get_trades(market)
|
||||
assert len(trades) > 0
|
Loading…
Reference in New Issue