Compare commits
3 Commits
d42bd9135b
...
b63ed37ec4
Author | SHA1 | Date |
---|---|---|
riordanp | b63ed37ec4 | |
Riordan Panayides | 423c446f91 | |
Riordan Panayides | e326e2a1ec |
14
package.json
14
package.json
|
@ -27,7 +27,7 @@
|
|||
"mocha": "^9.1.3",
|
||||
"prettier": "^2.0.5",
|
||||
"typedoc": "^0.22.5",
|
||||
"typescript": "^4.1.3"
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
|
@ -37,24 +37,24 @@
|
|||
"trailingComma": "all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blockworks-foundation/mango-client": "^3.4.6",
|
||||
"@blockworks-foundation/mango-client": "^3.6.5",
|
||||
"@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",
|
||||
"@solana/web3.js": "^1.43.5",
|
||||
"axios": "^0.21.1",
|
||||
"big.js": "^6.1.1",
|
||||
"bigint-buffer": "^1.1.5",
|
||||
"bn.js": "^5.2.0",
|
||||
"bn.js": "^5.1.0",
|
||||
"buffer-layout": "^1.2.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"ts-node": "^9.1.1"
|
||||
"ts-node": "^10.7.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"bn.js": "5.1.3",
|
||||
"bn.js": "5.2.0",
|
||||
"@types/bn.js": "5.1.0",
|
||||
"@solana/web3.js": "^1.31.0"
|
||||
"@solana/web3.js": "^1.43.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,19 @@ import {
|
|||
MangoClient,
|
||||
sleep,
|
||||
ZERO_I80F48,
|
||||
MarketMode,
|
||||
makeLiquidateDelistingTokenInstruction,
|
||||
makeForceSettlePerpPositionInstruction,
|
||||
TokenAccountLayout,
|
||||
TokenAccount,
|
||||
} from '@blockworks-foundation/mango-client';
|
||||
import { Commitment, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import {
|
||||
Commitment,
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
Transaction,
|
||||
} from '@solana/web3.js';
|
||||
import { Market, OpenOrders } from '@project-serum/serum';
|
||||
import BN from 'bn.js';
|
||||
import { Orderbook } from '@project-serum/serum/lib/market';
|
||||
|
@ -31,6 +42,11 @@ import * as Env from 'dotenv';
|
|||
import envExpand from 'dotenv-expand';
|
||||
import { Client as RpcWebSocketClient } from 'rpc-websockets';
|
||||
import { AsyncBlockingQueue } from './AsyncBlockingQueue';
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from '@solana/spl-token';
|
||||
|
||||
envExpand(Env.config());
|
||||
|
||||
|
@ -47,6 +63,9 @@ const rebalanceInterval = parseInt(process.env.INTERVAL_REBALANCE || '10000');
|
|||
const checkTriggers = process.env.CHECK_TRIGGERS
|
||||
? process.env.CHECK_TRIGGERS === 'true'
|
||||
: true;
|
||||
const checkDelisting = process.env.CHECK_DELISTING
|
||||
? process.env.CHECK_DELISTING === 'true'
|
||||
: false;
|
||||
const liabLimit = I80F48.fromNumber(
|
||||
Math.min(parseFloat(process.env.LIAB_LIMIT || '0.9'), 1),
|
||||
);
|
||||
|
@ -187,11 +206,425 @@ async function main() {
|
|||
}
|
||||
}
|
||||
|
||||
// never returns
|
||||
async function checkMangoGroup() {
|
||||
console.log('reloading group');
|
||||
|
||||
mangoGroup = await client.getMangoGroup(mangoGroupKey);
|
||||
await mangoGroup.loadRootBanks(connection);
|
||||
while (true) {
|
||||
try {
|
||||
for (let i = 0; i < mangoGroup.tokens.length; i++) {
|
||||
const tokenInfo = mangoGroup.tokens[i];
|
||||
|
||||
if (tokenInfo.spotMarketMode == MarketMode.ForceCloseOnly) {
|
||||
console.log('force closing spot market', i);
|
||||
const market = await Market.load(
|
||||
connection,
|
||||
mangoGroup.spotMarkets[i].spotMarket,
|
||||
undefined,
|
||||
mangoGroup.dexProgramId,
|
||||
);
|
||||
|
||||
const [dustAccountPk] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
mangoGroup.publicKey.toBytes(),
|
||||
Buffer.from('DustAccount', 'utf-8'),
|
||||
],
|
||||
groupIds.mangoProgramId,
|
||||
);
|
||||
|
||||
// Get liqor ATA
|
||||
const liqorLiabTokenAccountPk = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
tokenInfo.mint,
|
||||
liqorMangoAccount.owner,
|
||||
);
|
||||
let liqorLiabTokenAccountInfo = await connection.getAccountInfo(
|
||||
liqorLiabTokenAccountPk,
|
||||
);
|
||||
if (!liqorLiabTokenAccountInfo) {
|
||||
console.log('creating ata for liqor');
|
||||
const createAccountTx = new Transaction().add(
|
||||
Token.createAssociatedTokenAccountInstruction(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
tokenInfo.mint,
|
||||
liqorLiabTokenAccountPk,
|
||||
liqorMangoAccount.owner,
|
||||
payer.publicKey,
|
||||
),
|
||||
);
|
||||
await client.sendTransaction(createAccountTx, payer, []);
|
||||
liqorLiabTokenAccountInfo = (await connection.getAccountInfo(
|
||||
liqorLiabTokenAccountPk,
|
||||
))!;
|
||||
}
|
||||
|
||||
const liqorLiabTokenAccount = new TokenAccount(
|
||||
liqorLiabTokenAccountPk,
|
||||
TokenAccountLayout.decode(liqorLiabTokenAccountInfo.data),
|
||||
);
|
||||
|
||||
for (let mangoAccount of mangoAccounts) {
|
||||
try {
|
||||
// can't liquidate dust account or the liqor itself
|
||||
if (
|
||||
mangoAccount.publicKey.equals(dustAccountPk) ||
|
||||
mangoAccount.publicKey.equals(liqorMangoAccount.publicKey)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
mangoAccount.inMarginBasket[i] ||
|
||||
!mangoAccount.spotOpenOrders[i].equals(PublicKey.default) ||
|
||||
!mangoAccount.getNet(cache.rootBankCache[i], i).eq(ZERO_I80F48)
|
||||
) {
|
||||
console.log(
|
||||
'delisting for account',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
);
|
||||
|
||||
if (mangoAccount.beingLiquidated && perpMarkets[0]) {
|
||||
console.log(
|
||||
'resetBeingLiquidated',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
);
|
||||
//reset beingLiquidated flag
|
||||
await client.forceCancelAllPerpOrdersInMarket(
|
||||
mangoGroup,
|
||||
mangoAccount,
|
||||
perpMarkets[0],
|
||||
payer,
|
||||
10,
|
||||
);
|
||||
}
|
||||
|
||||
// Cancel all open orders for this market
|
||||
if (mangoAccount.inMarginBasket[i]) {
|
||||
console.log(
|
||||
'cancelAllSpotOrders',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
);
|
||||
await client.cancelAllSpotOrders(
|
||||
mangoGroup,
|
||||
mangoAccount,
|
||||
market,
|
||||
payer,
|
||||
20,
|
||||
);
|
||||
mangoAccount = await mangoAccount.reload(
|
||||
connection,
|
||||
mangoGroup.dexProgramId,
|
||||
);
|
||||
}
|
||||
|
||||
if (!mangoAccount.spotOpenOrders[i].equals(zeroKey)) {
|
||||
console.log(
|
||||
'closeSpotOpenOrders',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
);
|
||||
await client.closeSpotOpenOrders(
|
||||
mangoGroup,
|
||||
mangoAccount,
|
||||
payer,
|
||||
i,
|
||||
false,
|
||||
);
|
||||
await mangoAccount.reload(
|
||||
connection,
|
||||
mangoGroup.dexProgramId,
|
||||
);
|
||||
}
|
||||
|
||||
const liqeeNet = mangoAccount.getNet(cache.rootBankCache[i], i);
|
||||
|
||||
if (!liqeeNet.isZero()) {
|
||||
const liabRootBank = rootBanks[i]!;
|
||||
const liabNodeBank = rootBanks[i]!.nodeBankAccounts[0];
|
||||
const liquidateTx = new Transaction();
|
||||
|
||||
const maxAmount = liqeeNet.gt(ZERO_I80F48)
|
||||
? liqorMangoAccount
|
||||
.getNativeDeposit(rootBanks[QUOTE_INDEX]!, QUOTE_INDEX)
|
||||
.div(mangoGroup.getPriceNative(i, cache))
|
||||
.toNumber()
|
||||
: liqorLiabTokenAccount.amount;
|
||||
|
||||
// Find or create liqee ATA
|
||||
const liqeeLiabTokenAccountPk =
|
||||
await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
tokenInfo.mint,
|
||||
mangoAccount.owner,
|
||||
true,
|
||||
);
|
||||
const liqeeLiabTokenAccountInfo =
|
||||
await connection.getAccountInfo(liqeeLiabTokenAccountPk);
|
||||
if (!liqeeLiabTokenAccountInfo) {
|
||||
console.log('creating ata for liqee');
|
||||
liquidateTx.add(
|
||||
Token.createAssociatedTokenAccountInstruction(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
tokenInfo.mint,
|
||||
liqeeLiabTokenAccountPk,
|
||||
mangoAccount.owner,
|
||||
payer.publicKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const liquidateIx = makeLiquidateDelistingTokenInstruction(
|
||||
groupIds.mangoProgramId,
|
||||
mangoGroup.publicKey,
|
||||
mangoGroup.mangoCache,
|
||||
dustAccountPk,
|
||||
mangoAccount.publicKey,
|
||||
liqorMangoAccount.publicKey,
|
||||
payer.publicKey,
|
||||
rootBanks[QUOTE_INDEX]!.publicKey,
|
||||
rootBanks[QUOTE_INDEX]!.nodeBanks[0],
|
||||
liabRootBank.publicKey,
|
||||
liabNodeBank.publicKey,
|
||||
liabNodeBank.vault,
|
||||
liqeeLiabTokenAccountPk,
|
||||
liqorLiabTokenAccountPk,
|
||||
mangoGroup.signerKey,
|
||||
mangoAccount.spotOpenOrders.filter(
|
||||
(_, i) => mangoAccount.inMarginBasket[i],
|
||||
),
|
||||
liqorMangoAccount.spotOpenOrders.filter(
|
||||
(_, i) => liqorMangoAccount.inMarginBasket[i],
|
||||
),
|
||||
new BN(maxAmount),
|
||||
);
|
||||
|
||||
liquidateTx.add(liquidateIx);
|
||||
console.log(
|
||||
'liquidateDelistingToken',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
);
|
||||
await client.sendTransaction(liquidateTx, payer, []);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Error liquidating delisting token',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenInfo.spotMarketMode == MarketMode.SwappingSpotMarket) {
|
||||
console.log('preparing spot market for swapping', i);
|
||||
for (let mangoAccount of mangoAccounts) {
|
||||
try {
|
||||
if (mangoAccount.inMarginBasket[i]) {
|
||||
const market = await Market.load(
|
||||
connection,
|
||||
mangoGroup.spotMarkets[i].spotMarket,
|
||||
undefined,
|
||||
mangoGroup.dexProgramId,
|
||||
);
|
||||
|
||||
console.log(
|
||||
'cancelAllSpotOrders',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
);
|
||||
await client.cancelAllSpotOrders(
|
||||
mangoGroup,
|
||||
mangoAccount,
|
||||
market,
|
||||
payer,
|
||||
20,
|
||||
);
|
||||
|
||||
console.log('settleFunds', mangoAccount.publicKey.toBase58());
|
||||
await client.settleFunds(
|
||||
mangoGroup,
|
||||
mangoAccount,
|
||||
payer,
|
||||
market,
|
||||
);
|
||||
|
||||
await client.closeSpotOpenOrders(
|
||||
mangoGroup,
|
||||
mangoAccount,
|
||||
payer,
|
||||
i,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Error closing openorders',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenInfo.perpMarketMode == MarketMode.ForceCloseOnly) {
|
||||
console.log('force closing perp market', i);
|
||||
const perpMarket = perpMarkets.find((x) =>
|
||||
x.publicKey.equals(mangoGroup.perpMarkets[i].perpMarket),
|
||||
)!;
|
||||
const mngoVault = new TokenAccount(
|
||||
perpMarket.mngoVault,
|
||||
await connection.getAccountInfo(perpMarket.mngoVault),
|
||||
);
|
||||
const rawPrice = cache.priceCache[i].price;
|
||||
|
||||
for (let mangoAccount of mangoAccounts) {
|
||||
try {
|
||||
const perpAccount = mangoAccount.perpAccounts[i];
|
||||
// Cancel all open orders for this market
|
||||
if (
|
||||
perpAccount.bidsQuantity.gt(ZERO_BN) ||
|
||||
perpAccount.asksQuantity.gt(ZERO_BN)
|
||||
) {
|
||||
console.log(
|
||||
'cancelAllPerpOrders',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
);
|
||||
try {
|
||||
await client.cancelAllPerpOrders(
|
||||
mangoGroup,
|
||||
[perpMarket],
|
||||
mangoAccount,
|
||||
payer,
|
||||
false,
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error cancelling perp orders',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Redeem all MNGO
|
||||
if (perpAccount.mngoAccrued.gt(ZERO_BN) && mngoVault.amount > 0) {
|
||||
console.log('redeemMngo', mangoAccount.publicKey.toBase58());
|
||||
const MNGO_INDEX = groupIds!.oracles.findIndex(
|
||||
(t) => t.symbol === 'MNGO',
|
||||
);
|
||||
const mngoRootBank = rootBanks[MNGO_INDEX]!;
|
||||
const mngoNodeBank = rootBanks[MNGO_INDEX]!.nodeBankAccounts[0];
|
||||
await client.redeemMngo(
|
||||
mangoGroup,
|
||||
mangoAccount,
|
||||
perpMarket,
|
||||
payer,
|
||||
mngoRootBank.publicKey,
|
||||
mngoNodeBank.publicKey,
|
||||
mngoNodeBank.vault,
|
||||
);
|
||||
}
|
||||
|
||||
// Find an account with opposite base position and force settle against it
|
||||
let basePosition = mangoAccount.perpAccounts[i].basePosition;
|
||||
const quotePosition = mangoAccount.perpAccounts[
|
||||
i
|
||||
].getQuotePosition(cache.perpMarketCache[i]);
|
||||
|
||||
if (basePosition.isZero() && quotePosition.isZero()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sign = basePosition.gt(ZERO_BN) ? 1 : -1;
|
||||
|
||||
// get all accounts with an opposite base position
|
||||
const oppositeSignAccounts = mangoAccounts.filter((x) =>
|
||||
sign < 0
|
||||
? x.perpAccounts[i].basePosition.gt(ZERO_BN)
|
||||
: x.perpAccounts[i].basePosition.lt(ZERO_BN),
|
||||
);
|
||||
const settleTx = new Transaction();
|
||||
|
||||
for (const account of oppositeSignAccounts) {
|
||||
if (account.publicKey.equals(mangoAccount.publicKey)) {
|
||||
continue;
|
||||
}
|
||||
const otherBasePosition = account.perpAccounts[i].basePosition;
|
||||
// TODO test limit of 10, will it exceed max length?
|
||||
if (settleTx.instructions.length < 10) {
|
||||
console.log(
|
||||
'found account to settle against',
|
||||
basePosition.toNumber(),
|
||||
otherBasePosition.toNumber(),
|
||||
);
|
||||
settleTx.add(
|
||||
makeForceSettlePerpPositionInstruction(
|
||||
groupIds.mangoProgramId,
|
||||
mangoGroup.publicKey,
|
||||
mangoAccount.publicKey,
|
||||
account.publicKey,
|
||||
mangoGroup.mangoCache,
|
||||
perpMarket.publicKey,
|
||||
),
|
||||
);
|
||||
basePosition = basePosition.add(otherBasePosition);
|
||||
const postSign = basePosition.gt(ZERO_BN) ? 1 : -1;
|
||||
if (postSign !== sign) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settleTx.instructions.length > 0) {
|
||||
console.log(
|
||||
'force settling for',
|
||||
mangoAccount.publicKey.toBase58(),
|
||||
);
|
||||
await client.sendTransaction(settleTx, payer, []);
|
||||
} else {
|
||||
console.log('no accounts found to settle against');
|
||||
}
|
||||
|
||||
await client.settlePnl(
|
||||
mangoGroup,
|
||||
cache,
|
||||
mangoAccount,
|
||||
perpMarket,
|
||||
rootBanks[QUOTE_INDEX]!,
|
||||
rawPrice,
|
||||
payer,
|
||||
mangoAccounts,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Error force closing perps',
|
||||
mangoAccount.publicKey,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error checking for delisting markets', err);
|
||||
}
|
||||
await sleep(60000);
|
||||
}
|
||||
}
|
||||
|
||||
// never returns
|
||||
async function liquidatableFromSolanaRpc() {
|
||||
await refreshAccounts(mangoGroup, mangoAccounts);
|
||||
watchAccounts(groupIds.mangoProgramId, mangoGroup, mangoAccounts);
|
||||
|
||||
if (checkDelisting) {
|
||||
checkMangoGroup();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
try {
|
||||
|
@ -549,6 +982,16 @@ async function processTriggerOrders(
|
|||
const configMarketIndex = groupIds.perpMarkets.findIndex(
|
||||
(pm) => pm.marketIndex === trigger.marketIndex,
|
||||
);
|
||||
|
||||
// Remove advanced orders if the market is in force close
|
||||
if (
|
||||
mangoGroup.tokens[trigger.marketIndex].perpMarketMode ==
|
||||
MarketMode.ForceCloseOnly
|
||||
) {
|
||||
console.log('removeAdvancedOrder', mangoAccount.publicKey.toBase58(), i);
|
||||
return client.removeAdvancedOrder(mangoGroup, mangoAccount, payer, i);
|
||||
}
|
||||
|
||||
if (
|
||||
(trigger.triggerCondition == 'above' &&
|
||||
currentPrice.gt(trigger.triggerPrice)) ||
|
||||
|
|
Loading…
Reference in New Issue