Merge pull request #2 from microwavedcola1/dev

Dev
This commit is contained in:
microwavedcola1 2021-09-25 13:40:01 +02:00 committed by GitHub
commit eefd1f95bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 212 additions and 22 deletions

View File

@ -12,6 +12,7 @@
"license": "MIT",
"dependencies": {
"@blockworks-foundation/mango-client": "^3.0.13",
"ansi-regex": "5.0.1",
"big.js": "^6.1.1",
"body-parser": "^1.19.0",
"express": "^4.17.1",

View File

@ -8,6 +8,9 @@ import {
getMultipleAccounts,
getTokenBySymbol,
GroupConfig,
makeCancelPerpOrderInstruction,
makeCancelSpotOrderInstruction,
makeSettleFundsInstruction,
MangoAccount,
MangoClient,
MangoGroup,
@ -15,6 +18,7 @@ import {
PerpMarket,
PerpMarketLayout,
PerpOrder,
QUOTE_INDEX,
} from "@blockworks-foundation/mango-client";
import { Market, Orderbook } from "@project-serum/serum";
import { Order } from "@project-serum/serum/lib/market";
@ -32,6 +36,7 @@ import os from "os";
import { OrderInfo } from "types";
import { logger, zipDict } from "./utils";
import BN from "bn.js";
import { Transaction } from "@solana/web3.js";
class MangoSimpleClient {
constructor(
@ -422,15 +427,37 @@ class MangoSimpleClient {
public async cancelAllOrders(): Promise<void> {
const allMarkets = await this.fetchAllMarkets();
const orders = (await this.fetchAllBidsAndAsks(true)).flat();
// todo combine multiple cancels into one transaction
await Promise.all(
const transactions = await Promise.all(
orders.map((orderInfo) =>
this.cancelOrder(
this.buildCancelOrderTransaction(
orderInfo,
allMarkets[orderInfo.market.account.publicKey.toBase58()]
)
)
);
let i, j;
// assuming we can fit 10 cancel order transactions in a solana transaction
// we could switch to computing actual transactionSize every time we add an
// instruction and use a dynamic chunk size
const chunk = 10;
const transactionsToSend: Transaction[] = [];
for (i = 0, j = transactions.length; i < j; i += chunk) {
const transactionsChunk = transactions.slice(i, i + chunk);
const transactionToSend = new Transaction();
for (const transaction of transactionsChunk) {
for (const instruction of transaction.instructions) {
transactionToSend.add(instruction);
}
}
transactionsToSend.push(transactionToSend);
}
for (const transaction of transactionsToSend) {
await this.client.sendTransaction(transaction, this.owner, []);
}
}
public async cancelOrder(orderInfo: OrderInfo, market?: Market | PerpMarket) {
@ -479,6 +506,55 @@ class MangoSimpleClient {
}
}
public async buildCancelOrderTransaction(
orderInfo: OrderInfo,
market?: Market | PerpMarket
): Promise<Transaction> {
if (orderInfo.market.config.kind === "perp") {
const perpMarketConfig = getMarketByBaseSymbolAndKind(
this.mangoGroupConfig,
orderInfo.market.config.baseSymbol,
"perp"
);
if (market === undefined) {
market = await this.mangoGroup.loadPerpMarket(
this.connection,
perpMarketConfig.marketIndex,
perpMarketConfig.baseDecimals,
perpMarketConfig.quoteDecimals
);
}
return this.buildCancelPerpOrderInstruction(
this.mangoGroup,
this.mangoAccount,
this.owner,
market as PerpMarket,
orderInfo.order as PerpOrder
);
} else {
const spotMarketConfig = getMarketByBaseSymbolAndKind(
this.mangoGroupConfig,
orderInfo.market.config.baseSymbol,
"spot"
);
if (market === undefined) {
market = await Market.load(
this.connection,
spotMarketConfig.publicKey,
undefined,
this.mangoGroupConfig.serumProgramId
);
}
return await this.buildCancelSpotOrderTransaction(
this.mangoGroup,
this.mangoAccount,
this.owner,
market as Market,
orderInfo.order as Order
);
}
}
public async getOrderByOrderId(orderId: string): Promise<OrderInfo[]> {
const orders = (await this.fetchAllBidsAndAsks(true)).flat();
const orderInfos = orders.filter(
@ -580,6 +656,102 @@ class MangoSimpleClient {
}));
}
private buildCancelPerpOrderInstruction(
mangoGroup: MangoGroup,
mangoAccount: MangoAccount,
owner: Account,
perpMarket: PerpMarket,
order: PerpOrder,
invalidIdOk = false // Don't throw error if order is invalid
): Transaction {
const instruction = makeCancelPerpOrderInstruction(
this.mangoGroupConfig.mangoProgramId,
mangoGroup.publicKey,
mangoAccount.publicKey,
owner.publicKey,
perpMarket.publicKey,
perpMarket.bids,
perpMarket.asks,
order,
invalidIdOk
);
const transaction = new Transaction();
transaction.add(instruction);
return transaction;
}
private async buildCancelSpotOrderTransaction(
mangoGroup: MangoGroup,
mangoAccount: MangoAccount,
owner: Account,
spotMarket: Market,
order: Order
): Promise<Transaction> {
const transaction = new Transaction();
const instruction = makeCancelSpotOrderInstruction(
this.mangoGroupConfig.mangoProgramId,
mangoGroup.publicKey,
owner.publicKey,
mangoAccount.publicKey,
spotMarket.programId,
spotMarket.publicKey,
spotMarket["_decoded"].bids,
spotMarket["_decoded"].asks,
order.openOrdersAddress,
mangoGroup.signerKey,
spotMarket["_decoded"].eventQueue,
order
);
transaction.add(instruction);
const dexSigner = await PublicKey.createProgramAddress(
[
spotMarket.publicKey.toBuffer(),
spotMarket["_decoded"].vaultSignerNonce.toArrayLike(Buffer, "le", 8),
],
spotMarket.programId
);
const marketIndex = mangoGroup.getSpotMarketIndex(spotMarket.publicKey);
if (!mangoGroup.rootBankAccounts.length) {
await mangoGroup.loadRootBanks(this.connection);
}
const baseRootBank = mangoGroup.rootBankAccounts[marketIndex];
const quoteRootBank = mangoGroup.rootBankAccounts[QUOTE_INDEX];
const baseNodeBank = baseRootBank?.nodeBankAccounts[0];
const quoteNodeBank = quoteRootBank?.nodeBankAccounts[0];
if (!baseNodeBank || !quoteNodeBank) {
throw new Error("Invalid or missing node banks");
}
// todo what is a makeSettleFundsInstruction?
const settleFundsInstruction = makeSettleFundsInstruction(
this.mangoGroupConfig.mangoProgramId,
mangoGroup.publicKey,
mangoGroup.mangoCache,
owner.publicKey,
mangoAccount.publicKey,
spotMarket.programId,
spotMarket.publicKey,
mangoAccount.spotOpenOrders[marketIndex],
mangoGroup.signerKey,
spotMarket["_decoded"].baseVault,
spotMarket["_decoded"].quoteVault,
mangoGroup.tokens[marketIndex].rootBank,
baseNodeBank.publicKey,
mangoGroup.tokens[QUOTE_INDEX].rootBank,
quoteNodeBank.publicKey,
baseNodeBank.vault,
quoteNodeBank.vault,
dexSigner
);
transaction.add(settleFundsInstruction);
return transaction;
}
private roundRobinClusterUrl() {
if (process.env.CLUSTER_URL) {
return;

View File

@ -1,8 +1,23 @@
import { Config, GroupConfig } from "@blockworks-foundation/mango-client";
import { I80F48 } from "@blockworks-foundation/mango-client/lib/src/fixednum";
import { CustomValidator } from "express-validator";
/// logging related
import pino from "pino";
import { Transaction, Connection, Account } from "@solana/web3.js";
/// solana related
export async function transactionSize(
connection: Connection,
singleTransaction: Transaction,
owner: Account
) {
singleTransaction.recentBlockhash = (
await connection.getRecentBlockhash()
).blockhash;
singleTransaction.setSigners(owner.publicKey);
singleTransaction.sign(this.owner);
return singleTransaction.serialize().length;
}
/// mango related

View File

@ -335,16 +335,16 @@ ansi-align@^3.0.0:
dependencies:
string-width "^3.0.0"
ansi-regex@5.0.1, ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-regex@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"

View File

@ -1,6 +1,8 @@
import json
import os
import time
from decimal import Decimal
from threading import Timer
from typing import List
import httpx
@ -39,41 +41,41 @@ class MangoServiceV3Client:
else:
self.BASE_URL = "http://localhost:3000/api"
@delayed(os.environ["DELAY"])
@delayed(2)
def get_open_positions(self) -> List[Position]:
response = httpx.get(f"{self.BASE_URL}/positions", timeout=10.0)
return parse_obj_as(List[Position], json.loads(response.text)["result"])
@delayed(os.environ["DELAY"])
@delayed(2)
def get_balances(self) -> List[Balance]:
response = httpx.get(f"{self.BASE_URL}/wallet/balances", timeout=10.0)
return parse_obj_as(List[Balance], json.loads(response.text)["result"])
@delayed(os.environ["DELAY"])
@delayed(2)
def get_markets(self) -> List[Market]:
response = httpx.get(f"{self.BASE_URL}/markets", timeout=10.0)
return parse_obj_as(List[Market], json.loads(response.text)["result"])
@delayed(os.environ["DELAY"])
@delayed(2)
def get_market_by_market_name(self, market_name: str) -> List[Market]:
response = httpx.get(f"{self.BASE_URL}/markets/{market_name}", timeout=10.0)
return parse_obj_as(List[Market], json.loads(response.text)["result"])
@delayed(os.environ["DELAY"])
@delayed(2)
def get_orderboook(self, market_name: str, depth: int = 30) -> Orderbook:
response = httpx.get(
f"{self.BASE_URL}/markets/{market_name}/orderbook?depth={depth}"
)
return parse_obj_as(Orderbook, json.loads(response.text)["result"])
@delayed(os.environ["DELAY"])
@delayed(2)
def get_trades(self, market_name: str) -> List[Trade]:
response = httpx.get(
f"{self.BASE_URL}/markets/{market_name}/trades", timeout=10.0
)
return parse_obj_as(List[Trade], json.loads(response.text)["result"])
@delayed(os.environ["DELAY"])
@delayed(2)
def get_candles(
self, market_name: str, resolution: int, start_time: int, end_time: int
) -> List[Candle]:
@ -82,19 +84,19 @@ class MangoServiceV3Client:
)
return parse_obj_as(List[Candle], json.loads(response.text)["result"])
@delayed(os.environ["DELAY"])
@delayed(2)
def get_orders(self,) -> List[Order]:
response = httpx.get(f"{self.BASE_URL}/orders", timeout=10.0)
return parse_obj_as(List[Order], json.loads(response.text)["result"])
@delayed(os.environ["DELAY"])
@delayed(2)
def get_orders_by_market_name(self, market_name: str) -> List[Order]:
response = httpx.get(
f"{self.BASE_URL}/orders?market={market_name}", timeout=10.0
)
return parse_obj_as(List[Order], json.loads(response.text)["result"])
@delayed(os.environ["DELAY"])
@delayed(2)
def place_order(self, order: PlaceOrder) -> None:
response = httpx.post(
f"{self.BASE_URL}/orders", json=order.dict(by_alias=True), timeout=10.0
@ -104,17 +106,17 @@ class MangoServiceV3Client:
# List[BadRequestError], json.loads(response.text)["errors"]
# )
@delayed(os.environ["DELAY"])
@delayed(2)
def cancel_order_by_client_id(self, client_id):
response = httpx.delete(
f"{self.BASE_URL}/orders/by_client_id/{client_id}", timeout=10.0
)
@delayed(os.environ["DELAY"])
@delayed(2)
def cancel_order_by_order_id(self, order_id):
response = httpx.delete(f"{self.BASE_URL}/orders/{order_id}", timeout=10.0)
@delayed(os.environ["DELAY"])
@delayed(2)
def cancel_all_orders(self):
response = httpx.delete(f"{self.BASE_URL}/orders", timeout=10.0)