diff --git a/mango-service-v3/package.json b/mango-service-v3/package.json index 530b0d9..d01f6a2 100644 --- a/mango-service-v3/package.json +++ b/mango-service-v3/package.json @@ -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", diff --git a/mango-service-v3/src/mango.simple.client.ts b/mango-service-v3/src/mango.simple.client.ts index d9cb2d3..98dda60 100644 --- a/mango-service-v3/src/mango.simple.client.ts +++ b/mango-service-v3/src/mango.simple.client.ts @@ -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 { 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 { + 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 { 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 { + 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; diff --git a/mango-service-v3/src/utils.ts b/mango-service-v3/src/utils.ts index 173c3c5..ad46849 100644 --- a/mango-service-v3/src/utils.ts +++ b/mango-service-v3/src/utils.ts @@ -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 diff --git a/mango-service-v3/yarn.lock b/mango-service-v3/yarn.lock index 9685708..3cc72bd 100644 --- a/mango-service-v3/yarn.lock +++ b/mango-service-v3/yarn.lock @@ -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" diff --git a/py/mango_service_v3_py/api.py b/py/mango_service_v3_py/api.py index 3382086..60b942b 100644 --- a/py/mango_service_v3_py/api.py +++ b/py/mango_service_v3_py/api.py @@ -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)