diff --git a/package.json b/package.json index 444e82e..c30fd68 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "trailingComma": "all" }, "dependencies": { - "@blockworks-foundation/mango-client": "latest", + "@blockworks-foundation/mango-client": "^3.4.3", "@project-serum/anchor": "^0.16.2", "@project-serum/serum": "0.13.55", "@project-serum/sol-wallet-adapter": "^0.2.0", diff --git a/src/liquidator.ts b/src/liquidator.ts index dfc46f7..c110b3e 100644 --- a/src/liquidator.ts +++ b/src/liquidator.ts @@ -22,14 +22,19 @@ import { sleep, ZERO_I80F48, } from '@blockworks-foundation/mango-client'; -import { Account, Commitment, Connection, PublicKey } from '@solana/web3.js'; +import { + Commitment, + Connection, + Keypair, + PublicKey, +} from '@solana/web3.js'; import { Market, OpenOrders } from '@project-serum/serum'; import BN from 'bn.js'; import { Orderbook } from '@project-serum/serum/lib/market'; import axios from 'axios'; import * as Env from 'dotenv'; import envExpand from 'dotenv-expand'; -import {Client as RpcWebSocketClient} from 'rpc-websockets'; +import { Client as RpcWebSocketClient } from 'rpc-websockets'; import { AsyncBlockingQueue } from './AsyncBlockingQueue'; envExpand(Env.config()); @@ -41,7 +46,8 @@ const refreshAccountsInterval = parseInt( const refreshWebsocketInterval = parseInt( process.env.INTERVAL_WEBSOCKET || '300000', ); -const liquidatableFeedWebsocketAddress = process.env.LIQUIDATABLE_FEED_WEBSOCKET_ADDRESS; +const liquidatableFeedWebsocketAddress = + process.env.LIQUIDATABLE_FEED_WEBSOCKET_ADDRESS; const rebalanceInterval = parseInt(process.env.INTERVAL_REBALANCE || '10000'); const checkTriggers = process.env.CHECK_TRIGGERS ? process.env.CHECK_TRIGGERS === 'true' @@ -55,36 +61,43 @@ const config = new Config(IDS); const cluster = (process.env.CLUSTER || 'mainnet') as Cluster; const groupName = process.env.GROUP || 'mainnet.1'; -const groupIds = config.getGroup(cluster, groupName) ?? (() => { throw new Error(`Group ${groupName} not found`); })(); +const groupIds = + config.getGroup(cluster, groupName) ?? + (() => { + throw new Error(`Group ${groupName} not found`); + })(); // Target values to keep in spot, ordered the same as in mango client's ids.json // Example: // // MNGO BTC ETH SOL USDT SRM RAY COPE FTT MSOL // TARGETS=0 0 0 1 0 0 0 0 0 0 -const TARGETS = process.env.TARGETS?.replace(/\s+/g,' ').trim().split(' ').map((s) => parseFloat(s)) - ?? [0, 0, 0, 0, 0, 0, 0, 0, 0]; +const TARGETS = process.env.TARGETS?.replace(/\s+/g, ' ') + .trim() + .split(' ') + .map((s) => parseFloat(s)) ?? [0, 0, 0, 0, 0, 0, 0, 0, 0]; // Do not liquidate accounts that have less than this much in value -const minEquity = parseInt( - process.env.MIN_EQUITY || '0', -); -if(minEquity > 0) { +const minEquity = parseInt(process.env.MIN_EQUITY || '0'); +if (minEquity > 0) { console.log(`Minimum equity required to liquidate: ${minEquity}`); } const mangoProgramId = groupIds.mangoProgramId; const mangoGroupKey = groupIds.publicKey; -const payer = new Account( - JSON.parse( - process.env.PRIVATE_KEY || - fs.readFileSync( - process.env.KEYPAIR || os.homedir() + '/.config/solana/id.json', - 'utf-8', - ), +const payer = Keypair.fromSecretKey( + Uint8Array.from( + JSON.parse( + process.env.PRIVATE_KEY || + fs.readFileSync( + process.env.KEYPAIR || os.homedir() + '/.config/solana/id.json', + 'utf-8', + ), + ), ), ); + console.log(`Payer: ${payer.publicKey.toBase58()}`); const rpcEndpoint = process.env.ENDPOINT_URL || config.cluster_urls[cluster]; const connection = new Connection(rpcEndpoint, 'processed' as Commitment); @@ -274,56 +287,53 @@ async function liquidatableFromSolanaRpc() { } } -async function maybeLiquidateAccount(mangoAccount: MangoAccount): Promise { - const mangoAccountKeyString = mangoAccount.publicKey.toBase58(); +async function maybeLiquidateAccount( + mangoAccount: MangoAccount, +): Promise { + const mangoAccountKeyString = mangoAccount.publicKey.toBase58(); - if (!mangoAccount.isLiquidatable(mangoGroup, cache)) { - console.log( - `Account ${mangoAccountKeyString} no longer liquidatable`, - ); - return false; - } + if (!mangoAccount.isLiquidatable(mangoGroup, cache)) { + console.log(`Account ${mangoAccountKeyString} no longer liquidatable`); + return false; + } - const equity = mangoAccount.computeValue(mangoGroup, cache).toNumber() - if (equity < minEquity && minEquity > 0) { - // console.log(`Account ${mangoAccountKeyString} only has ${equity}, PASS`); - return false; - } + const equity = mangoAccount.computeValue(mangoGroup, cache).toNumber(); + if (equity < minEquity && minEquity > 0) { + // console.log(`Account ${mangoAccountKeyString} only has ${equity}, PASS`); + return false; + } - - const health = mangoAccount.getHealthRatio(mangoGroup, cache, 'Maint'); - const accountInfoString = mangoAccount.toPrettyString( - groupIds, + const health = mangoAccount.getHealthRatio(mangoGroup, cache, 'Maint'); + const accountInfoString = mangoAccount.toPrettyString( + groupIds, + mangoGroup, + cache, + ); + console.log( + `Sick account ${mangoAccountKeyString} health ratio: ${health.toString()}\n${accountInfoString}`, + ); + notify(`Sick account\n${accountInfoString}`); + try { + await liquidateAccount( mangoGroup, cache, + spotMarkets, + rootBanks, + perpMarkets, + mangoAccount, + liqorMangoAccount, ); - console.log( - `Sick account ${mangoAccountKeyString} health ratio: ${health.toString()}\n${accountInfoString}`, + + console.log('Liquidated account', mangoAccountKeyString); + notify(`Liquidated account ${mangoAccountKeyString}`); + } catch (err: any) { + console.error( + `Failed to liquidate account ${mangoAccountKeyString}: ${err}`, ); - notify(`Sick account\n${accountInfoString}`); - try { - await liquidateAccount( - mangoGroup, - cache, - spotMarkets, - rootBanks, - perpMarkets, - mangoAccount, - liqorMangoAccount, - ); + notify(`Failed to liquidate account ${mangoAccountKeyString}: ${err}`); + } - console.log('Liquidated account', mangoAccountKeyString); - notify(`Liquidated account ${mangoAccountKeyString}`); - } catch (err: any) { - console.error( - `Failed to liquidate account ${mangoAccountKeyString}: ${err}`, - ); - notify( - `Failed to liquidate account ${mangoAccountKeyString}: ${err}`, - ); - } - - return true; + return true; } async function newAccountOnLiquidatableFeed(account) { @@ -332,7 +342,7 @@ async function newAccountOnLiquidatableFeed(account) { const mangoAccountKey = new PublicKey(account); const mangoAccount = new MangoAccount(mangoAccountKey, null); - [cache, liqorMangoAccount, ] = await Promise.all([ + [cache, liqorMangoAccount] = await Promise.all([ mangoGroup.loadCache(connection), liqorMangoAccount.reload(connection, mangoGroup.dexProgramId), mangoAccount.reload(connection, mangoGroup.dexProgramId), @@ -364,16 +374,16 @@ async function liquidatableFromLiquidatableFeed() { const ws = new RpcWebSocketClient(liquidatableFeedWebsocketAddress, { max_reconnects: Infinity, }); - ws.on('open', (x) => console.log("opened liquidatable feed")); - ws.on('error', (status) => console.log("error on liquidatable feed", status)); - ws.on('close', (err) => console.log("closed liquidatable feed", err)); + ws.on('open', (x) => console.log('opened liquidatable feed')); + ws.on('error', (status) => console.log('error on liquidatable feed', status)); + ws.on('close', (err) => console.log('closed liquidatable feed', err)); ws.on('candidate', (params) => { - const account = params.account; - if (!candidatesSet.has(account)) { - candidatesSet.add(account); - candidates.enqueue(account); - console.log(`Enqueued ${account.publicKey.toBase58()}`) - } + const account = params.account; + if (!candidatesSet.has(account)) { + candidatesSet.add(account); + candidates.enqueue(account); + console.log(`Enqueued ${account.publicKey.toBase58()}`); + } }); while (true) { @@ -567,7 +577,7 @@ async function liquidateAccount( rootBanks: (RootBank | undefined)[], perpMarkets: PerpMarket[], liqee: MangoAccount, - liqor: MangoAccount + liqor: MangoAccount, ) { const hasPerpOpenOrders = liqee.perpAccounts.some( (pa) => pa.bidsQuantity.gt(ZERO_BN) || pa.asksQuantity.gt(ZERO_BN), @@ -830,7 +840,7 @@ async function liquidatePerps( perpMarkets: PerpMarket[], rootBanks: (RootBank | undefined)[], liqee: MangoAccount, - liqor: MangoAccount + liqor: MangoAccount, ) { console.log('liquidatePerps'); const lowestHealthMarket = perpMarkets @@ -894,9 +904,17 @@ async function liquidatePerps( // token liquidation of 0. // // https://discord.com/channels/791995070613159966/826034521261604874/934629112734167060 - + if (quoteRootBank) { - await client.settlePosPnl(mangoGroup, cache, liqee, perpMarkets, quoteRootBank, payer, mangoAccounts) + await client.settlePosPnl( + mangoGroup, + cache, + liqee, + perpMarkets, + quoteRootBank, + payer, + mangoAccounts, + ); } for (let i = 0; i < mangoGroup.tokens.length; i++) { @@ -1017,7 +1035,13 @@ function getDiffsAndNet( const marketIndex = groupIds!.spotMarkets[i].marketIndex; const diff = mangoAccount .getUiDeposit(cache.rootBankCache[marketIndex], mangoGroup, marketIndex) - .sub(mangoAccount.getUiBorrow(cache.rootBankCache[marketIndex], mangoGroup, marketIndex)) + .sub( + mangoAccount.getUiBorrow( + cache.rootBankCache[marketIndex], + mangoGroup, + marketIndex, + ), + ) .sub(I80F48.fromNumber(target)); diffs.push(diff); netValues.push([i, diff.mul(cache.priceCache[i].price), marketIndex]); @@ -1075,7 +1099,7 @@ async function balanceTokens( console.log('balanceTokens'); await mangoAccount.reload(connection, mangoGroup.dexProgramId); const cache = await mangoGroup.loadCache(connection); - const cancelOrdersPromises: Promise[] = []; + const cancelOrdersPromises: Promise[] = []; const bidsInfo = await getMultipleAccounts( connection, markets.map((m) => m.bidsAddress), @@ -1116,7 +1140,7 @@ async function balanceTokens( connection, mangoGroup.dexProgramId, ); - const settlePromises: Promise[] = []; + const settlePromises: Promise[] = []; for (let i = 0; i < markets.length; i++) { const marketIndex = mangoGroup.getSpotMarketIndex(markets[i].publicKey); const oo = openOrders[marketIndex]; @@ -1143,8 +1167,12 @@ async function balanceTokens( for (let i = 0; i < groupIds!.spotMarkets.length; i++) { const marketIndex = netValues[i][2]; const netIndex = netValues[i][0]; - const marketConfig = groupIds!.spotMarkets.find((m) => m.marketIndex == marketIndex)! - const market = markets.find((m) => m.publicKey.equals(mangoGroup.spotMarkets[marketIndex].spotMarket))!; + const marketConfig = groupIds!.spotMarkets.find( + (m) => m.marketIndex == marketIndex, + )!; + const market = markets.find((m) => + m.publicKey.equals(mangoGroup.spotMarkets[marketIndex].spotMarket), + )!; const liquidationFee = mangoGroup.spotMarkets[marketIndex].liquidationFee; if (Math.abs(diffs[netIndex].toNumber()) > market!.minOrderSize) { const side = netValues[i][1].gt(ZERO_I80F48) ? 'sell' : 'buy'; diff --git a/yarn.lock b/yarn.lock index 2214f11..2c5bf7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -37,20 +37,22 @@ dependencies: regenerator-runtime "^0.13.4" -"@blockworks-foundation/mango-client@latest": - version "3.2.24" - resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-client/-/mango-client-3.2.24.tgz#c46f2cff3dca06d88e304610643f40fddbf0ee76" - integrity sha512-1rWvmMBK1wGVLG7UfTpHI2FPnXfRNTLFWifGa9q/FkRLp07GzGDY3fGhquJW2NxGv2KPwzX/YJEAHX+2x485OA== +"@blockworks-foundation/mango-client@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-client/-/mango-client-3.4.3.tgz#a5d72cc4520736edf843caaf8aff5376d1c056a4" + integrity sha512-8JFyE3nlwolfcqbhAYIenv25VnNvvYcEIUUUczHc31WaCtrcwC96Epj7wprHQO4OP6uLc9TBF4zuuGlZY3Fd6Q== dependencies: - "@project-serum/anchor" "^0.16.2" + "@project-serum/anchor" "^0.21.0" "@project-serum/serum" "0.13.55" "@project-serum/sol-wallet-adapter" "^0.2.0" "@solana/spl-token" "^0.1.6" "@solana/web3.js" "^1.31.0" big.js "^6.1.1" - bn.js "^5.2.0" + bn.js "^5.1.0" buffer-layout "^1.2.1" + cross-fetch "^3.1.5" dotenv "^10.0.0" + toformat "^2.0.0" yargs "^17.0.1" "@eslint/eslintrc@^0.4.3": @@ -164,6 +166,27 @@ snake-case "^3.0.4" toml "^3.0.0" +"@project-serum/anchor@^0.21.0": + version "0.21.0" + resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.21.0.tgz#ad5fb33744991ec1900cdb2fd22707c908b12b5f" + integrity sha512-flRuW/F+iC8mitNokx82LOXyND7Dyk6n5UUPJpQv/+NfySFrNFlzuQZaBZJ4CG5g9s8HS/uaaIz1nVkDR8V/QA== + dependencies: + "@project-serum/borsh" "^0.2.4" + "@solana/web3.js" "^1.17.0" + base64-js "^1.5.1" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^5.3.1" + cross-fetch "^3.1.5" + crypto-hash "^1.3.0" + eventemitter3 "^4.0.7" + find "^0.3.0" + js-sha256 "^0.9.0" + pako "^2.0.3" + snake-case "^3.0.4" + toml "^3.0.0" + "@project-serum/borsh@^0.2.2": version "0.2.2" resolved "https://registry.yarnpkg.com/@project-serum/borsh/-/borsh-0.2.2.tgz#63e558f2d6eb6ab79086bf499dea94da3182498f" @@ -172,6 +195,14 @@ bn.js "^5.1.2" buffer-layout "^1.2.0" +"@project-serum/borsh@^0.2.4": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@project-serum/borsh/-/borsh-0.2.5.tgz#6059287aa624ecebbfc0edd35e4c28ff987d8663" + integrity sha512-UmeUkUoKdQ7rhx6Leve1SssMR/Ghv8qrEiyywyxSWg7ooV7StdpPBhciiy5eB3T0qU1BXvdRNC8TdrkxK7WC5Q== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + "@project-serum/serum@0.13.55": version "0.13.55" resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.13.55.tgz#2ac44fe7b07651274eb57ac54ea9325789df5dd7" @@ -654,7 +685,7 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-layout@^1.2.0, buffer-layout@^1.2.1: +buffer-layout@^1.2.0, buffer-layout@^1.2.1, buffer-layout@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== @@ -813,6 +844,13 @@ cross-fetch@^3.1.4: dependencies: node-fetch "2.6.1" +cross-fetch@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -1651,6 +1689,13 @@ node-fetch@2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" @@ -2023,11 +2068,21 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/toformat/-/toformat-2.0.0.tgz#7a043fd2dfbe9021a4e36e508835ba32056739d8" + integrity sha512-03SWBVop6nU8bpyZCx7SodpYznbZF5R4ljwNLBcTQzKOD9xuihRo/psX58llS1BMFhhAI08H3luot5GoXJz2pQ== + toml@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + traverse-chain@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1" @@ -2129,6 +2184,19 @@ vscode-textmate@5.2.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which@2.0.2, which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -2191,9 +2259,9 @@ yargs-parser@^20.2.2: integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== yargs-parser@^21.0.0: - version "21.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55" - integrity sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA== + version "21.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== yargs-unparser@2.0.0: version "2.0.0" @@ -2219,9 +2287,9 @@ yargs@16.2.0: yargs-parser "^20.2.2" yargs@^17.0.1: - version "17.3.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" - integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== + version "17.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.1.tgz#ebe23284207bb75cee7c408c33e722bfb27b5284" + integrity sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g== dependencies: cliui "^7.0.2" escalade "^3.1.1"