separated liquidator from mango repo
This commit is contained in:
parent
47b88d7b47
commit
f728a4f64f
|
@ -0,0 +1,17 @@
|
|||
# Mango Liquidator
|
||||
|
||||
## Setup
|
||||
Make sure to edit the .env file to look something like this:
|
||||
```
|
||||
export CLUSTER="mainnet-beta"
|
||||
export CLUSTER_URL="https://solana-api.projectserum.com"
|
||||
export KEYPAIR=~/.config/solana/id.json
|
||||
export NODE_ENV=production
|
||||
```
|
||||
|
||||
## Run
|
||||
```
|
||||
yarn install
|
||||
source .env
|
||||
yarn liquidate
|
||||
```
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "@mango/liquidator",
|
||||
"version": "0.0.1",
|
||||
"description": "Library for interacting with the serum dex",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "ts-node src/index.ts",
|
||||
"liquidate": "ts-node src/liquidate.ts",
|
||||
"test": "ts-node src/test.ts",
|
||||
"watch": "tsc --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"prepare": "run-s clean build",
|
||||
"shell": "node -e \"$(< shell)\" -i --experimental-repl-await"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@types/jest": "^26.0.9",
|
||||
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
||||
"@typescript-eslint/parser": "^4.6.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"cross-env": "^7.0.2",
|
||||
"eslint": "^7.6.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"jest": "^26.4.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.0.5",
|
||||
"ts-jest": "^26.2.0",
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mango/client": "git+ssh://git@github.com:blockworks-foundation/mango-client-ts.git#srm_account",
|
||||
"@project-serum/serum": "^0.13.20",
|
||||
"@solana/spl-token": "0.0.13",
|
||||
"@solana/web3.js": "^0.90.0",
|
||||
"bn.js": "^5.1.3",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"ts-node": "^9.1.1"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
import { IDS, MangoClient, MarginAccount, NUM_TOKENS } from '@mango/client';
|
||||
|
||||
import { Account, Connection, PublicKey, TransactionSignature } from '@solana/web3.js';
|
||||
import * as fs from 'fs';
|
||||
import { Market } from '@project-serum/serum';
|
||||
import { NUM_MARKETS } from '@mango/client/lib/layout';
|
||||
import { sleep } from './utils';
|
||||
|
||||
async function setupMarginAccounts() {
|
||||
const keyPairPath = '/home/dd/.config/solana/id.json'
|
||||
const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')))
|
||||
const cluster = "devnet";
|
||||
const client = new MangoClient();
|
||||
const clusterIds = IDS[cluster]
|
||||
|
||||
const connection = new Connection(IDS.cluster_urls[cluster], 'singleGossip')
|
||||
const mangoGroupPk = new PublicKey(clusterIds.mango_groups.BTC_ETH_USDC.mango_group_pk);
|
||||
const mangoProgramId = new PublicKey(clusterIds.mango_program_id);
|
||||
const dexProgramId = new PublicKey(clusterIds.dex_program_id)
|
||||
|
||||
let mangoGroup = await client.getMangoGroup(connection, mangoGroupPk);
|
||||
|
||||
const srmAccountPk = new PublicKey("6utvndL8EEjpwK5QVtguErncQEPVbkuyABmXu6FeygeV")
|
||||
// TODO auto fetch
|
||||
const marginAccounts = await client.getMarginAccountsForOwner(connection, mangoProgramId, mangoGroup, payer)
|
||||
let marginAccount: MarginAccount | undefined = undefined
|
||||
let minVal = 0
|
||||
for (const ma of marginAccounts) {
|
||||
const val = await ma.getValue(connection, mangoGroup)
|
||||
if (val >= minVal) {
|
||||
minVal = val
|
||||
marginAccount = ma
|
||||
}
|
||||
}
|
||||
if (marginAccount == undefined) {
|
||||
throw new Error("No margin accounts")
|
||||
}
|
||||
// await client.depositSrm(connection, mangoProgramId, mangoGroup, marginAccount, payer, srmAccountPk, 10000)
|
||||
|
||||
marginAccount = await client.getMarginAccount(connection, marginAccount.publicKey, dexProgramId)
|
||||
|
||||
const prices = await mangoGroup.getPrices(connection)
|
||||
|
||||
console.log(marginAccount.toPrettyString(mangoGroup, prices))
|
||||
|
||||
for (const ooa of marginAccount.openOrdersAccounts) {
|
||||
if (ooa == undefined) {
|
||||
continue
|
||||
}
|
||||
console.log(ooa.baseTokenFree.toString(), ooa.quoteTokenFree.toString(), ooa.baseTokenTotal.toString(), ooa.quoteTokenTotal.toString())
|
||||
}
|
||||
|
||||
const marketIndex = 0 // index for BTC/USDC
|
||||
const spotMarket = await Market.load(
|
||||
connection,
|
||||
mangoGroup.spotMarkets[marketIndex],
|
||||
{skipPreflight: true, commitment: 'singleGossip'},
|
||||
mangoGroup.dexProgramId
|
||||
)
|
||||
console.log(prices)
|
||||
|
||||
console.log('placing order')
|
||||
// margin short 0.1 BTC
|
||||
await client.placeOrder(
|
||||
connection,
|
||||
mangoProgramId,
|
||||
mangoGroup,
|
||||
marginAccount,
|
||||
spotMarket,
|
||||
payer,
|
||||
'sell',
|
||||
12000,
|
||||
0.1
|
||||
)
|
||||
|
||||
// marginAccount = await client.getCompleteMarginAccount(connection, marginAccount.publicKey, dexProgramId)
|
||||
|
||||
// await client.settleFunds(
|
||||
// connection,
|
||||
// mangoProgramId,
|
||||
// mangoGroup,
|
||||
// marginAccount,
|
||||
// payer,
|
||||
// spotMarket
|
||||
// )
|
||||
|
||||
// await client.settleBorrow(connection, mangoProgramId, mangoGroup, marginAccount, payer, mangoGroup.tokens[2], 5000)
|
||||
// await client.settleBorrow(connection, mangoProgramId, mangoGroup, marginAccount, payer, mangoGroup.tokens[0], 1.0)
|
||||
|
||||
await sleep(3000)
|
||||
marginAccount = await client.getMarginAccount(connection, marginAccount.publicKey, dexProgramId)
|
||||
console.log(marginAccount.toPrettyString(mangoGroup, prices))
|
||||
for (const ooa of marginAccount.openOrdersAccounts) {
|
||||
if (ooa == undefined) {
|
||||
continue
|
||||
}
|
||||
console.log(ooa.baseTokenFree.toString(), ooa.quoteTokenFree.toString(), ooa.baseTokenTotal.toString(), ooa.quoteTokenTotal.toString())
|
||||
}
|
||||
|
||||
|
||||
const [bids, asks] = await Promise.all([spotMarket.loadBids(connection), spotMarket.loadAsks(connection)])
|
||||
|
||||
await marginAccount.cancelAllOrdersByMarket(
|
||||
connection,
|
||||
client,
|
||||
mangoProgramId,
|
||||
mangoGroup,
|
||||
spotMarket,
|
||||
bids,
|
||||
asks,
|
||||
payer
|
||||
)
|
||||
await client.settleFunds(connection, mangoProgramId, mangoGroup, marginAccount, payer, spotMarket)
|
||||
|
||||
await sleep(3000)
|
||||
mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||
marginAccount = await client.getMarginAccount(connection, marginAccount.publicKey, dexProgramId)
|
||||
console.log(marginAccount.toPrettyString(mangoGroup, prices))
|
||||
// @ts-ignore
|
||||
for (const ooa of marginAccount.openOrdersAccounts) {
|
||||
if (ooa == undefined) {
|
||||
continue
|
||||
}
|
||||
console.log(ooa.baseTokenFree.toString(), ooa.quoteTokenFree.toString(), ooa.baseTokenTotal.toString(), ooa.quoteTokenTotal.toString())
|
||||
}
|
||||
|
||||
console.log(mangoGroup.getUiTotalDeposit(0), mangoGroup.getUiTotalBorrow(0))
|
||||
console.log(mangoGroup.getUiTotalDeposit(NUM_MARKETS), mangoGroup.getUiTotalBorrow(NUM_MARKETS))
|
||||
|
||||
}
|
||||
|
||||
async function testing() {
|
||||
const client = new MangoClient()
|
||||
const cluster = 'devnet'
|
||||
const connection = new Connection(IDS.cluster_urls[cluster], 'singleGossip')
|
||||
|
||||
// The address of the Mango Program on the blockchain
|
||||
const programId = new PublicKey(IDS[cluster].mango_program_id)
|
||||
// The address of the serum dex program on the blockchain: https://github.com/project-serum/serum-dex
|
||||
const dexProgramId = new PublicKey(IDS[cluster].dex_program_id)
|
||||
|
||||
// Address of the MangoGroup
|
||||
const mangoGroupPk = new PublicKey(IDS[cluster].mango_groups.BTC_ETH_USDC.mango_group_pk)
|
||||
|
||||
|
||||
// TODO fetch these automatically
|
||||
const keyPairPath = '/home/dd/.config/solana/id.json'
|
||||
const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')))
|
||||
|
||||
let mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||
const totalBorrow = mangoGroup.getUiTotalBorrow(0)
|
||||
const totalDeposit = mangoGroup.getUiTotalDeposit(0)
|
||||
|
||||
// save it in the database
|
||||
|
||||
mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||
|
||||
await sleep(5000)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// setupMarginAccounts()
|
||||
// main()
|
||||
// testing()
|
|
@ -0,0 +1,209 @@
|
|||
// TODO make sure funds in liquidatable account can actually be withdrawn
|
||||
// -- can't be withdrawn if total deposits == total_borrows
|
||||
|
||||
import {
|
||||
getMultipleAccounts,
|
||||
IDS,
|
||||
MangoClient,
|
||||
MangoGroup,
|
||||
MarginAccount,
|
||||
MarginAccountLayout, nativeToUi,
|
||||
NUM_TOKENS, parseTokenAccountData,
|
||||
findLargestTokenAccountForOwner,
|
||||
} from '@mango/client';
|
||||
import { Account, Connection, LAMPORTS_PER_SOL, PublicKey, TransactionSignature } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { Market, OpenOrders } from '@project-serum/serum';
|
||||
import { NUM_MARKETS } from '@mango/client/lib/layout';
|
||||
import { getUnixTs, sleep } from './utils';
|
||||
import { homedir } from 'os'
|
||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||
|
||||
|
||||
async function drainAccount(
|
||||
client: MangoClient,
|
||||
connection: Connection,
|
||||
programId: PublicKey,
|
||||
mangoGroup: MangoGroup,
|
||||
ma: MarginAccount,
|
||||
markets: Market[],
|
||||
payer: Account,
|
||||
prices: number[],
|
||||
usdWallet: PublicKey
|
||||
) {
|
||||
// Cancel all open orders
|
||||
const bidsPromises = markets.map((market) => market.loadBids(connection))
|
||||
const asksPromises = markets.map((market) => market.loadAsks(connection))
|
||||
const books = await Promise.all(bidsPromises.concat(asksPromises))
|
||||
const bids = books.slice(0, books.length / 2)
|
||||
const asks = books.slice(books.length / 2, books.length)
|
||||
|
||||
const cancelProms: Promise<TransactionSignature[]>[] = []
|
||||
for (let i = 0; i < NUM_MARKETS; i++) {
|
||||
cancelProms.push(ma.cancelAllOrdersByMarket(connection, client, programId, mangoGroup, markets[i], bids[i], asks[i], payer))
|
||||
}
|
||||
|
||||
await Promise.all(cancelProms)
|
||||
console.log('all orders cancelled')
|
||||
|
||||
console.log()
|
||||
await client.settleAll(connection, programId, mangoGroup, ma, markets, payer)
|
||||
console.log('settleAll complete')
|
||||
await sleep(2000)
|
||||
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
|
||||
|
||||
// sort non-quote currency assets by value
|
||||
const assets = ma.getAssets(mangoGroup)
|
||||
const liabs = ma.getLiabs(mangoGroup)
|
||||
|
||||
const netValues: [number, number][] = []
|
||||
|
||||
for (let i = 0; i < NUM_TOKENS - 1; i++) {
|
||||
netValues.push([i, (assets[i] - liabs[i]) * prices[i]])
|
||||
}
|
||||
netValues.sort((a, b) => (b[1] - a[1]))
|
||||
|
||||
for (let i = 0; i < NUM_TOKENS - 1; i++) {
|
||||
const marketIndex = netValues[i][0]
|
||||
const market = markets[marketIndex]
|
||||
|
||||
if (netValues[i][1] > 0) { // sell to close
|
||||
const price = prices[marketIndex] * 0.95
|
||||
const size = assets[marketIndex]
|
||||
console.log(`Sell to close ${marketIndex} ${size}`)
|
||||
await client.placeOrder(connection, programId, mangoGroup, ma, market, payer, 'sell', price, size, 'limit')
|
||||
|
||||
} else if (netValues[i][1] < 0) { // buy to close
|
||||
const price = prices[marketIndex] * 1.05 // buy at up to 5% higher than oracle price
|
||||
const size = liabs[marketIndex]
|
||||
console.log(mangoGroup.getUiTotalDeposit(NUM_MARKETS), mangoGroup.getUiTotalBorrow(NUM_MARKETS))
|
||||
console.log(ma.getUiDeposit(mangoGroup, NUM_MARKETS), ma.getUiBorrow(mangoGroup, NUM_MARKETS))
|
||||
console.log(`Buy to close ${marketIndex} ${size}`)
|
||||
await client.placeOrder(connection, programId, mangoGroup, ma, market, payer, 'buy', price, size, 'limit')
|
||||
}
|
||||
}
|
||||
await sleep(3000)
|
||||
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
|
||||
await client.settleAll(connection, programId, mangoGroup, ma, markets, payer)
|
||||
console.log('settleAll complete')
|
||||
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
|
||||
console.log('Liquidation process complete\n', ma.toPrettyString(mangoGroup, prices))
|
||||
|
||||
console.log('Withdrawing USD')
|
||||
await client.withdraw(connection, programId, mangoGroup, ma, payer, mangoGroup.tokens[NUM_TOKENS-1], usdWallet, ma.getUiDeposit(mangoGroup, NUM_TOKENS-1) * 0.999)
|
||||
|
||||
console.log('Successfully drained account', ma.publicKey.toString())
|
||||
}
|
||||
|
||||
async function runLiquidator() {
|
||||
const client = new MangoClient()
|
||||
const cluster = process.env.CLUSTER || 'mainnet-beta'
|
||||
const group_name = 'BTC_ETH_USDT'
|
||||
const clusterUrl = process.env.CLUSTER_URL || IDS.cluster_urls[cluster]
|
||||
const connection = new Connection(clusterUrl, 'singleGossip')
|
||||
|
||||
// The address of the Mango Program on the blockchain
|
||||
const programId = new PublicKey(IDS[cluster].mango_program_id)
|
||||
|
||||
// The address of the serum dex program on the blockchain: https://github.com/project-serum/serum-dex
|
||||
const dexProgramId = new PublicKey(IDS[cluster].dex_program_id)
|
||||
|
||||
// Address of the MangoGroup
|
||||
const mangoGroupPk = new PublicKey(IDS[cluster].mango_groups[group_name].mango_group_pk)
|
||||
|
||||
// liquidator's keypair
|
||||
const keyPairPath = process.env.KEYPAIR || homedir() + '/.config/solana/id.json'
|
||||
const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')))
|
||||
|
||||
let mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||
|
||||
const tokenWallets = (await Promise.all(
|
||||
mangoGroup.tokens.map(
|
||||
(mint) => findLargestTokenAccountForOwner(connection, payer.publicKey, mint).then(
|
||||
(response) => response.publicKey
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
// load all markets
|
||||
const markets = await Promise.all(mangoGroup.spotMarkets.map(
|
||||
(pk) => Market.load(connection, pk, {skipPreflight: true, commitment: 'singleGossip'}, dexProgramId)
|
||||
))
|
||||
const sleepTime = 5000
|
||||
// TODO handle failures in any of the steps
|
||||
// Find a way to get all margin accounts without querying fresh--get incremental updates to margin accounts
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||
const marginAccounts = await client.getAllMarginAccounts(connection, programId, mangoGroup)
|
||||
let prices = await mangoGroup.getPrices(connection) // TODO put this on websocket as well
|
||||
|
||||
console.log(prices)
|
||||
|
||||
const tokenAccs = await getMultipleAccounts(connection, mangoGroup.vaults)
|
||||
const vaultValues = tokenAccs.map(
|
||||
(a, i) => nativeToUi(parseTokenAccountData(a.accountInfo.data).amount, mangoGroup.mintDecimals[i])
|
||||
)
|
||||
console.log(vaultValues)
|
||||
|
||||
for (let ma of marginAccounts) { // parallelize this if possible
|
||||
|
||||
let liquidated = false
|
||||
while (true) {
|
||||
try {
|
||||
const assetsVal = ma.getAssetsVal(mangoGroup, prices)
|
||||
const liabsVal = ma.getLiabsVal(mangoGroup, prices)
|
||||
|
||||
if (liabsVal === 0) {
|
||||
break
|
||||
}
|
||||
const collRatio = assetsVal / liabsVal
|
||||
|
||||
if (collRatio >= mangoGroup.maintCollRatio) {
|
||||
break
|
||||
}
|
||||
|
||||
const deficit = liabsVal * mangoGroup.initCollRatio - assetsVal
|
||||
console.log('liquidatable', deficit)
|
||||
console.log(ma.toPrettyString(mangoGroup, prices), '\n')
|
||||
await client.liquidate(connection, programId, mangoGroup, ma, payer,
|
||||
tokenWallets, [0, 0, deficit * 1.01])
|
||||
liquidated = true
|
||||
break
|
||||
} catch (e) {
|
||||
if (!e.timeout) {
|
||||
throw e
|
||||
} else {
|
||||
await sleep(1000)
|
||||
prices = await mangoGroup.getPrices(connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (liquidated) {
|
||||
console.log('liquidation success')
|
||||
console.log(ma.toPrettyString(mangoGroup, prices))
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
ma = await client.getMarginAccount(connection, ma.publicKey, dexProgramId)
|
||||
await drainAccount(client, connection, programId, mangoGroup, ma, markets, payer, prices, tokenWallets[NUM_TOKENS-1])
|
||||
console.log('Account drain success')
|
||||
break
|
||||
} catch (e) {
|
||||
console.log('Failed while draining account. Trying again in 1s')
|
||||
await sleep(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
await sleep(sleepTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runLiquidator()
|
|
@ -0,0 +1,348 @@
|
|||
import {
|
||||
findLargestTokenAccountForOwner,
|
||||
IDS,
|
||||
MangoClient,
|
||||
MangoGroup,
|
||||
MarginAccount,
|
||||
MarginAccountLayout, nativeToUi,
|
||||
NUM_TOKENS,
|
||||
} from '@mango/client';
|
||||
import {
|
||||
Account,
|
||||
Connection, LAMPORTS_PER_SOL,
|
||||
PublicKey,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
Transaction,
|
||||
TransactionInstruction, TransactionSignature,
|
||||
} from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { getUnixTs, sleep } from './utils';
|
||||
import { createAccountInstruction, getFilteredProgramAccounts } from '@mango/client/lib/utils';
|
||||
import { encodeMangoInstruction, NUM_MARKETS } from '@mango/client/lib/layout';
|
||||
import { Token, MintLayout, AccountLayout, TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||
import { homedir } from 'os';
|
||||
import { Market } from '@project-serum/serum';
|
||||
|
||||
|
||||
async function genMarginAccounts() {
|
||||
const client = new MangoClient()
|
||||
const cluster = 'devnet'
|
||||
const connection = new Connection(IDS.cluster_urls[cluster], 'singleGossip')
|
||||
|
||||
// The address of the Mango Program on the blockchain
|
||||
const programId = new PublicKey(IDS[cluster].mango_program_id)
|
||||
// The address of the serum dex program on the blockchain: https://github.com/project-serum/serum-dex
|
||||
const dexProgramId = new PublicKey(IDS[cluster].dex_program_id)
|
||||
|
||||
// Address of the MangoGroup
|
||||
const mangoGroupPk = new PublicKey(IDS[cluster].mango_groups.BTC_ETH_USDC.mango_group_pk)
|
||||
|
||||
const keyPairPath = '/home/dd/.config/solana/id.json'
|
||||
const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')))
|
||||
|
||||
const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||
|
||||
const n = 1800
|
||||
|
||||
const t0 = getUnixTs()
|
||||
for (let i = 0; i < n; i++) {
|
||||
// const pk = await client.initMarginAccount(connection, programId, mangoGroup, payer)
|
||||
const pks = await initMultipleMarginAccounts(client, connection, programId, mangoGroup, payer, 5)
|
||||
|
||||
const elapsed = getUnixTs() - t0
|
||||
console.log(i, elapsed / (i+1), elapsed)
|
||||
|
||||
for (const pk of pks) {
|
||||
console.log(pk.toBase58())
|
||||
}
|
||||
console.log('\n')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function initMultipleMarginAccounts(
|
||||
client: MangoClient,
|
||||
connection: Connection,
|
||||
programId: PublicKey,
|
||||
mangoGroup: MangoGroup,
|
||||
owner: Account, // assumed to be same as payer for now
|
||||
n: number
|
||||
): Promise<PublicKey[]> {
|
||||
const transaction = new Transaction()
|
||||
|
||||
const additionalSigners: Account[] = []
|
||||
const marginAccountKeys: PublicKey[] = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
// Create a Solana account for the MarginAccount and allocate space
|
||||
const accInstr = await createAccountInstruction(connection,
|
||||
owner.publicKey, MarginAccountLayout.span, programId)
|
||||
|
||||
// Specify the accounts this instruction takes in (see program/src/instruction.rs)
|
||||
const keys = [
|
||||
{ isSigner: false, isWritable: false, pubkey: mangoGroup.publicKey },
|
||||
{ isSigner: false, isWritable: true, pubkey: accInstr.account.publicKey },
|
||||
{ isSigner: true, isWritable: false, pubkey: owner.publicKey },
|
||||
{ isSigner: false, isWritable: false, pubkey: SYSVAR_RENT_PUBKEY }
|
||||
]
|
||||
|
||||
// Encode and create instruction for actual initMarginAccount instruction
|
||||
const data = encodeMangoInstruction({ InitMarginAccount: {} })
|
||||
const initMarginAccountInstruction = new TransactionInstruction( { keys, data, programId })
|
||||
|
||||
// Add all instructions to one atomic transaction
|
||||
transaction.add(accInstr.instruction)
|
||||
transaction.add(initMarginAccountInstruction)
|
||||
|
||||
// Specify signers in addition to the wallet
|
||||
additionalSigners.push(accInstr.account)
|
||||
|
||||
|
||||
marginAccountKeys.push(accInstr.account.publicKey)
|
||||
}
|
||||
|
||||
// sign, send and confirm transaction
|
||||
await client.sendTransaction(connection, transaction, owner, additionalSigners)
|
||||
|
||||
return marginAccountKeys
|
||||
|
||||
}
|
||||
|
||||
async function testRent() {
|
||||
const client = new MangoClient()
|
||||
const cluster = 'mainnet-beta'
|
||||
const connection = new Connection(IDS.cluster_urls[cluster], 'singleGossip')
|
||||
const r = await connection.getMinimumBalanceForRentExemption(240, 'singleGossip')
|
||||
|
||||
console.log(r, LAMPORTS_PER_SOL, r / LAMPORTS_PER_SOL, 16 * r / LAMPORTS_PER_SOL)
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function testTokenCall() {
|
||||
|
||||
const client = new MangoClient()
|
||||
const cluster = 'mainnet-beta'
|
||||
const clusterUrl = IDS['cluster_urls'][cluster]
|
||||
const connection = new Connection(clusterUrl, 'singleGossip')
|
||||
const usdtKey = new PublicKey(IDS[cluster]['symbols']['USDC'])
|
||||
// const usdtKey = new PublicKey("8GxiBm7XirFqisDry3QdgiZDYMNfuZF1RKFTQbqBRVmp")
|
||||
|
||||
const filters = [
|
||||
{
|
||||
memcmp: {
|
||||
offset: AccountLayout.offsetOf('mint'),
|
||||
bytes: usdtKey.toBase58(),
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
dataSize: AccountLayout.span,
|
||||
},
|
||||
]
|
||||
const t0 = getUnixTs()
|
||||
const accounts = await getFilteredProgramAccounts(connection, TOKEN_PROGRAM_ID, filters)
|
||||
const t1 = getUnixTs()
|
||||
console.log(accounts.length, t1 - t0)
|
||||
}
|
||||
|
||||
async function testServer() {
|
||||
const cluster = 'mainnet-beta'
|
||||
let clusterUrl = process.env.CLUSTER_URL
|
||||
if (!clusterUrl) {
|
||||
clusterUrl = IDS['cluster_urls'][cluster]
|
||||
}
|
||||
const connection = new Connection(clusterUrl, 'singleGossip')
|
||||
const usdtKey = new PublicKey(IDS[cluster]['symbols']['USDT'])
|
||||
const filters = [
|
||||
{
|
||||
memcmp: {
|
||||
offset: AccountLayout.offsetOf('mint'),
|
||||
bytes: usdtKey.toBase58(),
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
dataSize: AccountLayout.span,
|
||||
},
|
||||
]
|
||||
const t0 = getUnixTs()
|
||||
const accounts = await getFilteredProgramAccounts(connection, TOKEN_PROGRAM_ID, filters)
|
||||
const t1 = getUnixTs()
|
||||
console.log(accounts.length, t1 - t0, accounts.length * AccountLayout.span)
|
||||
}
|
||||
|
||||
async function drainAccount(
|
||||
client: MangoClient,
|
||||
connection: Connection,
|
||||
programId: PublicKey,
|
||||
mangoGroup: MangoGroup,
|
||||
ma: MarginAccount,
|
||||
markets: Market[],
|
||||
payer: Account,
|
||||
prices: number[],
|
||||
usdWallet: PublicKey
|
||||
) {
|
||||
// Cancel all open orders
|
||||
const bidsPromises = markets.map((market) => market.loadBids(connection))
|
||||
const asksPromises = markets.map((market) => market.loadAsks(connection))
|
||||
const books = await Promise.all(bidsPromises.concat(asksPromises))
|
||||
const bids = books.slice(0, books.length / 2)
|
||||
const asks = books.slice(books.length / 2, books.length)
|
||||
|
||||
const cancelProms: Promise<TransactionSignature[]>[] = []
|
||||
for (let i = 0; i < NUM_MARKETS; i++) {
|
||||
cancelProms.push(ma.cancelAllOrdersByMarket(connection, client, programId, mangoGroup, markets[i], bids[i], asks[i], payer))
|
||||
}
|
||||
|
||||
await Promise.all(cancelProms)
|
||||
console.log('all orders cancelled')
|
||||
|
||||
console.log()
|
||||
await client.settleAll(connection, programId, mangoGroup, ma, markets, payer)
|
||||
console.log('settleAll complete')
|
||||
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
|
||||
|
||||
// sort non-quote currency assets by value
|
||||
const assets = ma.getAssets(mangoGroup)
|
||||
const liabs = ma.getLiabs(mangoGroup)
|
||||
|
||||
const netValues: [number, number][] = []
|
||||
|
||||
for (let i = 0; i < NUM_TOKENS - 1; i++) {
|
||||
netValues.push([i, (assets[i] - liabs[i]) * prices[i]])
|
||||
}
|
||||
netValues.sort((a, b) => (b[1] - a[1]))
|
||||
|
||||
for (let i = 0; i < NUM_TOKENS - 1; i++) {
|
||||
const marketIndex = netValues[i][0]
|
||||
const market = markets[marketIndex]
|
||||
|
||||
if (netValues[i][1] > 0) { // sell to close
|
||||
const price = prices[marketIndex] * 0.95
|
||||
const size = assets[marketIndex]
|
||||
console.log(`Sell to close ${marketIndex} ${size}`)
|
||||
await client.placeOrder(connection, programId, mangoGroup, ma, market, payer, 'sell', price, size, 'limit')
|
||||
|
||||
} else if (netValues[i][1] < 0) { // buy to close
|
||||
const price = prices[marketIndex] * 1.05 // buy at up to 5% higher than oracle price
|
||||
const size = liabs[marketIndex]
|
||||
console.log(mangoGroup.getUiTotalDeposit(NUM_MARKETS), mangoGroup.getUiTotalBorrow(NUM_MARKETS))
|
||||
console.log(ma.getUiDeposit(mangoGroup, NUM_MARKETS), ma.getUiBorrow(mangoGroup, NUM_MARKETS))
|
||||
console.log(`Buy to close ${marketIndex} ${size}`)
|
||||
await client.placeOrder(connection, programId, mangoGroup, ma, market, payer, 'buy', price, size, 'limit')
|
||||
}
|
||||
}
|
||||
|
||||
await client.settleAll(connection, programId, mangoGroup, ma, markets, payer)
|
||||
console.log('settleAll complete')
|
||||
ma = await client.getMarginAccount(connection, ma.publicKey, mangoGroup.dexProgramId)
|
||||
console.log('Liquidation process complete\n', ma.toPrettyString(mangoGroup, prices))
|
||||
|
||||
console.log('Withdrawing USD')
|
||||
await client.withdraw(connection, programId, mangoGroup, ma, payer, mangoGroup.tokens[NUM_TOKENS-1], usdWallet, ma.getUiDeposit(mangoGroup, NUM_TOKENS-1) * 0.999)
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function testAll() {
|
||||
const client = new MangoClient()
|
||||
const cluster = 'mainnet-beta'
|
||||
const clusterUrl = process.env.CLUSTER_URL || IDS.cluster_urls[cluster]
|
||||
const connection = new Connection(clusterUrl, 'singleGossip')
|
||||
const programId = new PublicKey(IDS[cluster].mango_program_id)
|
||||
const dexProgramId = new PublicKey(IDS[cluster].dex_program_id)
|
||||
const mangoGroupPk = new PublicKey(IDS[cluster].mango_groups['BTC_ETH_USDT'].mango_group_pk)
|
||||
|
||||
const keyPairPath = process.env.KEYPAIR || homedir() + '/.config/solana/id.json'
|
||||
|
||||
const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')))
|
||||
const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk)
|
||||
|
||||
|
||||
/**
|
||||
* Verify that balances in the vault matches total deposits + amount in all the open orders
|
||||
*/
|
||||
async function testVaultBalances() {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Test what happens when you try to withdraw what's in your deposits, but some of your coins are still borrowed
|
||||
*/
|
||||
async function testWithdrawExcess() {
|
||||
return 0
|
||||
}
|
||||
|
||||
async function testPlaceCancelOrder() {
|
||||
|
||||
const prices = await mangoGroup.getPrices(connection)
|
||||
const marginAccounts = (await client.getMarginAccountsForOwner(connection, programId, mangoGroup, payer))
|
||||
marginAccounts.sort(
|
||||
(a, b) => (a.computeValue(mangoGroup, prices) > b.computeValue(mangoGroup, prices) ? -1 : 1)
|
||||
)
|
||||
let marginAccount = marginAccounts[0]
|
||||
|
||||
const market = await Market.load(connection, mangoGroup.spotMarkets[0], { skipPreflight: true, commitment: 'singleGossip'}, mangoGroup.dexProgramId)
|
||||
console.log('placing order')
|
||||
const txid = await client.placeOrder(connection, programId, mangoGroup, marginAccount, market, payer, 'buy', 48000, 0.0001)
|
||||
console.log('order placed')
|
||||
|
||||
await sleep(5000)
|
||||
marginAccount = await client.getMarginAccount(connection, marginAccount.publicKey, mangoGroup.dexProgramId)
|
||||
const bids = await market.loadBids(connection)
|
||||
const asks = await market.loadAsks(connection)
|
||||
console.log('canceling orders')
|
||||
await marginAccount.cancelAllOrdersByMarket(connection, client, programId, mangoGroup, market, bids, asks, payer)
|
||||
console.log('orders canceled')
|
||||
|
||||
}
|
||||
|
||||
async function testGetOpenOrdersLatency() {
|
||||
const t0 = getUnixTs()
|
||||
const accounts = await client.getMarginAccountsForOwner(connection, programId, mangoGroup, payer)
|
||||
const t1 = getUnixTs()
|
||||
console.log(t1 - t0, accounts.length)
|
||||
}
|
||||
|
||||
async function testDrainAccount() {
|
||||
const prices = await mangoGroup.getPrices(connection)
|
||||
const tokenWallets = (await Promise.all(
|
||||
mangoGroup.tokens.map(
|
||||
(mint) => findLargestTokenAccountForOwner(connection, payer.publicKey, mint).then(
|
||||
(response) => response.publicKey
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
// load all markets
|
||||
const markets = await Promise.all(mangoGroup.spotMarkets.map(
|
||||
(pk) => Market.load(connection, pk, {skipPreflight: true, commitment: 'singleGossip'}, dexProgramId)
|
||||
))
|
||||
|
||||
const marginAccountPk = new PublicKey("BrfYHWjU8UaWELfdR73qug1T5bWReg2tNJwUyHbzCgc2")
|
||||
const ma = await client.getMarginAccount(connection, marginAccountPk, mangoGroup.dexProgramId)
|
||||
while (true) {
|
||||
try {
|
||||
await drainAccount(client, connection, programId, mangoGroup, ma, markets, payer, prices, tokenWallets[NUM_TOKENS-1])
|
||||
console.log('complete')
|
||||
break
|
||||
} catch (e) {
|
||||
await sleep(1000)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function testBorrowLimits() {
|
||||
console.log(mangoGroup.borrowLimits.map((b, i) => nativeToUi(b, mangoGroup.mintDecimals[i])))
|
||||
}
|
||||
|
||||
await testBorrowLimits()
|
||||
// await testGetOpenOrdersLatency()
|
||||
// await testPlaceCancelOrder()
|
||||
// await testDrainAccount()
|
||||
}
|
||||
|
||||
|
||||
testAll()
|
||||
// testServer()
|
|
@ -0,0 +1,7 @@
|
|||
export function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const getUnixTs = () => {
|
||||
return new Date().getTime() / 1000;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "@tsconfig/node14/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noImplicitAny": false,
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"]
|
||||
}
|
Loading…
Reference in New Issue