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