Skip to content

Commit

Permalink
Merge branch 'man0s/sdk-prep-2' of https://github.com/mrgnlabs/mrgn-ts
Browse files Browse the repository at this point in the history
…into man0s/sdk-prep-2
  • Loading branch information
k0beLeenders committed Aug 31, 2023
2 parents c26b571 + ddaeb03 commit 5038600
Show file tree
Hide file tree
Showing 27 changed files with 1,288 additions and 780 deletions.
5 changes: 3 additions & 2 deletions apps/alpha-liquidator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
"watch": "tsc-watch -p tsconfig.json --onCompilationComplete 'yarn build' --onSuccess 'yarn serve'",
"setup": "ts-node src/setup.ts",
"inspect": "ts-node src/inspect.ts",
"serve": "pm2-runtime scripts/pm2.config.js"
"serve": "pm2-runtime scripts/pm2.config.js",
"start": "ts-node src/runLiquidatorOnlyJup.ts"
},
"license": "MIT",
"dependencies": {
"@jup-ag/core": "^4.0.0-beta.18",
"@jup-ag/core": "^4.0.0-beta.20",
"@mongodb-js/zstd": "^1.1.0",
"@mrgnlabs/eslint-config-custom": "*",
"@mrgnlabs/marginfi-client-v2": "*",
Expand Down
20 changes: 19 additions & 1 deletion apps/alpha-liquidator/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,28 @@ import { loadKeypair } from "@mrgnlabs/mrgn-common";
import * as fs from "fs";
import path from "path";
import { homedir } from "os";
import BigNumber from "bignumber.js";

const Sentry = require("@sentry/node");

dotenv.config();

// Nicely log when LIQUIDATOR_PK, WALLET_KEYPAIR, or RPC_ENDPOINT are missing
if (!process.env.LIQUIDATOR_PK) {
console.error("LIQUIDATOR_PK is required");
process.exit(1);
}

if (!process.env.WALLET_KEYPAIR) {
console.error("WALLET_KEYPAIR is required");
process.exit(1);
}

if (!process.env.RPC_ENDPOINT) {
console.error("RPC_ENDPOINT is required");
process.exit(1);
}

/*eslint sort-keys: "error"*/
let envSchema = z.object({
IS_DEV: z
Expand All @@ -31,6 +48,7 @@ let envSchema = z.object({
return pkArrayStr.split(",").map((pkStr) => new PublicKey(pkStr));
})
.optional(),
MIN_LIQUIDATION_AMOUNT_USD_UI: z.string().default("0.1").transform((s) => new BigNumber(s)),
MIN_SOL_BALANCE: z.coerce.number().default(0.5),
MRGN_ENV: z
.enum(["production", "alpha", "staging", "dev", "mainnet-test-1", "dev.1"])
Expand All @@ -43,7 +61,7 @@ let envSchema = z.object({
.default("false")
.transform((s) => s === "true" || s === "1"),
SENTRY_DSN: z.string().optional(),
SLEEP_INTERVAL: z.number().default(5_000),
SLEEP_INTERVAL: z.string().default("10000").transform((s) => parseInt(s, 10)),
WALLET_KEYPAIR: z.string().transform((keypairStr) => {
if (fs.existsSync(resolveHome(keypairStr))) {
return loadKeypair(keypairStr);
Expand Down
95 changes: 59 additions & 36 deletions apps/alpha-liquidator/src/liquidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import { Bank } from "@mrgnlabs/marginfi-client-v2/dist/models/bank";

const DUST_THRESHOLD = new BigNumber(10).pow(USDC_DECIMALS - 2);
const DUST_THRESHOLD_UI = new BigNumber(0.1);
const DUST_THRESHOLD_VALUE_UI = new BigNumber(0);
const MIN_LIQUIDATION_AMOUNT_USD_UI = env_config.MIN_LIQUIDATION_AMOUNT_USD_UI;

const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");

const MIN_SOL_BALANCE = env_config.MIN_SOL_BALANCE * LAMPORTS_PER_SOL;
const SLIPPAGE_BPS = 10000;

Expand Down Expand Up @@ -76,6 +78,7 @@ class Liquidator {

private async mainLoop() {
const debug = getDebugLogger("main-loop");
drawSpinner("Scanning")
try {
await this.swapNonUsdcInTokenAccounts();
while (true) {
Expand All @@ -85,7 +88,10 @@ class Liquidator {
continue;
}

await this.liquidationStage();
// Don't sleep after liquidating an account, start rebalance immediately
if (!await this.liquidationStage()) {
await sleep(env_config.SLEEP_INTERVAL);
}
}
} catch (e) {
console.error(e);
Expand All @@ -107,6 +113,7 @@ class Liquidator {
outputMint: mintOut,
amount: JSBI.BigInt(amountIn.toString()),
slippageBps: SLIPPAGE_BPS,
forceFetch: true,
});

const route = routesInfos[0];
Expand All @@ -116,7 +123,7 @@ class Liquidator {
const result = await execute();

// @ts-ignore
if (result.error) {
if (result.error && false) {
// @ts-ignore
debug("Error: %s", result.error);
// @ts-ignore
Expand Down Expand Up @@ -367,7 +374,7 @@ class Liquidator {
return { bank, assets, liabilities };
})
.filter(({ bank, assets, liabilities }) => {
return (assets.gt(DUST_THRESHOLD_VALUE_UI) && !bank.mint.equals(USDC_MINT)) || liabilities.gt(new BigNumber(0));
return (assets.gt(DUST_THRESHOLD) && !bank.mint.equals(USDC_MINT)) || liabilities.gt(new BigNumber(0));
});

const lendingAccountToRebalanceExists = lendingAccountToRebalance.length > 0;
Expand All @@ -383,47 +390,49 @@ class Liquidator {
return lendingAccountToRebalanceExists;
}

private async liquidationStage() {
private async liquidationStage(): Promise<boolean> {
const debug = getDebugLogger("liquidation-stage");
debug("Started liquidation stage");
const allAccounts = await this.client.getAllMarginfiAccountAddresses();
const targetAccounts = allAccounts.filter((address) => {
const allAccounts = await this.client.getAllMarginfiAccounts();
const targetAccounts = allAccounts.filter((account) => {
if (this.account_whitelist) {
return this.account_whitelist.find((whitelistedAddress) => whitelistedAddress.equals(address)) !== undefined;
return this.account_whitelist.find((whitelistedAddress) => whitelistedAddress.equals(account.address)) !== undefined;
} else if (this.account_blacklist) {
return this.account_blacklist.find((whitelistedAddress) => whitelistedAddress.equals(address)) === undefined;
return this.account_blacklist.find((whitelistedAddress) => whitelistedAddress.equals(account.address)) === undefined;
}
return true;
});
const addresses = shuffle(targetAccounts);

const accounts = shuffle(targetAccounts);
debug("Found %s accounts in total", allAccounts.length);
debug("Monitoring %s accounts", targetAccounts.length);

for (let i = 0; i < addresses.length; i++) {
const liquidatedAccount = await this.processAccount(addresses[i]);
for (let i = 0; i < accounts.length; i++) {
const liquidatedAccount = await this.processAccount(accounts[i]);

debug("Account %s liquidated: %s", addresses[i], liquidatedAccount);
debug("Account %s liquidated: %s", accounts[i], liquidatedAccount);

if (liquidatedAccount) {
debug("Account liquidated, stopping to rebalance");
break;
return true;
}
}

return false;
}

private async processAccount(account: PublicKey): Promise<boolean> {
const client = this.client;
const group = this.client;
private async processAccount(marginfiAccount: MarginfiAccountWrapper): Promise<boolean> {
const group = this.client.group;
const liquidatorAccount = this.account;

if (account.equals(liquidatorAccount.address)) {
if (marginfiAccount.address.equals(liquidatorAccount.address)) {
return false;
}

const debug = getDebugLogger(`process-account:${account.toBase58()}`);
const debug = getDebugLogger(`process-account:${marginfiAccount.address.toBase58()}`);

debug("Processing account %s", marginfiAccount.address);

debug("Processing account %s", account);
const marginfiAccount = await MarginfiAccountWrapper.fetch(account, client);
if (marginfiAccount.canBeLiquidated()) {
const { assets, liabilities } = marginfiAccount.computeHealthComponents(MarginRequirementType.Maintenance);

Expand All @@ -434,16 +443,16 @@ class Liquidator {
return false;
}

captureMessage(`Liquidating account ${account.toBase58()}`);
captureMessage(`Liquidating account ${marginfiAccount.address.toBase58()}`);

let maxLiabilityPaydownUsdValue = new BigNumber(0);
let bestLiabAccountIndex = 0;

// Find the biggest liability account that can be covered by liquidator
for (let i = 0; i < marginfiAccount.activeBalances.length; i++) {
const balance = marginfiAccount.activeBalances[i];
const bank = group.getBankByPk(balance.bankPk)!;
const priceInfo = group.getOraclePriceByBank(balance.bankPk)!;
const bank = this.client.getBankByPk(balance.bankPk)!;
const priceInfo = this.client.getOraclePriceByBank(balance.bankPk)!;

if (EXCLUDE_ISOLATED_BANKS && bank.config.assetWeightInit.isEqualTo(0)) {
debug("Skipping isolated bank %s", this.getTokenSymbol(bank));
Expand Down Expand Up @@ -476,10 +485,10 @@ class Liquidator {
debug(
"Biggest liability balance paydown USD value: %d, mint: %s",
maxLiabilityPaydownUsdValue,
group.getBankByPk(marginfiAccount.activeBalances[bestLiabAccountIndex].bankPk)!.mint
this.client.getBankByPk(marginfiAccount.activeBalances[bestLiabAccountIndex].bankPk)!.mint
);

if (maxLiabilityPaydownUsdValue.lt(DUST_THRESHOLD_UI)) {
if (maxLiabilityPaydownUsdValue.lt(MIN_LIQUIDATION_AMOUNT_USD_UI)) {
debug("No liability to liquidate");
return false;
}
Expand All @@ -490,8 +499,8 @@ class Liquidator {
// Find the biggest collateral account
for (let i = 0; i < marginfiAccount.activeBalances.length; i++) {
const balance = marginfiAccount.activeBalances[i];
const bank = group.getBankByPk(balance.bankPk)!;
const priceInfo = group.getOraclePriceByBank(balance.bankPk)!;
const bank = this.client.getBankByPk(balance.bankPk)!;
const priceInfo = this.client.getOraclePriceByBank(balance.bankPk)!;

if (EXCLUDE_ISOLATED_BANKS && bank.config.assetWeightInit.isEqualTo(0)) {
debug("Skipping isolated bank %s", this.getTokenSymbol(bank));
Expand All @@ -508,16 +517,16 @@ class Liquidator {
debug(
"Max collateral USD value: %d, mint: %s",
maxCollateralUsd,
group.getBankByPk(marginfiAccount.activeBalances[bestCollateralIndex].bankPk)!.mint
this.client.getBankByPk(marginfiAccount.activeBalances[bestCollateralIndex].bankPk)!.mint
);

const collateralBankPk = marginfiAccount.activeBalances[bestCollateralIndex].bankPk;
const collateralBank = group.getBankByPk(collateralBankPk)!;
const collateralPriceInfo = group.getOraclePriceByBank(collateralBankPk)!;
const collateralBank = this.client.getBankByPk(collateralBankPk)!;
const collateralPriceInfo = this.client.getOraclePriceByBank(collateralBankPk)!;

const liabBankPk = marginfiAccount.activeBalances[bestLiabAccountIndex].bankPk;
const liabBank = group.getBankByPk(liabBankPk)!;
const liabPriceInfo = group.getOraclePriceByBank(liabBankPk)!;
const liabBank = this.client.getBankByPk(liabBankPk)!;
const liabPriceInfo = this.client.getOraclePriceByBank(liabBankPk)!;

// MAX collateral amount to liquidate for given banks and the trader marginfi account balances
// this doesn't account for liquidators liquidation capacity
Expand Down Expand Up @@ -557,12 +566,12 @@ class Liquidator {

const slippageAdjustedCollateralAmountToLiquidate = collateralAmountToLiquidate.times(0.75);

if (slippageAdjustedCollateralAmountToLiquidate.lt(DUST_THRESHOLD_UI)) {
if (slippageAdjustedCollateralAmountToLiquidate.lt(MIN_LIQUIDATION_AMOUNT_USD_UI)) {
debug("No collateral to liquidate");
return false;
}

debug(
console.log(
"Liquidating %d %s for %s",
slippageAdjustedCollateralAmountToLiquidate,
this.getTokenSymbol(collateralBank),
Expand All @@ -575,7 +584,7 @@ class Liquidator {
slippageAdjustedCollateralAmountToLiquidate,
liabBank.address
);
debug("Liquidation tx: %s", sig);
console.log("Liquidation tx: %s", sig);

return true;
}
Expand All @@ -601,3 +610,17 @@ const shuffle = ([...arr]) => {
};

export { Liquidator };

function drawSpinner(message: string) {
if (!!process.env.DEBUG) {
// Don't draw spinner when logging is enabled
return;
}
const spinnerFrames = ['-', '\\', '|', '/'];
let frameIndex = 0;

setInterval(() => {
process.stdout.write(`\r${message} ${spinnerFrames[frameIndex]}`);
frameIndex = (frameIndex + 1) % spinnerFrames.length;
}, 100);
}
42 changes: 42 additions & 0 deletions apps/alpha-liquidator/src/runLiquidatorOnlyJup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Jupiter } from "@jup-ag/core";
import { ammsToExclude } from "./ammsToExclude";
import { connection } from "./utils/connection";
import { NodeWallet } from "@mrgnlabs/mrgn-common";
import { getConfig, MarginfiAccountWrapper, MarginfiClient } from "@mrgnlabs/marginfi-client-v2";
import { env_config } from "./config";
import { Liquidator } from "./liquidator";

async function start() {
console.log("Initializing");
const wallet = new NodeWallet(env_config.WALLET_KEYPAIR);

const jupiter = await Jupiter.load({
connection: connection,
cluster: "mainnet-beta",
routeCacheDuration: 5_000,
restrictIntermediateTokens: true,
ammsToExclude,
usePreloadedAddressLookupTableCache: true,
user: wallet.payer,
});

const config = getConfig(env_config.MRGN_ENV);
const client = await MarginfiClient.fetch(config, wallet, connection);

const liquidatorAccount = await MarginfiAccountWrapper.fetch(env_config.LIQUIDATOR_PK, client);
const liquidator = new Liquidator(
connection,
liquidatorAccount,
client,
wallet,
jupiter,
env_config.MARGINFI_ACCOUNT_WHITELIST,
env_config.MARGINFI_ACCOUNT_BLACKLIST
);
await liquidator.start();
}

start().catch((e) => {
console.log(e);
process.exit(1);
});
8 changes: 6 additions & 2 deletions apps/marginfi-v2-ui/next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
module.exports = {
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
/**
* Dynamic configuration available for the browser and server.
* Note: requires `ssr: true` or a `getInitialProps` in `_app.tsx`
Expand Down Expand Up @@ -63,4 +67,4 @@ module.exports = {
}
],
},
};
});
1 change: 1 addition & 0 deletions apps/marginfi-v2-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@mrgnlabs/mrgn-common": "*",
"@mui/icons-material": "^5.11.0",
"@mui/material": "^5.11.2",
"@next/bundle-analyzer": "^13.4.19",
"@next/font": "13.1.1",
"@socialgouv/matomo-next": "^1.4.0",
"@solana/wallet-adapter-base": "^0.9.20",
Expand Down
4 changes: 1 addition & 3 deletions apps/marginfi-v2-ui/src/components/AssetsList/AssetsList.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import Image from "next/image";
import React, { FC, useEffect, useRef, useState } from "react";
import { useWallet } from "@solana/wallet-adapter-react";
import { Card, Skeleton, Table, TableHead, TableBody, TableContainer, TableRow, TableCell } from "@mui/material";
import { Card, Table, TableHead, TableBody, TableContainer, TableCell } from "@mui/material";
import { styled } from "@mui/material/styles";
import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { BorrowLendToggle } from "./BorrowLendToggle";
import AssetRow from "./AssetRow";
import { useMrgnlendStore, useUserProfileStore } from "~/store";
import { useHotkeys } from "react-hotkeys-hook";
import { BankMetadata } from "@mrgnlabs/mrgn-common";
import { ExtendedBankMetadata } from "@mrgnlabs/marginfi-v2-ui-state";
import { LoadingAsset } from "./AssetRow/AssetRow";

const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
Expand Down
2 changes: 1 addition & 1 deletion apps/marginfi-v2-ui/src/components/CampaignWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import {
NATIVE_MINT,
} from "@mrgnlabs/mrgn-common";
import { useLipClient } from "~/context";
import { EarnAction } from "~/pages/earn";
import { useWallet } from "@solana/wallet-adapter-react";
import { MenuItem, Select, TextField } from "@mui/material";
import { Bank } from "@mrgnlabs/marginfi-client-v2";
import Image from "next/image";
import { NumberFormatValues, NumericFormat } from "react-number-format";
import { useMrgnlendStore } from "~/store";
import { computeGuaranteedApy } from "@mrgnlabs/lip-client";
import { EarnAction } from "./Earn";

interface CampaignWizardInputBox {
value: number;
Expand Down
Loading

0 comments on commit 5038600

Please sign in to comment.