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", "license": "MIT",
"dependencies": { "dependencies": {
"@blockworks-foundation/mango-client": "^3.0.13", "@blockworks-foundation/mango-client": "^3.0.13",
"ansi-regex": "5.0.1",
"big.js": "^6.1.1", "big.js": "^6.1.1",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",

View File

@ -8,6 +8,9 @@ import {
getMultipleAccounts, getMultipleAccounts,
getTokenBySymbol, getTokenBySymbol,
GroupConfig, GroupConfig,
makeCancelPerpOrderInstruction,
makeCancelSpotOrderInstruction,
makeSettleFundsInstruction,
MangoAccount, MangoAccount,
MangoClient, MangoClient,
MangoGroup, MangoGroup,
@ -15,6 +18,7 @@ import {
PerpMarket, PerpMarket,
PerpMarketLayout, PerpMarketLayout,
PerpOrder, PerpOrder,
QUOTE_INDEX,
} from "@blockworks-foundation/mango-client"; } from "@blockworks-foundation/mango-client";
import { Market, Orderbook } from "@project-serum/serum"; import { Market, Orderbook } from "@project-serum/serum";
import { Order } from "@project-serum/serum/lib/market"; import { Order } from "@project-serum/serum/lib/market";
@ -32,6 +36,7 @@ import os from "os";
import { OrderInfo } from "types"; import { OrderInfo } from "types";
import { logger, zipDict } from "./utils"; import { logger, zipDict } from "./utils";
import BN from "bn.js"; import BN from "bn.js";
import { Transaction } from "@solana/web3.js";
class MangoSimpleClient { class MangoSimpleClient {
constructor( constructor(
@ -422,15 +427,37 @@ class MangoSimpleClient {
public async cancelAllOrders(): Promise<void> { public async cancelAllOrders(): Promise<void> {
const allMarkets = await this.fetchAllMarkets(); const allMarkets = await this.fetchAllMarkets();
const orders = (await this.fetchAllBidsAndAsks(true)).flat(); 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) => orders.map((orderInfo) =>
this.cancelOrder( this.buildCancelOrderTransaction(
orderInfo, orderInfo,
allMarkets[orderInfo.market.account.publicKey.toBase58()] 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) { 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[]> { public async getOrderByOrderId(orderId: string): Promise<OrderInfo[]> {
const orders = (await this.fetchAllBidsAndAsks(true)).flat(); const orders = (await this.fetchAllBidsAndAsks(true)).flat();
const orderInfos = orders.filter( 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() { private roundRobinClusterUrl() {
if (process.env.CLUSTER_URL) { if (process.env.CLUSTER_URL) {
return; return;

View File

@ -1,8 +1,23 @@
import { Config, GroupConfig } from "@blockworks-foundation/mango-client"; import { Config, GroupConfig } from "@blockworks-foundation/mango-client";
import { I80F48 } from "@blockworks-foundation/mango-client/lib/src/fixednum"; import { I80F48 } from "@blockworks-foundation/mango-client/lib/src/fixednum";
import { CustomValidator } from "express-validator"; import { CustomValidator } from "express-validator";
/// logging related
import pino from "pino"; 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 /// mango related

View File

@ -335,16 +335,16 @@ ansi-align@^3.0.0:
dependencies: dependencies:
string-width "^3.0.0" 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: ansi-regex@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== 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: ansi-styles@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"

View File

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