First commit
This commit is contained in:
commit
37fcaa8bcb
|
@ -0,0 +1 @@
|
|||
node_modules
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run pretty-quick --staged
|
||||
npm run lint
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"search.exclude": {
|
||||
".git": true,
|
||||
"node_modules": true
|
||||
},
|
||||
"files.exclude": {
|
||||
".git": true,
|
||||
"node_modules": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "mango-v3-service",
|
||||
"version": "0.0.1",
|
||||
"description": "REST API for trading against serum & mango markets",
|
||||
"main": "./src/server.ts",
|
||||
"scripts": {
|
||||
"dev": "ts-node ./src/server.ts",
|
||||
"lint": "tslint -p tsconfig.json -c tslint.json",
|
||||
"prepare": "husky install",
|
||||
"pretty-quick": "pretty-quick",
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "microwavedcola1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blockworks-foundation/mango-client": "^3.0.13",
|
||||
"body-parser": "^1.19.0",
|
||||
"envalid": "^7.2.1",
|
||||
"express": "^4.17.1",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^2.0.4",
|
||||
"@types/express": "^4.17.13",
|
||||
"husky": "^7.0.2",
|
||||
"prettier": "^2.3.2",
|
||||
"prettier-plugin-organize-imports": "^2.3.3",
|
||||
"pretty-quick": "^3.1.1",
|
||||
"ts-node": "^10.2.1",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typescript": "^4.4.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
import {
|
||||
getMarketByPublicKey,
|
||||
PerpMarket,
|
||||
ZERO_BN,
|
||||
} from "@blockworks-foundation/mango-client";
|
||||
import BN from "bn.js";
|
||||
import Controller from "controller.interface";
|
||||
import { NextFunction, Request, Response, Router } from "express";
|
||||
import MangoSimpleClient from "mango.simple.client";
|
||||
|
||||
class AccountController implements Controller {
|
||||
public path = "/positions";
|
||||
public router = Router();
|
||||
|
||||
constructor(public mangoMarkets: MangoSimpleClient) {
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes() {
|
||||
this.router.get(this.path, this.getPositions);
|
||||
}
|
||||
|
||||
private getPositions = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const groupConfig = this.mangoMarkets.mangoGroupConfig;
|
||||
const mangoGroup = this.mangoMarkets.mangoGroup;
|
||||
const mangoAccount = await this.mangoMarkets.mangoAccount.reload(
|
||||
this.mangoMarkets.connection,
|
||||
this.mangoMarkets.mangoGroup.dexProgramId
|
||||
);
|
||||
const mangoCache = await this.mangoMarkets.mangoGroup.loadCache(
|
||||
this.mangoMarkets.connection
|
||||
);
|
||||
|
||||
const allMarkets = await this.mangoMarkets.getAllMarkets();
|
||||
const mangoAccountFills = await this.mangoMarkets.getAllFills(true);
|
||||
|
||||
const perpAccounts = mangoAccount
|
||||
? groupConfig.perpMarkets.map((m) => {
|
||||
return {
|
||||
perpAccount: mangoAccount.perpAccounts[m.marketIndex],
|
||||
marketIndex: m.marketIndex,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
const filteredPerpAccounts = perpAccounts.filter(
|
||||
({ perpAccount }) => !perpAccount.basePosition.eq(new BN(0))
|
||||
);
|
||||
|
||||
const postionDtos = filteredPerpAccounts.map(
|
||||
({ perpAccount, marketIndex }, index) => {
|
||||
const perpMarketInfo =
|
||||
this.mangoMarkets.mangoGroup.perpMarkets[marketIndex];
|
||||
const marketConfig = getMarketByPublicKey(
|
||||
groupConfig,
|
||||
perpMarketInfo.perpMarket
|
||||
);
|
||||
const perpMarket = allMarkets[
|
||||
perpMarketInfo.perpMarket.toBase58()
|
||||
] as PerpMarket;
|
||||
const perpTradeHistory = mangoAccountFills.filter(
|
||||
(t) => t.marketName === marketConfig.name
|
||||
);
|
||||
|
||||
let breakEvenPrice;
|
||||
try {
|
||||
breakEvenPrice = perpAccount.getBreakEvenPrice(
|
||||
mangoAccount,
|
||||
perpMarket,
|
||||
perpTradeHistory
|
||||
);
|
||||
} catch (e) {
|
||||
breakEvenPrice = null;
|
||||
}
|
||||
|
||||
const pnl =
|
||||
breakEvenPrice !== null
|
||||
? perpMarket.baseLotsToNumber(perpAccount.basePosition) *
|
||||
(this.mangoMarkets.mangoGroup
|
||||
.getPrice(marketIndex, mangoCache)
|
||||
.toNumber() -
|
||||
parseFloat(breakEvenPrice))
|
||||
: null;
|
||||
|
||||
let entryPrice;
|
||||
try {
|
||||
entryPrice = perpAccount.getAverageOpenPrice(
|
||||
mangoAccount,
|
||||
perpMarket,
|
||||
perpTradeHistory
|
||||
);
|
||||
} catch {
|
||||
entryPrice = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
cost: Math.abs(
|
||||
perpMarket.baseLotsToNumber(perpAccount.basePosition) *
|
||||
mangoGroup.getPrice(marketIndex, mangoCache).toNumber()
|
||||
),
|
||||
cumulativeBuySize: undefined,
|
||||
cumulativeSellSize: undefined,
|
||||
entryPrice: entryPrice,
|
||||
estimatedLiquidationPrice: undefined,
|
||||
future: marketConfig.baseSymbol,
|
||||
initialMarginRequirement: undefined,
|
||||
longOrderSize: undefined,
|
||||
maintenanceMarginRequirement: undefined,
|
||||
netSize: undefined,
|
||||
openSize: undefined,
|
||||
realizedPnl: undefined,
|
||||
recentAverageOpenPrice: undefined,
|
||||
recentBreakEvenPrice: breakEvenPrice,
|
||||
recentPnl: pnl,
|
||||
shortOrderSize: undefined,
|
||||
side: perpAccount.basePosition.gt(ZERO_BN) ? "long" : "short",
|
||||
size: Math.abs(perpMarket.baseLotsToNumber(perpAccount.basePosition)),
|
||||
unrealizedPnl: pnl,
|
||||
collateralUsed: undefined,
|
||||
} as PositionDto;
|
||||
}
|
||||
);
|
||||
|
||||
response.send({ success: true, result: postionDtos } as PositionsDto);
|
||||
};
|
||||
}
|
||||
|
||||
export default AccountController;
|
||||
|
||||
/// Dtos
|
||||
|
||||
// e.g.
|
||||
// {
|
||||
// "success": true,
|
||||
// "result": [
|
||||
// {
|
||||
// "cost": -31.7906,
|
||||
// "cumulativeBuySize": 1.2,
|
||||
// "cumulativeSellSize": 0.0,
|
||||
// "entryPrice": 138.22,
|
||||
// "estimatedLiquidationPrice": 152.1,
|
||||
// "future": "ETH-PERP",
|
||||
// "initialMarginRequirement": 0.1,
|
||||
// "longOrderSize": 1744.55,
|
||||
// "maintenanceMarginRequirement": 0.04,
|
||||
// "netSize": -0.23,
|
||||
// "openSize": 1744.32,
|
||||
// "realizedPnl": 3.39441714,
|
||||
// "recentAverageOpenPrice": 135.31,
|
||||
// "recentBreakEvenPrice": 135.31,
|
||||
// "recentPnl": 3.1134,
|
||||
// "shortOrderSize": 1732.09,
|
||||
// "side": "sell",
|
||||
// "size": 0.23,
|
||||
// "unrealizedPnl": 0,
|
||||
// "collateralUsed": 3.17906
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
interface PositionsDto {
|
||||
success: boolean;
|
||||
result: PositionDto[];
|
||||
}
|
||||
|
||||
interface PositionDto {
|
||||
cost: number;
|
||||
cumulativeBuySize: number;
|
||||
cumulativeSellSize: number;
|
||||
entryPrice: number;
|
||||
estimatedLiquidationPrice: number;
|
||||
future: "ETH-PERP";
|
||||
initialMarginRequirement: number;
|
||||
longOrderSize: number;
|
||||
maintenanceMarginRequirement: number;
|
||||
netSize: number;
|
||||
openSize: number;
|
||||
realizedPnl: number;
|
||||
recentAverageOpenPrice: number;
|
||||
recentBreakEvenPrice: number;
|
||||
recentPnl: number;
|
||||
shortOrderSize: number;
|
||||
side: string;
|
||||
size: number;
|
||||
unrealizedPnl: number;
|
||||
collateralUsed: number;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import AccountController from "./account.controller";
|
||||
import MangoSimpleClient from "./mango.simple.client";
|
||||
import MarketsController from "./markets.controller";
|
||||
import OrdersController from "./orders.controller";
|
||||
import WalletController from "./wallet.controller";
|
||||
import Controller from "controller.interface";
|
||||
import express from "express";
|
||||
import log from "loglevel";
|
||||
|
||||
const bodyParser = require("body-parser");
|
||||
|
||||
class App {
|
||||
public app: express.Application;
|
||||
public mangoMarkets: MangoSimpleClient;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
MangoSimpleClient.create().then((mangoSimpleClient) => {
|
||||
this.mangoMarkets = mangoSimpleClient;
|
||||
|
||||
this.app.use(bodyParser.json({ limit: "50mb" }));
|
||||
|
||||
this.initializeControllers([
|
||||
new WalletController(this.mangoMarkets),
|
||||
new OrdersController(this.mangoMarkets),
|
||||
new MarketsController(this.mangoMarkets),
|
||||
new AccountController(this.mangoMarkets),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
private initializeControllers(controllers: Controller[]) {
|
||||
controllers.forEach((controller) => {
|
||||
this.app.use("/", controller.router);
|
||||
});
|
||||
}
|
||||
|
||||
public listen() {
|
||||
this.app.listen(process.env.PORT, () => {
|
||||
log.info(`App listening on the port ${process.env.PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
public getServer() {
|
||||
return this.app;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,8 @@
|
|||
import { Router } from "express";
|
||||
|
||||
interface Controller {
|
||||
path: string;
|
||||
router: Router;
|
||||
}
|
||||
|
||||
export default Controller;
|
|
@ -0,0 +1,442 @@
|
|||
import { zipDict } from "./utils";
|
||||
import {
|
||||
Config,
|
||||
getAllMarkets,
|
||||
getMarketByPublicKey,
|
||||
getMultipleAccounts,
|
||||
GroupConfig,
|
||||
MangoClient,
|
||||
MangoGroup,
|
||||
PerpMarketLayout,
|
||||
MarketConfig,
|
||||
MangoAccount,
|
||||
PerpMarket,
|
||||
BookSide,
|
||||
BookSideLayout,
|
||||
PerpOrder,
|
||||
getMarketByBaseSymbolAndKind,
|
||||
} from "@blockworks-foundation/mango-client";
|
||||
import { Market, Orderbook } from "@project-serum/serum";
|
||||
import { Order } from "@project-serum/serum/lib/market";
|
||||
import { Account, Commitment, Connection } from "@solana/web3.js";
|
||||
import { AccountInfo, PublicKey } from "@solana/web3.js";
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import { OrderInfo } from "types";
|
||||
|
||||
class MangoSimpleClient {
|
||||
constructor(
|
||||
public mangoGroupConfig: GroupConfig,
|
||||
public connection: Connection,
|
||||
public client: MangoClient,
|
||||
// todo: load/reload/cache markets often
|
||||
public mangoGroup: MangoGroup,
|
||||
public owner: Account,
|
||||
// todo: load/reload/cache mangoAccount and various load* methods often
|
||||
public mangoAccount: MangoAccount
|
||||
) {}
|
||||
|
||||
static async create() {
|
||||
const groupName = process.env.GROUP || "devnet.1";
|
||||
const mangoGroupConfig: GroupConfig = Config.ids().groups.filter(
|
||||
(group) => group.name == groupName
|
||||
)[0];
|
||||
|
||||
const connection = new Connection(
|
||||
process.env.CLUSTER_URL || "https://api.devnet.solana.com",
|
||||
"processed" as Commitment
|
||||
);
|
||||
|
||||
const mangoClient = new MangoClient(
|
||||
connection,
|
||||
mangoGroupConfig.mangoProgramId
|
||||
);
|
||||
|
||||
const mangoGroup = await mangoClient.getMangoGroup(
|
||||
mangoGroupConfig.publicKey
|
||||
);
|
||||
|
||||
const user = new Account(
|
||||
JSON.parse(
|
||||
process.env.KEYPAIR ||
|
||||
fs.readFileSync(
|
||||
os.homedir() + "/.config/solana/mainnet.json",
|
||||
"utf-8"
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const mangoAccounts = await mangoClient.getMangoAccountsForOwner(
|
||||
mangoGroup,
|
||||
user.publicKey
|
||||
);
|
||||
if (!mangoAccounts.length) {
|
||||
throw new Error(`No mango account found ${user.publicKey.toBase58()}`);
|
||||
}
|
||||
|
||||
const sortedMangoAccounts = mangoAccounts
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
a.publicKey.toBase58() > b.publicKey.toBase58() ? 1 : -1
|
||||
);
|
||||
|
||||
return new MangoSimpleClient(
|
||||
// todo: these things might get stale as time goes
|
||||
mangoGroupConfig,
|
||||
connection,
|
||||
mangoClient,
|
||||
mangoGroup,
|
||||
user,
|
||||
sortedMangoAccounts[0]
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
public async getAllMarkets(
|
||||
onlyMarket?: string
|
||||
): Promise<Partial<Record<string, Market | PerpMarket>>> {
|
||||
let allMarketConfigs = getAllMarkets(this.mangoGroupConfig);
|
||||
let allMarketPks = allMarketConfigs.map((m) => m.publicKey);
|
||||
|
||||
if (onlyMarket !== undefined) {
|
||||
allMarketConfigs = allMarketConfigs.filter(
|
||||
(marketConfig) => marketConfig.name === onlyMarket
|
||||
);
|
||||
allMarketPks = allMarketConfigs.map((m) => m.publicKey);
|
||||
}
|
||||
|
||||
const allMarketAccountInfos = await getMultipleAccounts(
|
||||
this.connection,
|
||||
allMarketPks
|
||||
);
|
||||
|
||||
const allMarketAccounts = allMarketConfigs.map((config, i) => {
|
||||
if (config.kind == "spot") {
|
||||
const decoded = Market.getLayout(
|
||||
this.mangoGroupConfig.mangoProgramId
|
||||
).decode(allMarketAccountInfos[i].accountInfo.data);
|
||||
return new Market(
|
||||
decoded,
|
||||
config.baseDecimals,
|
||||
config.quoteDecimals,
|
||||
undefined,
|
||||
this.mangoGroupConfig.serumProgramId
|
||||
);
|
||||
}
|
||||
if (config.kind == "perp") {
|
||||
const decoded = PerpMarketLayout.decode(
|
||||
allMarketAccountInfos[i].accountInfo.data
|
||||
);
|
||||
return new PerpMarket(
|
||||
config.publicKey,
|
||||
config.baseDecimals,
|
||||
config.quoteDecimals,
|
||||
decoded
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return zipDict(
|
||||
allMarketPks.map((pk) => pk.toBase58()),
|
||||
allMarketAccounts
|
||||
);
|
||||
}
|
||||
|
||||
public async getAllBidsAndAsks(
|
||||
filterForMangoAccount: boolean = false,
|
||||
onlyMarket?: string
|
||||
): Promise<OrderInfo[][]> {
|
||||
this.mangoAccount.loadOpenOrders(
|
||||
this.connection,
|
||||
new PublicKey(this.mangoGroupConfig.serumProgramId)
|
||||
);
|
||||
|
||||
let allMarketConfigs = getAllMarkets(this.mangoGroupConfig);
|
||||
let allMarketPks = allMarketConfigs.map((m) => m.publicKey);
|
||||
|
||||
if (onlyMarket !== undefined) {
|
||||
allMarketConfigs = allMarketConfigs.filter(
|
||||
(marketConfig) => marketConfig.name === onlyMarket
|
||||
);
|
||||
allMarketPks = allMarketConfigs.map((m) => m.publicKey);
|
||||
}
|
||||
|
||||
const allBidsAndAsksPks = allMarketConfigs
|
||||
.map((m) => [m.bidsKey, m.asksKey])
|
||||
.flat();
|
||||
const allBidsAndAsksAccountInfos = await getMultipleAccounts(
|
||||
this.connection,
|
||||
allBidsAndAsksPks
|
||||
);
|
||||
|
||||
const accountInfos: { [key: string]: AccountInfo<Buffer> } = {};
|
||||
allBidsAndAsksAccountInfos.forEach(
|
||||
({ publicKey, context, accountInfo }) => {
|
||||
accountInfos[publicKey.toBase58()] = accountInfo;
|
||||
}
|
||||
);
|
||||
|
||||
const markets = await this.getAllMarkets(onlyMarket);
|
||||
|
||||
return Object.entries(markets).map(([address, market]) => {
|
||||
const marketConfig = getMarketByPublicKey(this.mangoGroupConfig, address);
|
||||
if (market instanceof Market) {
|
||||
return this.parseSpotOrders(
|
||||
market,
|
||||
marketConfig,
|
||||
accountInfos,
|
||||
filterForMangoAccount ? this.mangoAccount : undefined
|
||||
);
|
||||
} else if (market instanceof PerpMarket) {
|
||||
return this.parsePerpOpenOrders(
|
||||
market,
|
||||
marketConfig,
|
||||
accountInfos,
|
||||
filterForMangoAccount ? this.mangoAccount : undefined
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getAllFills(
|
||||
filterForMangoAccount: boolean = false
|
||||
): Promise<any[]> {
|
||||
let allMarketConfigs = getAllMarkets(this.mangoGroupConfig);
|
||||
const allMarkets = await this.getAllMarkets();
|
||||
|
||||
let mangoAccountFills: any[] = [];
|
||||
|
||||
allMarketConfigs.map((config, i) => {
|
||||
if (config.kind == "spot") {
|
||||
const openOrdersAccount =
|
||||
this.mangoAccount.spotOpenOrdersAccounts[config.marketIndex];
|
||||
const mangoAccountFills_ = allMarkets[config.publicKey.toBase58()]
|
||||
// todo: what if we want to fetch the 10001 position?
|
||||
.loadFills(this.connection, 10000)
|
||||
.then((fills) => {
|
||||
if (filterForMangoAccount) {
|
||||
fills = fills.filter((fill) => {
|
||||
return openOrdersAccount?.publicKey
|
||||
? fill.openOrders.equals(openOrdersAccount?.publicKey)
|
||||
: false;
|
||||
});
|
||||
}
|
||||
return fills.map((fill) => ({ ...fill, marketName: config.name }));
|
||||
});
|
||||
mangoAccountFills = mangoAccountFills.concat(mangoAccountFills_);
|
||||
}
|
||||
if (config.kind == "perp") {
|
||||
const mangoAccountFills_ = allMarkets[config.publicKey.toBase58()]
|
||||
.loadFills(this.connection)
|
||||
.then((fills) => {
|
||||
if (filterForMangoAccount) {
|
||||
fills = fills.filter(
|
||||
(fill) =>
|
||||
fill.taker.equals(this.mangoAccount.publicKey) ||
|
||||
fill.maker.equals(this.mangoAccount.publicKey)
|
||||
);
|
||||
}
|
||||
return fills.map((fill) => ({ ...fill, marketName: config.name }));
|
||||
});
|
||||
mangoAccountFills = mangoAccountFills.concat(mangoAccountFills_);
|
||||
}
|
||||
});
|
||||
|
||||
return mangoAccountFills;
|
||||
}
|
||||
|
||||
public async placeOrder(
|
||||
market: string,
|
||||
type: "market" | "limit",
|
||||
side: "buy" | "sell",
|
||||
quantity: number,
|
||||
price?: number,
|
||||
orderType: "ioc" | "postOnly" | "limit" = "limit"
|
||||
): Promise<void> {
|
||||
if (type === "market") {
|
||||
throw new Error("Not implemented!");
|
||||
}
|
||||
|
||||
if (market.includes("PERP")) {
|
||||
const perpMarketConfig = getMarketByBaseSymbolAndKind(
|
||||
this.mangoGroupConfig,
|
||||
market.split("/")[0],
|
||||
"perp"
|
||||
);
|
||||
const perpMarket = await this.mangoGroup.loadPerpMarket(
|
||||
this.connection,
|
||||
perpMarketConfig.marketIndex,
|
||||
perpMarketConfig.baseDecimals,
|
||||
perpMarketConfig.quoteDecimals
|
||||
);
|
||||
await this.client.placePerpOrder(
|
||||
this.mangoGroup,
|
||||
this.mangoAccount,
|
||||
this.mangoGroup.mangoCache,
|
||||
perpMarket,
|
||||
this.owner,
|
||||
side,
|
||||
price,
|
||||
quantity,
|
||||
orderType
|
||||
);
|
||||
} else {
|
||||
const spotMarketConfig = getMarketByBaseSymbolAndKind(
|
||||
this.mangoGroupConfig,
|
||||
market.split("/")[0],
|
||||
"spot"
|
||||
);
|
||||
const spotMarket = await Market.load(
|
||||
this.connection,
|
||||
spotMarketConfig.publicKey,
|
||||
undefined,
|
||||
this.mangoGroupConfig.serumProgramId
|
||||
);
|
||||
await this.client.placeSpotOrder(
|
||||
this.mangoGroup,
|
||||
this.mangoAccount,
|
||||
this.mangoGroup.mangoCache,
|
||||
spotMarket,
|
||||
this.owner,
|
||||
side,
|
||||
price,
|
||||
quantity,
|
||||
orderType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async cancelAllOrders(): Promise<void> {
|
||||
const orders = await (await this.getAllBidsAndAsks(true)).flat();
|
||||
// todo: this would fetch a market for every call, cache markets
|
||||
const orderInfo = Promise.all(
|
||||
orders.map((orderInfo) => this.cancelOrder(orderInfo))
|
||||
);
|
||||
}
|
||||
|
||||
public async cancelOrderByOrderId(orderId: string): Promise<void> {
|
||||
const orders = await (await this.getAllBidsAndAsks(true)).flat();
|
||||
const orderInfo = orders.filter(
|
||||
(orderInfo) => orderInfo.order.orderId.toNumber().toString() === orderId
|
||||
)[0];
|
||||
|
||||
await this.cancelOrder(orderInfo);
|
||||
}
|
||||
|
||||
public async cancelOrderByClientId(clientId: string): Promise<void> {
|
||||
const orders = await (await this.getAllBidsAndAsks(true)).flat();
|
||||
const orderInfo = orders.filter(
|
||||
(orderInfo) => orderInfo.order.clientId.toNumber().toString() === clientId
|
||||
)[0];
|
||||
|
||||
await this.cancelOrder(orderInfo);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
parseSpotOrders(
|
||||
market: Market,
|
||||
config: MarketConfig,
|
||||
accountInfos: { [key: string]: AccountInfo<Buffer> },
|
||||
mangoAccount?: MangoAccount
|
||||
): OrderInfo[] {
|
||||
const openOrders = mangoAccount.spotOpenOrdersAccounts[config.marketIndex];
|
||||
if (!openOrders) return [];
|
||||
|
||||
const bidData = accountInfos[market["_decoded"].bids.toBase58()]?.data;
|
||||
const askData = accountInfos[market["_decoded"].asks.toBase58()]?.data;
|
||||
|
||||
const bidOrderBook =
|
||||
market && bidData ? Orderbook.decode(market, bidData) : ([] as Order[]);
|
||||
const askOrderBook =
|
||||
market && askData ? Orderbook.decode(market, askData) : ([] as Order[]);
|
||||
|
||||
let openOrdersForMarket = [...bidOrderBook, ...askOrderBook];
|
||||
if (mangoAccount !== undefined) {
|
||||
openOrdersForMarket = openOrdersForMarket.filter((o) =>
|
||||
o.openOrdersAddress.equals(openOrders.address)
|
||||
);
|
||||
}
|
||||
|
||||
return openOrdersForMarket.map<OrderInfo>((order) => ({
|
||||
order,
|
||||
market: { account: market, config: config },
|
||||
}));
|
||||
}
|
||||
|
||||
parsePerpOpenOrders(
|
||||
market: PerpMarket,
|
||||
config: MarketConfig,
|
||||
accountInfos: { [key: string]: AccountInfo<Buffer> },
|
||||
mangoAccount?: MangoAccount
|
||||
): OrderInfo[] {
|
||||
const bidData = accountInfos[market.bids.toBase58()]?.data;
|
||||
const askData = accountInfos[market.asks.toBase58()]?.data;
|
||||
|
||||
const bidOrderBook =
|
||||
market && bidData
|
||||
? new BookSide(market.bids, market, BookSideLayout.decode(bidData))
|
||||
: ([] as PerpOrder[]);
|
||||
const askOrderBook =
|
||||
market && askData
|
||||
? new BookSide(market.asks, market, BookSideLayout.decode(askData))
|
||||
: ([] as PerpOrder[]);
|
||||
|
||||
let openOrdersForMarket = [...bidOrderBook, ...askOrderBook];
|
||||
if (mangoAccount !== undefined) {
|
||||
openOrdersForMarket = openOrdersForMarket.filter((o) =>
|
||||
o.owner.equals(mangoAccount.publicKey)
|
||||
);
|
||||
}
|
||||
|
||||
return openOrdersForMarket.map<OrderInfo>((order) => ({
|
||||
order,
|
||||
market: { account: market, config: config },
|
||||
}));
|
||||
}
|
||||
|
||||
async cancelOrder(orderInfo: OrderInfo) {
|
||||
if (orderInfo.market.config.kind === "perp") {
|
||||
const perpMarketConfig = getMarketByBaseSymbolAndKind(
|
||||
this.mangoGroupConfig,
|
||||
orderInfo.market.config.baseSymbol,
|
||||
"perp"
|
||||
);
|
||||
const perpMarket = await this.mangoGroup.loadPerpMarket(
|
||||
this.connection,
|
||||
perpMarketConfig.marketIndex,
|
||||
perpMarketConfig.baseDecimals,
|
||||
perpMarketConfig.quoteDecimals
|
||||
);
|
||||
await this.client.cancelPerpOrder(
|
||||
this.mangoGroup,
|
||||
this.mangoAccount,
|
||||
this.owner,
|
||||
perpMarket,
|
||||
orderInfo.order as PerpOrder
|
||||
);
|
||||
} else {
|
||||
const spotMarketConfig = getMarketByBaseSymbolAndKind(
|
||||
this.mangoGroupConfig,
|
||||
orderInfo.market.config.baseSymbol,
|
||||
"spot"
|
||||
);
|
||||
const spotMarket = await Market.load(
|
||||
this.connection,
|
||||
spotMarketConfig.publicKey,
|
||||
undefined,
|
||||
this.mangoGroupConfig.serumProgramId
|
||||
);
|
||||
await this.client.cancelSpotOrder(
|
||||
this.mangoGroup,
|
||||
this.mangoAccount,
|
||||
this.owner,
|
||||
spotMarket,
|
||||
orderInfo.order as Order
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MangoSimpleClient;
|
|
@ -0,0 +1,364 @@
|
|||
import Controller from "./controller.interface";
|
||||
import MangoSimpleClient from "./mango.simple.client";
|
||||
import {
|
||||
BookSide,
|
||||
BookSideLayout,
|
||||
getAllMarkets,
|
||||
MarketConfig,
|
||||
PerpMarket,
|
||||
PerpOrder,
|
||||
} from "@blockworks-foundation/mango-client";
|
||||
import { Market, Orderbook } from "@project-serum/serum";
|
||||
import { Order } from "@project-serum/serum/lib/market";
|
||||
import { AccountInfo } from "@solana/web3.js";
|
||||
import { NextFunction, Request, Response, Router } from "express";
|
||||
|
||||
class MarketsController implements Controller {
|
||||
public path = "/markets";
|
||||
public router = Router();
|
||||
|
||||
constructor(public mangoMarkets: MangoSimpleClient) {
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes() {
|
||||
// GET /markets
|
||||
this.router.get(this.path, this.getMarkets);
|
||||
|
||||
// todo GET /markets/{market_name}
|
||||
|
||||
// GET /markets/{market_name}/orderbook?depth={depth}
|
||||
this.router.get(`${this.path}/:market_name/orderbook`, this.getOrderBook);
|
||||
|
||||
// GET /markets/{market_name}/trades
|
||||
this.router.get(`${this.path}/:market_name/trades`, this.getTrades);
|
||||
|
||||
// GET /markets/{market_name}/candles?resolution={resolution}&start_time={start_time}&end_time={end_time}
|
||||
this.router.get(`${this.path}/:market_name/candles`, this.getCandles);
|
||||
}
|
||||
|
||||
private getMarkets = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
let allMarketConfigs = getAllMarkets(this.mangoMarkets.mangoGroupConfig);
|
||||
|
||||
const marketDtos = [];
|
||||
for (const marketConfig of allMarketConfigs) {
|
||||
marketDtos.push({
|
||||
name: marketConfig.name,
|
||||
baseCurrency: marketConfig.baseSymbol,
|
||||
quoteCurrency: "USDC",
|
||||
quoteVolume24h: await getVolumeForMarket(marketConfig),
|
||||
change1h: undefined,
|
||||
change24h: undefined,
|
||||
changeBod: undefined,
|
||||
highLeverageFeeExempt: undefined,
|
||||
minProvideSize: undefined,
|
||||
type: marketConfig.name.includes("PERP") ? "futures" : "spot",
|
||||
underlying: marketConfig.baseSymbol,
|
||||
enabled: undefined,
|
||||
ask: undefined,
|
||||
bid: undefined,
|
||||
last: undefined,
|
||||
postOnly: undefined,
|
||||
price: undefined,
|
||||
priceIncrement: undefined,
|
||||
sizeIncrement: undefined,
|
||||
restricted: undefined,
|
||||
volumeUsd24h: await getVolumeForMarket(marketConfig),
|
||||
} as MarketDto);
|
||||
}
|
||||
response.send({ success: true, result: marketDtos } as MarketsDto);
|
||||
};
|
||||
|
||||
private getOrderBook = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
let marketName = request.params.market_name;
|
||||
const depth = Number(request.query.depth) || 20;
|
||||
|
||||
const ordersInfo = await this.mangoMarkets.getAllBidsAndAsks(
|
||||
false,
|
||||
marketName
|
||||
);
|
||||
let bids = ordersInfo
|
||||
.flat()
|
||||
.filter((orderInfo) => orderInfo.order.side == "buy")
|
||||
.sort((b1, b2) => b2.order.price - b1.order.price);
|
||||
let 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);
|
||||
};
|
||||
|
||||
private getTrades = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const allMarketConfigs = getAllMarkets(this.mangoMarkets.mangoGroupConfig);
|
||||
const marketName = request.params.market_name;
|
||||
const marketPk = allMarketConfigs
|
||||
.filter((marketConfig) => marketConfig.name === marketName)[0]
|
||||
.publicKey.toBase58();
|
||||
|
||||
const tradesResponse = await fetch(
|
||||
`https://serum-history.herokuapp.com/trades/address/${marketPk}`
|
||||
);
|
||||
const parsedTradesResponse = tradesResponse.json() as any;
|
||||
const 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;
|
||||
});
|
||||
|
||||
response.send({ success: true, result: tradeDtos } as TradesDto);
|
||||
};
|
||||
|
||||
private getCandles = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const marketName = request.params.market_name;
|
||||
const resolution = request.query.resolution;
|
||||
const fromEpochS = request.query.start_time;
|
||||
const toEpochS = request.query.end_time;
|
||||
|
||||
const historyResponse = await fetch(
|
||||
`https://serum-history.herokuapp.com/tv/history` +
|
||||
`?symbol=${marketName}&resolution=${resolution}` +
|
||||
`&from=${fromEpochS}&to=${toEpochS}`
|
||||
);
|
||||
const { time, open, high, low, close, volume } =
|
||||
(await historyResponse.json()) as any;
|
||||
|
||||
const ohlcvDtos: OhlcvDto[] = [];
|
||||
for (let i = 0; i < time.length; i++) {
|
||||
ohlcvDtos.push({
|
||||
time: time[i],
|
||||
open: open[i],
|
||||
high: high[i],
|
||||
low: low[i],
|
||||
close: close[i],
|
||||
volume: volume[i],
|
||||
} as OhlcvDto);
|
||||
}
|
||||
|
||||
response.send({ success: true, result: ohlcvDtos } as OhlcvsDto);
|
||||
};
|
||||
}
|
||||
|
||||
export default MarketsController;
|
||||
|
||||
/// helper functions
|
||||
|
||||
export async function getVolumeForMarket(marketConfig: MarketConfig) {
|
||||
const perpVolume = await fetch(
|
||||
`https://event-history-api.herokuapp.com/stats/perps/${marketConfig.publicKey.toString()}`
|
||||
);
|
||||
const parsedPerpVolume = await perpVolume.json();
|
||||
return parsedPerpVolume?.data?.volume;
|
||||
}
|
||||
|
||||
export function parseSpotOrders(
|
||||
market: Market,
|
||||
config: MarketConfig,
|
||||
accountInfos: { [key: string]: AccountInfo<Buffer> }
|
||||
): { bids: Order[]; asks: Order[] } {
|
||||
const bidData = accountInfos[market["_decoded"].bids.toBase58()]?.data;
|
||||
const askData = accountInfos[market["_decoded"].asks.toBase58()]?.data;
|
||||
|
||||
const bidOrderBook =
|
||||
market && bidData ? Orderbook.decode(market, bidData) : ([] as Order[]);
|
||||
const askOrderBook =
|
||||
market && askData ? Orderbook.decode(market, askData) : ([] as Order[]);
|
||||
|
||||
return { bids: [...bidOrderBook], asks: [...askOrderBook] };
|
||||
}
|
||||
|
||||
export function parsePerpOpenOrders(
|
||||
market: PerpMarket,
|
||||
config: MarketConfig,
|
||||
accountInfos: { [key: string]: AccountInfo<Buffer> }
|
||||
): { bids: PerpOrder[]; asks: PerpOrder[] } {
|
||||
const bidData = accountInfos[market.bids.toBase58()]?.data;
|
||||
const askData = accountInfos[market.asks.toBase58()]?.data;
|
||||
|
||||
const bidOrderBook =
|
||||
market && bidData
|
||||
? new BookSide(market.bids, market, BookSideLayout.decode(bidData))
|
||||
: ([] as PerpOrder[]);
|
||||
const askOrderBook =
|
||||
market && askData
|
||||
? new BookSide(market.asks, market, BookSideLayout.decode(askData))
|
||||
: ([] as PerpOrder[]);
|
||||
|
||||
return { bids: [...bidOrderBook], asks: [...askOrderBook] };
|
||||
}
|
||||
|
||||
/// Dtos
|
||||
|
||||
// e.g.
|
||||
// {
|
||||
// "success": true,
|
||||
// "result": [
|
||||
// {
|
||||
// "name": "BTC-0628",
|
||||
// "baseCurrency": null,
|
||||
// "quoteCurrency": null,
|
||||
// "quoteVolume24h": 28914.76,
|
||||
// "change1h": 0.012,
|
||||
// "change24h": 0.0299,
|
||||
// "changeBod": 0.0156,
|
||||
// "highLeverageFeeExempt": false,
|
||||
// "minProvideSize": 0.001,
|
||||
// "type": "future",
|
||||
// "underlying": "BTC",
|
||||
// "enabled": true,
|
||||
// "ask": 3949.25,
|
||||
// "bid": 3949,
|
||||
// "last": 3949.00,
|
||||
// "postOnly": false,
|
||||
// "price": 10579.52,
|
||||
// "priceIncrement": 0.25,
|
||||
// "sizeIncrement": 0.0001,
|
||||
// "restricted": false,
|
||||
// "volumeUsd24h": 28914.76
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
interface MarketsDto {
|
||||
success: boolean;
|
||||
result: MarketDto[];
|
||||
}
|
||||
|
||||
interface MarketDto {
|
||||
name: string;
|
||||
baseCurrency: string;
|
||||
quoteCurrency: string;
|
||||
quoteVolume24h: number;
|
||||
change1h: number;
|
||||
change24h: number;
|
||||
changeBod: number;
|
||||
highLeverageFeeExempt: boolean;
|
||||
minProvideSize: number;
|
||||
type: string;
|
||||
underlying: string;
|
||||
enabled: boolean;
|
||||
ask: number;
|
||||
bid: number;
|
||||
last: number;
|
||||
postOnly: boolean;
|
||||
price: number;
|
||||
priceIncrement: number;
|
||||
sizeIncrement: number;
|
||||
restricted: boolean;
|
||||
volumeUsd24h: number;
|
||||
}
|
||||
|
||||
// e.g.
|
||||
// {
|
||||
// "success": true,
|
||||
// "result": {
|
||||
// "asks": [
|
||||
// [
|
||||
// 4114.25,
|
||||
// 6.263
|
||||
// ]
|
||||
// ],
|
||||
// "bids": [
|
||||
// [
|
||||
// 4112.25,
|
||||
// 49.29
|
||||
// ]
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
interface OrdersDto {
|
||||
success: boolean;
|
||||
result: {
|
||||
asks: number[][];
|
||||
bids: number[][];
|
||||
};
|
||||
}
|
||||
|
||||
// e.g.
|
||||
// {
|
||||
// "success": true,
|
||||
// "result": [
|
||||
// {
|
||||
// "id": 3855995,
|
||||
// "liquidation": false,
|
||||
// "price": 3857.75,
|
||||
// "side": "buy",
|
||||
// "size": 0.111,
|
||||
// "time": "2019-03-20T18:16:23.397991+00:00"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
interface TradesDto {
|
||||
success: boolean;
|
||||
result: TradeDto[];
|
||||
}
|
||||
|
||||
interface TradeDto {
|
||||
id: string;
|
||||
liquidation: boolean;
|
||||
price: number;
|
||||
side: string;
|
||||
size: number;
|
||||
time: Date;
|
||||
}
|
||||
|
||||
// e.g.
|
||||
// {
|
||||
// "success": true,
|
||||
// "result": [
|
||||
// {
|
||||
// "close": 11055.25,
|
||||
// "high": 11089.0,
|
||||
// "low": 11043.5,
|
||||
// "open": 11059.25,
|
||||
// "startTime": "2019-06-24T17:15:00+00:00",
|
||||
// "volume": 464193.95725
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
interface OhlcvsDto {
|
||||
success: boolean;
|
||||
result: OhlcvDto[];
|
||||
}
|
||||
|
||||
interface OhlcvDto {
|
||||
time: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
import Controller from "./controller.interface";
|
||||
import MangoSimpleClient from "./mango.simple.client";
|
||||
import { OrderInfo } from "./types";
|
||||
import { PerpOrder } from "@blockworks-foundation/mango-client";
|
||||
import { Order } from "@project-serum/serum/lib/market";
|
||||
import { NextFunction, Request, Response, Router } from "express";
|
||||
|
||||
class OrdersController implements Controller {
|
||||
public path = "/orders";
|
||||
public router = Router();
|
||||
|
||||
constructor(public mangoSimpleClient: MangoSimpleClient) {
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes() {
|
||||
// GET /orders?market={market_name}
|
||||
this.router.get(this.path, this.getOpenOrders);
|
||||
|
||||
// POST /orders
|
||||
this.router.post(this.path, this.placeOrder);
|
||||
|
||||
// DELETE /orders
|
||||
this.router.delete(this.path, this.cancelAllOrders);
|
||||
// DELETE /orders/{order_id}
|
||||
this.router.delete(`${this.path}/:order_id`, this.cancelOrderByOrderId);
|
||||
// DELETE /orders/by_client_id/{client_order_id}
|
||||
this.router.delete(
|
||||
`${this.path}/by_client_id/:client_id`,
|
||||
this.cancelOrderByClientId
|
||||
);
|
||||
}
|
||||
|
||||
private getOpenOrders = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const openOrders = await this.mangoSimpleClient.getAllBidsAndAsks(
|
||||
true,
|
||||
request.query.market ? String(request.query.market) : undefined
|
||||
);
|
||||
|
||||
const orderDtos = openOrders.flat().map((orderInfo: OrderInfo) => {
|
||||
if ("bestInitial" in orderInfo.order) {
|
||||
const order_ = orderInfo.order as PerpOrder;
|
||||
return {
|
||||
createdAt: new Date(order_.timestamp.toNumber()),
|
||||
filledSize: undefined,
|
||||
future: orderInfo.market.config.name,
|
||||
id: order_.orderId.toString(),
|
||||
market: orderInfo.market.config.name,
|
||||
price: order_.price,
|
||||
avgFillPrice: undefined,
|
||||
remainingSize: undefined,
|
||||
side: order_.side,
|
||||
size: order_.size,
|
||||
status: "open",
|
||||
type: undefined,
|
||||
reduceOnly: undefined,
|
||||
ioc: undefined,
|
||||
postOnly: undefined,
|
||||
clientId:
|
||||
order_.clientId && order_.clientId.toString() !== "0"
|
||||
? order_.clientId.toString()
|
||||
: undefined,
|
||||
} as OrderDto;
|
||||
}
|
||||
|
||||
const order_ = orderInfo.order as Order;
|
||||
return {
|
||||
createdAt: undefined,
|
||||
filledSize: undefined,
|
||||
future: orderInfo.market.config.name,
|
||||
id: order_.orderId.toString(),
|
||||
market: orderInfo.market.config.name,
|
||||
price: order_.price,
|
||||
avgFillPrice: undefined,
|
||||
remainingSize: undefined,
|
||||
side: order_.side,
|
||||
size: order_.size,
|
||||
status: "open",
|
||||
type: undefined,
|
||||
reduceOnly: undefined,
|
||||
ioc: undefined,
|
||||
postOnly: undefined,
|
||||
clientId:
|
||||
order_.clientId && order_.clientId.toString() !== "0"
|
||||
? order_.clientId.toString()
|
||||
: undefined,
|
||||
} as OrderDto;
|
||||
});
|
||||
response.send({ success: true, result: orderDtos } as OrdersDto);
|
||||
};
|
||||
|
||||
private placeOrder = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const placeOrderDto = <PlaceOrderDto>request.body;
|
||||
await this.mangoSimpleClient.placeOrder(
|
||||
placeOrderDto.market,
|
||||
placeOrderDto.type,
|
||||
placeOrderDto.side,
|
||||
placeOrderDto.size,
|
||||
placeOrderDto.price,
|
||||
placeOrderDto.ioc ? "ioc" : placeOrderDto.postOnly ? "postOnly" : "limit"
|
||||
);
|
||||
};
|
||||
|
||||
private cancelAllOrders = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
await this.mangoSimpleClient.cancelAllOrders();
|
||||
};
|
||||
|
||||
private cancelOrderByOrderId = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
let order_id = request.params.order_id;
|
||||
await this.mangoSimpleClient.cancelOrderByOrderId(order_id);
|
||||
};
|
||||
|
||||
private cancelOrderByClientId = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
let client_id = request.params.client_id;
|
||||
await this.mangoSimpleClient.cancelOrderByClientId(client_id);
|
||||
};
|
||||
}
|
||||
|
||||
export default OrdersController;
|
||||
|
||||
/// Dtos
|
||||
|
||||
// e.g.
|
||||
// {
|
||||
// "success": true,
|
||||
// "result": [
|
||||
// {
|
||||
// "createdAt": "2019-03-05T09:56:55.728933+00:00",
|
||||
// "filledSize": 10,
|
||||
// "future": "XRP-PERP",
|
||||
// "id": 9596912,
|
||||
// "market": "XRP-PERP",
|
||||
// "price": 0.306525,
|
||||
// "avgFillPrice": 0.306526,
|
||||
// "remainingSize": 31421,
|
||||
// "side": "sell",
|
||||
// "size": 31431,
|
||||
// "status": "open",
|
||||
// "type": "limit",
|
||||
// "reduceOnly": false,
|
||||
// "ioc": false,
|
||||
// "postOnly": false,
|
||||
// "clientId": null
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
interface OrdersDto {
|
||||
success: boolean;
|
||||
result: OrderDto[];
|
||||
}
|
||||
|
||||
interface OrderDto {
|
||||
createdAt: Date;
|
||||
filledSize: number;
|
||||
future: string;
|
||||
id: string;
|
||||
market: string;
|
||||
price: number;
|
||||
avgFillPrice: number;
|
||||
remainingSize: number;
|
||||
side: string;
|
||||
size: number;
|
||||
status: string;
|
||||
type: string;
|
||||
reduceOnly: boolean;
|
||||
ioc: boolean;
|
||||
postOnly: boolean;
|
||||
clientId: null;
|
||||
}
|
||||
|
||||
// e.g.
|
||||
// {
|
||||
// "market": "XRP-PERP",
|
||||
// "side": "sell",
|
||||
// "price": 0.306525,
|
||||
// "type": "limit",
|
||||
// "size": 31431.0,
|
||||
// "reduceOnly": false,
|
||||
// "ioc": false,
|
||||
// "postOnly": false,
|
||||
// "clientId": null
|
||||
// }
|
||||
|
||||
interface PlaceOrderDto {
|
||||
market: string;
|
||||
side: "sell" | "buy";
|
||||
price: number;
|
||||
type: "limit" | "market";
|
||||
size: number;
|
||||
reduceOnly: boolean;
|
||||
ioc: boolean;
|
||||
postOnly: boolean;
|
||||
clientId: string;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import App from "./app";
|
||||
import { cleanEnv, port } from "envalid";
|
||||
|
||||
cleanEnv(process.env, {
|
||||
PORT: port({ default: 3000 }),
|
||||
});
|
||||
|
||||
const app = new App();
|
||||
|
||||
app.listen();
|
|
@ -0,0 +1,31 @@
|
|||
import {
|
||||
MarketConfig,
|
||||
PerpMarket,
|
||||
PerpOrder,
|
||||
} from "@blockworks-foundation/mango-client";
|
||||
import { I80F48 } from "@blockworks-foundation/mango-client/lib/src/fixednum";
|
||||
import { Market, OpenOrders } from "@project-serum/serum";
|
||||
import { Order } from "@project-serum/serum/lib/market";
|
||||
|
||||
interface BalancesBase {
|
||||
key: string;
|
||||
symbol: string;
|
||||
wallet?: number | null | undefined;
|
||||
orders?: number | null | undefined;
|
||||
openOrders?: OpenOrders | null | undefined;
|
||||
unsettled?: number | null | undefined;
|
||||
}
|
||||
|
||||
export interface Balances extends BalancesBase {
|
||||
deposits?: I80F48 | null | undefined;
|
||||
borrows?: I80F48 | null | undefined;
|
||||
net?: I80F48 | null | undefined;
|
||||
value?: I80F48 | null | undefined;
|
||||
depositRate?: I80F48 | null | undefined;
|
||||
borrowRate?: I80F48 | null | undefined;
|
||||
}
|
||||
|
||||
export type OrderInfo = {
|
||||
order: Order | PerpOrder;
|
||||
market: { account: Market | PerpMarket; config: MarketConfig };
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { I80F48 } from "@blockworks-foundation/mango-client/lib/src/fixednum";
|
||||
|
||||
export const i80f48ToPercent = (value: I80F48) =>
|
||||
value.mul(I80F48.fromNumber(100));
|
||||
|
||||
export function zipDict<K extends string | number | symbol, V>(
|
||||
keys: K[],
|
||||
values: V[]
|
||||
): Partial<Record<K, V>> {
|
||||
const result: Partial<Record<K, V>> = {};
|
||||
keys.forEach((key, index) => {
|
||||
result[key] = values[index];
|
||||
});
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
import { Balances } from "./types";
|
||||
import { i80f48ToPercent } from "./utils";
|
||||
import {
|
||||
I80F48,
|
||||
getTokenBySymbol,
|
||||
nativeI80F48ToUi,
|
||||
nativeToUi,
|
||||
QUOTE_INDEX,
|
||||
} from "@blockworks-foundation/mango-client";
|
||||
import Controller from "controller.interface";
|
||||
import { NextFunction, Request, Response, Router } from "express";
|
||||
import { sumBy } from "lodash";
|
||||
import MangoSimpleClient from "mango.simple.client";
|
||||
|
||||
class WalletController implements Controller {
|
||||
public path = "/wallet";
|
||||
public router = Router();
|
||||
|
||||
constructor(public mangoMarkets: MangoSimpleClient) {
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes() {
|
||||
this.router.get(`${this.path}/balances`, this.getBalances);
|
||||
}
|
||||
|
||||
private getBalances = async (
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
// local copies of mango objects
|
||||
const mangoGroupConfig = this.mangoMarkets.mangoGroupConfig;
|
||||
const mangoGroup = this.mangoMarkets.mangoGroup;
|
||||
|
||||
// (re)load things which we want fresh
|
||||
const mangoAccount = await this.mangoMarkets.mangoAccount.reload(
|
||||
this.mangoMarkets.connection,
|
||||
this.mangoMarkets.mangoGroup.dexProgramId
|
||||
);
|
||||
const mangoCache = await mangoGroup.loadCache(this.mangoMarkets.connection);
|
||||
await mangoGroup.loadRootBanks(this.mangoMarkets.connection);
|
||||
|
||||
////// copy pasta block from mango-ui-v3
|
||||
const balances: Balances[][] = new Array();
|
||||
|
||||
for (const {
|
||||
marketIndex,
|
||||
baseSymbol,
|
||||
name,
|
||||
} of mangoGroupConfig.spotMarkets) {
|
||||
if (!mangoAccount || !mangoGroup) {
|
||||
response.send([]);
|
||||
}
|
||||
|
||||
const openOrders: any = mangoAccount.spotOpenOrdersAccounts[marketIndex];
|
||||
const quoteCurrencyIndex = QUOTE_INDEX;
|
||||
|
||||
let nativeBaseFree = 0;
|
||||
let nativeQuoteFree = 0;
|
||||
let nativeBaseLocked = 0;
|
||||
let nativeQuoteLocked = 0;
|
||||
if (openOrders) {
|
||||
nativeBaseFree = openOrders.baseTokenFree.toNumber();
|
||||
nativeQuoteFree = openOrders.quoteTokenFree
|
||||
.add(openOrders["referrerRebatesAccrued"])
|
||||
.toNumber();
|
||||
nativeBaseLocked = openOrders.baseTokenTotal
|
||||
.sub(openOrders.baseTokenFree)
|
||||
.toNumber();
|
||||
nativeQuoteLocked = openOrders.quoteTokenTotal
|
||||
.sub(openOrders.quoteTokenFree)
|
||||
.toNumber();
|
||||
}
|
||||
|
||||
const tokenIndex = marketIndex;
|
||||
|
||||
const net = (nativeBaseLocked: number, tokenIndex: number) => {
|
||||
const amount = mangoAccount
|
||||
.getUiDeposit(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
.add(
|
||||
nativeI80F48ToUi(
|
||||
I80F48.fromNumber(nativeBaseLocked),
|
||||
mangoGroup.tokens[tokenIndex].decimals
|
||||
).sub(
|
||||
mangoAccount.getUiBorrow(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return amount;
|
||||
};
|
||||
|
||||
const value = (nativeBaseLocked: number, tokenIndex: number) => {
|
||||
const amount = mangoGroup
|
||||
.getPrice(tokenIndex, mangoCache)
|
||||
.mul(net(nativeBaseLocked, tokenIndex));
|
||||
|
||||
return amount;
|
||||
};
|
||||
|
||||
const marketPair = [
|
||||
{
|
||||
market: null as null,
|
||||
key: `${name}`,
|
||||
symbol: baseSymbol,
|
||||
deposits: mangoAccount.getUiDeposit(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
),
|
||||
borrows: mangoAccount.getUiBorrow(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
),
|
||||
orders: nativeToUi(
|
||||
nativeBaseLocked,
|
||||
mangoGroup.tokens[tokenIndex].decimals
|
||||
),
|
||||
unsettled: nativeToUi(
|
||||
nativeBaseFree,
|
||||
mangoGroup.tokens[tokenIndex].decimals
|
||||
),
|
||||
net: net(nativeBaseLocked, tokenIndex),
|
||||
value: value(nativeBaseLocked, tokenIndex),
|
||||
depositRate: i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex)),
|
||||
borrowRate: i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex)),
|
||||
},
|
||||
{
|
||||
market: null as null,
|
||||
key: `${name}`,
|
||||
symbol: mangoGroupConfig.quoteSymbol,
|
||||
deposits: mangoAccount.getUiDeposit(
|
||||
mangoCache.rootBankCache[quoteCurrencyIndex],
|
||||
mangoGroup,
|
||||
quoteCurrencyIndex
|
||||
),
|
||||
borrows: mangoAccount.getUiBorrow(
|
||||
mangoCache.rootBankCache[quoteCurrencyIndex],
|
||||
mangoGroup,
|
||||
quoteCurrencyIndex
|
||||
),
|
||||
orders: nativeToUi(
|
||||
nativeQuoteLocked,
|
||||
mangoGroup.tokens[quoteCurrencyIndex].decimals
|
||||
),
|
||||
unsettled: nativeToUi(
|
||||
nativeQuoteFree,
|
||||
mangoGroup.tokens[quoteCurrencyIndex].decimals
|
||||
),
|
||||
net: net(nativeQuoteLocked, quoteCurrencyIndex),
|
||||
value: value(nativeQuoteLocked, quoteCurrencyIndex),
|
||||
depositRate: i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex)),
|
||||
borrowRate: i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex)),
|
||||
},
|
||||
];
|
||||
balances.push(marketPair);
|
||||
}
|
||||
|
||||
const baseBalances = balances.map((b) => b[0]);
|
||||
const quoteBalances = balances.map((b) => b[1]);
|
||||
const quoteMeta = quoteBalances[0];
|
||||
const quoteInOrders = sumBy(quoteBalances, "orders");
|
||||
const unsettled = sumBy(quoteBalances, "unsettled");
|
||||
|
||||
const net: I80F48 = quoteMeta.deposits
|
||||
.add(I80F48.fromNumber(unsettled))
|
||||
.sub(quoteMeta.borrows)
|
||||
.add(I80F48.fromNumber(quoteInOrders));
|
||||
const token = getTokenBySymbol(mangoGroupConfig, quoteMeta.symbol);
|
||||
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey);
|
||||
const value = net.mul(mangoGroup.getPrice(tokenIndex, mangoCache));
|
||||
|
||||
////// end of copy pasta block from mango-ui-v3
|
||||
|
||||
// append balances for base symbols
|
||||
const balanceDtos = baseBalances.map((baseBalance) => {
|
||||
return {
|
||||
coin: baseBalance.key,
|
||||
free: baseBalance.deposits.toNumber(),
|
||||
spotBorrow: baseBalance.borrows.toNumber(),
|
||||
total: baseBalance.net.toNumber(),
|
||||
usdValue: baseBalance.value.toNumber(),
|
||||
availableWithoutBorrow: baseBalance.net
|
||||
.sub(baseBalance.borrows)
|
||||
.toNumber(),
|
||||
} as BalanceDto;
|
||||
});
|
||||
|
||||
// append balance for quote symbol
|
||||
balanceDtos.push({
|
||||
coin: this.mangoMarkets.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);
|
||||
};
|
||||
}
|
||||
|
||||
export default WalletController;
|
||||
|
||||
/// Dtos
|
||||
|
||||
// e.g.
|
||||
// {
|
||||
// "success": true,
|
||||
// "result": [
|
||||
// {
|
||||
// "coin": "USDTBEAR",
|
||||
// "free": 2320.2,
|
||||
// "spotBorrow": 0.0,
|
||||
// "total": 2340.2,
|
||||
// "usdValue": 2340.2,
|
||||
// "availableWithoutBorrow": 2320.2
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
interface BalancesDto {
|
||||
success: boolean;
|
||||
result: BalanceDto[];
|
||||
}
|
||||
|
||||
interface BalanceDto {
|
||||
coin: string;
|
||||
free: number;
|
||||
spotBorrow: number;
|
||||
total: number;
|
||||
usdValue: number;
|
||||
availableWithoutBorrow: number;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"alwaysStrict": true,
|
||||
"baseUrl": "./src",
|
||||
"emitDecoratorMetadata": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"outDir": "./dist",
|
||||
"sourceMap": true,
|
||||
"target": "ES2020"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"]
|
||||
}
|
Loading…
Reference in New Issue