diff --git a/apps/alpha-liquidator/package.json b/apps/alpha-liquidator/package.json index f5fc9a3b89..5b4d1a40f3 100644 --- a/apps/alpha-liquidator/package.json +++ b/apps/alpha-liquidator/package.json @@ -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": "*", diff --git a/apps/alpha-liquidator/src/config.ts b/apps/alpha-liquidator/src/config.ts index fd122a27c1..ad986f8eab 100644 --- a/apps/alpha-liquidator/src/config.ts +++ b/apps/alpha-liquidator/src/config.ts @@ -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 @@ -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"]) @@ -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); diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 819e62bf61..ebe629d165 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -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; @@ -76,6 +78,7 @@ class Liquidator { private async mainLoop() { const debug = getDebugLogger("main-loop"); + drawSpinner("Scanning") try { await this.swapNonUsdcInTokenAccounts(); while (true) { @@ -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); @@ -107,6 +113,7 @@ class Liquidator { outputMint: mintOut, amount: JSBI.BigInt(amountIn.toString()), slippageBps: SLIPPAGE_BPS, + forceFetch: true, }); const route = routesInfos[0]; @@ -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 @@ -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; @@ -383,47 +390,49 @@ class Liquidator { return lendingAccountToRebalanceExists; } - private async liquidationStage() { + private async liquidationStage(): Promise { 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 { - const client = this.client; - const group = this.client; + private async processAccount(marginfiAccount: MarginfiAccountWrapper): Promise { + 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); @@ -434,7 +443,7 @@ class Liquidator { return false; } - captureMessage(`Liquidating account ${account.toBase58()}`); + captureMessage(`Liquidating account ${marginfiAccount.address.toBase58()}`); let maxLiabilityPaydownUsdValue = new BigNumber(0); let bestLiabAccountIndex = 0; @@ -442,8 +451,8 @@ class Liquidator { // 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)); @@ -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; } @@ -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)); @@ -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 @@ -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), @@ -575,7 +584,7 @@ class Liquidator { slippageAdjustedCollateralAmountToLiquidate, liabBank.address ); - debug("Liquidation tx: %s", sig); + console.log("Liquidation tx: %s", sig); return true; } @@ -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); +} diff --git a/apps/alpha-liquidator/src/runLiquidatorOnlyJup.ts b/apps/alpha-liquidator/src/runLiquidatorOnlyJup.ts new file mode 100644 index 0000000000..87c66337f4 --- /dev/null +++ b/apps/alpha-liquidator/src/runLiquidatorOnlyJup.ts @@ -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); +}); diff --git a/apps/marginfi-v2-ui/next.config.js b/apps/marginfi-v2-ui/next.config.js index 2f452f7d6f..f6c78fadf2 100644 --- a/apps/marginfi-v2-ui/next.config.js +++ b/apps/marginfi-v2-ui/next.config.js @@ -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` @@ -63,4 +67,4 @@ module.exports = { } ], }, -}; +}); diff --git a/apps/marginfi-v2-ui/package.json b/apps/marginfi-v2-ui/package.json index b65ca46cd9..f6ce58cb02 100644 --- a/apps/marginfi-v2-ui/package.json +++ b/apps/marginfi-v2-ui/package.json @@ -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", diff --git a/apps/marginfi-v2-ui/src/components/AssetsList/AssetsList.tsx b/apps/marginfi-v2-ui/src/components/AssetsList/AssetsList.tsx index 68f2558ab7..6c9e4467f6 100644 --- a/apps/marginfi-v2-ui/src/components/AssetsList/AssetsList.tsx +++ b/apps/marginfi-v2-ui/src/components/AssetsList/AssetsList.tsx @@ -1,7 +1,7 @@ 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"; @@ -9,8 +9,6 @@ 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) => ( diff --git a/apps/marginfi-v2-ui/src/components/CampaignWizard.tsx b/apps/marginfi-v2-ui/src/components/CampaignWizard.tsx index 79bd125664..d768adfbf6 100644 --- a/apps/marginfi-v2-ui/src/components/CampaignWizard.tsx +++ b/apps/marginfi-v2-ui/src/components/CampaignWizard.tsx @@ -13,7 +13,6 @@ 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"; @@ -21,6 +20,7 @@ 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; diff --git a/apps/marginfi-v2-ui/src/components/Earn/index.tsx b/apps/marginfi-v2-ui/src/components/Earn/index.tsx new file mode 100644 index 0000000000..5cfad096c4 --- /dev/null +++ b/apps/marginfi-v2-ui/src/components/Earn/index.tsx @@ -0,0 +1,666 @@ +import React, { FC, MouseEventHandler, ReactNode, useCallback, useEffect, useMemo, useState } from "react"; +import { useConnection, useWallet } from "@solana/wallet-adapter-react"; +import { PageHeader } from "~/components/PageHeader"; +import { useLipClient } from "~/context"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import CircularProgress from "@mui/material/CircularProgress"; +import InputAdornment from "@mui/material/InputAdornment"; +import LinearProgress from "@mui/material/LinearProgress"; +import TextField from "@mui/material/TextField"; +import { ButtonProps } from "@mui/material"; +import { NumberFormatValues, NumericFormat } from "react-number-format"; +import dynamic from "next/dynamic"; +import Radio from "@mui/material/Radio"; +import RadioGroup from "@mui/material/RadioGroup"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import FormControl from "@mui/material/FormControl"; +import Image from "next/image"; +import LipAccount, { Campaign, Deposit } from "@mrgnlabs/lip-client/src/account"; +import config from "~/config/marginfi"; +import { + BankMetadataMap, + floor, + groupedNumberFormatterDyn, + percentFormatterDyn, + shortenAddress, + usdFormatter, + usdFormatterDyn, +} from "@mrgnlabs/mrgn-common"; +import { Bank, PriceBias } from "@mrgnlabs/marginfi-client-v2"; +import { Countdown } from "~/components/Countdown"; +import { toast } from "react-toastify"; +import BigNumber from "bignumber.js"; +import { useWalletWithOverride } from "~/components/useWalletWithOverride"; +import { useMrgnlendStore } from "~/store"; + +const Earn = () => { + const walletContext = useWallet(); + const { wallet, isOverride } = useWalletWithOverride(); + const { connection } = useConnection(); + const { lipClient } = useLipClient(); + + const setIsRefreshingStore = useMrgnlendStore((state) => state.setIsRefreshingStore); + const [mfiClient, bankMetadataMap, fetchMrgnlendState] = useMrgnlendStore((state) => [ + state.marginfiClient, + state.bankMetadataMap, + state.fetchMrgnlendState, + ]); + + const [initialFetchDone, setInitialFetchDone] = useState(false); + const [reloading, setReloading] = useState(false); + const [selectedCampaign, setSelectedCampaign] = useState(null); + const [amount, setAmount] = useState(0); + const [progressPercent, setProgressPercent] = useState(0); + const [lipAccount, setLipAccount] = useState(null); + + useEffect(() => { + setIsRefreshingStore(true); + fetchMrgnlendState({ marginfiConfig: config.mfiConfig, connection, wallet, isOverride }).catch(console.error); + const id = setInterval(() => { + setIsRefreshingStore(true); + fetchMrgnlendState().catch(console.error); + }, 30_000); + return () => clearInterval(id); + }, [wallet, isOverride]); // eslint-disable-line react-hooks/exhaustive-deps + // ^ crucial to omit both `connection` and `fetchMrgnlendState` from the dependency array + // TODO: fix... + + const whitelistedCampaignsWithMeta = useMemo(() => { + if (!lipClient) return []; + const whitelistedCampaigns = + lipClient.campaigns.filter((c) => + config.campaignWhitelist.map((wc) => wc.publicKey).includes(c.publicKey.toBase58()) + ) || []; + return whitelistedCampaigns + .map((c) => { + const campaignFound = config.campaignWhitelist.find((wc) => wc.publicKey === c.publicKey.toBase58()); + if (!campaignFound) throw Error("Campaign not found"); + const { publicKey, ...meta } = campaignFound; + return { + campaign: c, + meta, + }; + }) + .sort((c1, c2) => { + if (c1.campaign.bank.mint.toBase58() < c2.campaign.bank.mint.toBase58()) return 1; + if (c1.campaign.bank.mint.toBase58() > c2.campaign.bank.mint.toBase58()) return -1; + return 0; + }); + //eslint-disable-next-line react-hooks/exhaustive-deps + }, [lipClient, lipAccount]); // the extra `lipAccount` dependency is on purpose + + const maxDepositAmount = useMemo( + () => (selectedCampaign ? selectedCampaign.campaign.remainingCapacity : 0), + [selectedCampaign] + ); + + const marks = [ + { value: 0, label: "CONNECT", color: progressPercent > 0 ? "#51B56A" : "#484848" }, + { value: 50, label: "SELECT", color: progressPercent >= 50 ? "#51B56A" : "#484848" }, + { value: 100, label: "READY", color: progressPercent >= 100 ? "#51B56A" : "#484848" }, + ]; + + useEffect(() => { + if (!selectedCampaign) return; + const campaign = whitelistedCampaignsWithMeta.find( + (c) => c.campaign.publicKey.toBase58() === selectedCampaign.campaign.publicKey.toBase58() + ); + if (!campaign) throw new Error("Campaign not found"); + setSelectedCampaign(campaign); + }, [selectedCampaign, whitelistedCampaignsWithMeta]); + + useEffect(() => { + if ( + whitelistedCampaignsWithMeta === null || + whitelistedCampaignsWithMeta.length === 0 || + selectedCampaign !== null + ) { + return; + } + setSelectedCampaign(whitelistedCampaignsWithMeta[0]); + }, [selectedCampaign, whitelistedCampaignsWithMeta]); + + useEffect(() => { + setAmount(0); + }, [selectedCampaign]); + + useEffect(() => { + (async function () { + setInitialFetchDone(true); + if (!mfiClient || !lipClient || !walletContext.publicKey) return; + const lipAccount = await LipAccount.fetch(walletContext.publicKey, lipClient, mfiClient); + setLipAccount(lipAccount); + })(); + }, [lipClient, mfiClient, walletContext.publicKey]); + + useEffect(() => { + if (walletContext.connected) { + setProgressPercent(50); + } else { + setProgressPercent(0); + } + }, [walletContext.connected]); + + useEffect(() => { + if (amount > 0) { + setProgressPercent(100); + } else { + if (walletContext.connected) { + setProgressPercent(50); + } else { + setProgressPercent(0); + } + } + }, [amount, walletContext.connected]); + + const depositAction = useCallback(async () => { + if (!lipAccount || !lipClient || !selectedCampaign || amount === 0 || whitelistedCampaignsWithMeta.length === 0) + return; + + setReloading(true); + try { + await lipClient.deposit( + selectedCampaign.campaign.publicKey, + floor(amount, selectedCampaign.campaign.bank.mintDecimals), + selectedCampaign.campaign.bank + ); + setLipAccount(await lipAccount.reloadAndClone()); + setAmount(0); + } catch (e) { + console.error(e); + } + setReloading(false); + }, [amount, lipAccount, lipClient, selectedCampaign, whitelistedCampaignsWithMeta]); + + const loadingSafetyCheck = useCallback(() => { + if (!mfiClient || !lipAccount || !lipClient) { + setInitialFetchDone(false); + } + }, [lipAccount, lipClient, mfiClient, setInitialFetchDone]); + + const closeDeposit = useCallback( + async (deposit: Deposit) => { + if (!lipAccount) return; + toast.loading(`Closing deposit`, { + toastId: "close-deposit", + }); + try { + await lipAccount.closePosition(deposit); + toast.update("close-deposit", { + render: `Closing deposit 👍`, + type: toast.TYPE.SUCCESS, + autoClose: 2000, + isLoading: false, + }); + } catch (e) { + console.error(e); + toast.update("close-deposit", { + render: `Error closing deposit: ${e}`, + type: toast.TYPE.ERROR, + autoClose: 2000, + isLoading: false, + }); + } + setReloading(true); + setLipAccount(await lipAccount.reloadAndClone()); + setReloading(false); + }, + [lipAccount] + ); + + return ( + <> + +
+
+
+
+ {walletContext.connected && ( +
+ Your total deposits: + + { + // Since users will only be able to deposit to the LIP, + // the balance of their account should match total deposits. + } + {usdFormatter.format(lipAccount?.getTotalBalance().toNumber() || 0)} + +
+ )} +
+
+ +
+ +
+
+
+ +
+
+ FUNDS WILL BE LOCKED FOR: +
+
+ ⚠️6 MONTHS⚠️ +
+
+ FROM DEPOSIT DATE +
+
+ +
+ +
+ +
+ +
+ +
+ { + // You can only deposit right now. + // All funds will be locked up for 6 months, each from the date of its *own* deposit. + } + + Deposit + +
+
+
+ {lipAccount && ( + <> +
+ Your deposits +
+
+ {lipAccount.deposits.map((deposit, index) => ( + + ))} +
+ + )} + + ); +}; + +interface DepositTileProps { + deposit: Deposit; + closeDepositCb: (position: Deposit) => void; + bankMetadataMap: BankMetadataMap; +} + +const DepositTile: FC = ({ deposit, closeDepositCb, bankMetadataMap }) => { + const [isEnded, setIsEnded] = useState(false); + + useEffect(() => { + const interval = setInterval(() => { + const secondsRemaining = (deposit.endDate.getTime() - new Date().getTime()) / 1000; + if (secondsRemaining <= 0) { + setIsEnded(true); + } + }, 1000); + return () => clearInterval(interval); + }, [deposit.endDate]); + + return ( +
+ +
+ {!isEnded ? ( + + ) : ( +
READY
+ )} +
+
+ Start date: + {deposit.startDate.toLocaleString()} +
+
+ End date: + {deposit.endDate.toLocaleString()} +
+ +
+ Lock-up: + {Math.floor(deposit.lockupPeriodInDays)} days +
+
+ Minimum APY: + {percentFormatterDyn.format(deposit.campaign.guaranteedApy)} +
+
+ Asset: + {getTokenSymbol(deposit.campaign.bank, bankMetadataMap)} +
+
+
+
+
+ Amount locked: + {groupedNumberFormatterDyn.format(deposit.amount)} {getTokenSymbol(deposit.campaign.bank, bankMetadataMap)} ( + {usdFormatterDyn.format(deposit.computeUsdValue(deposit.campaign.oraclePrice, deposit.campaign.bank))}) +
+
+ Minimum payout: + {groupedNumberFormatterDyn.format(deposit.maturityAmount)}{" "} + {getTokenSymbol(deposit.campaign.bank, bankMetadataMap)} ( + {usdFormatterDyn.format( + deposit.campaign.bank + .computeUsdValue( + deposit.campaign.oraclePrice, + new BigNumber(deposit.maturityAmount), + PriceBias.None, + undefined, + false + ) + .toNumber() + )} + ) +
+ +
+
+ ); +}; + +const Marks: FC<{ marks: { value: any; color: string; label: string }[] }> = ({ marks }) => ( + <> + {marks.map((mark, index) => ( +
+
+
+ {mark.label} +
+
+
+ ))} + +); + +// ================================ +// ASSET SELECTION +// ================================ + +interface WhitelistedCampaignWithMeta { + campaign: Campaign; + meta: { + icon: string; + size: number; + }; +} + +interface AssetSelectionProps { + setSelectedCampaign: (campaign: WhitelistedCampaignWithMeta) => void; + whitelistedCampaigns: WhitelistedCampaignWithMeta[]; + bankMetadataMap?: BankMetadataMap; +} + +const AssetSelection: FC = ({ whitelistedCampaigns, setSelectedCampaign, bankMetadataMap }) => { + if (whitelistedCampaigns.length === 0) return null; + const defaultCampaign = whitelistedCampaigns[0]; + + return ( + + { + const campaign = whitelistedCampaigns.find((b) => b.campaign.publicKey.toBase58() === event.target.value); + if (!campaign) throw new Error("Campaign not found"); + setSelectedCampaign(campaign); + }} + > + {whitelistedCampaigns.map(({ campaign, meta }) => { + return ( + + } + label={ +
+
{getTokenSymbol(campaign.bank, bankMetadataMap || {})}
+
+
+ Min. APY: {percentFormatterDyn.format(campaign.computeGuaranteedApyForCampaign())} +
+
+ {campaign.bank.mint.toBase58()} +
+
+
+ } + className="w-full bg-[#000] ml-0 mr-0 rounded-[100px] p-1 h-12" + style={{ border: "solid #1C2125 1px" }} + /> + ); + })} +
+
+ ); +}; + +// ================================ +// INPUT BOX +// ================================ + +interface EarnInputBox { + value: number; + setValue: (value: number) => void; + loadingSafetyCheck: () => void; + maxValue?: number; + maxDecimals?: number; + disabled?: boolean; +} + +export const EarnInputBox: FC = ({ + value, + setValue, + loadingSafetyCheck, + maxValue, + maxDecimals, + disabled, +}) => { + const onChange = useCallback( + (event: NumberFormatValues) => { + const updatedAmountStr = event.value; + if (updatedAmountStr !== "" && !/^\d*\.?\d*$/.test(updatedAmountStr)) return; + + const updatedAmount = Number(updatedAmountStr); + if (maxValue !== undefined && updatedAmount > maxValue) { + loadingSafetyCheck(); + setValue(maxValue); + return; + } + + loadingSafetyCheck(); + setValue(updatedAmount); + }, + [maxValue, setValue, loadingSafetyCheck] + ); + + return ( + // TODO: re-rendering after initial amount capping is messed up and lets anything you type through + { + if (maxValue !== undefined) { + setValue(maxValue); + } + }} + /> + ), + }} + /> + ); +}; + +export const MaxInputAdornment: FC<{ + onClick: MouseEventHandler; + disabled?: boolean; +}> = ({ onClick, disabled }) => ( + +
+ max +
+
+); + +// ================================ +// ACTION BUTTON +// ================================ + +const WalletMultiButtonDynamic = dynamic( + async () => (await import("@solana/wallet-adapter-react-ui")).WalletMultiButton, + { ssr: false } +); + +interface EarnActionProps extends ButtonProps { + children: ReactNode; + spinning?: boolean; +} + +export const EarnAction: FC = ({ children, spinning, disabled, ...otherProps }) => { + const walletContext = useWallet(); + + return walletContext.connected ? ( + + ) : ( + + Connect + + ); +}; + +function getTokenSymbol(bank: Bank, bankMetadataMap: BankMetadataMap): string { + const bankMetadata = bankMetadataMap[bank.address.toBase58()]; + if (!bankMetadata) { + console.log("Bank metadata not found for %s", bank.address.toBase58()); + return shortenAddress(bank.mint); + } + + return bankMetadata.tokenSymbol; +} + +export { Earn }; diff --git a/apps/marginfi-v2-ui/src/components/UserPositions/UserPositions.tsx b/apps/marginfi-v2-ui/src/components/UserPositions/UserPositions.tsx index 65dea2b365..9a7e189556 100644 --- a/apps/marginfi-v2-ui/src/components/UserPositions/UserPositions.tsx +++ b/apps/marginfi-v2-ui/src/components/UserPositions/UserPositions.tsx @@ -5,7 +5,6 @@ import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip"; import { styled } from "@mui/material/styles"; import Image from "next/image"; import Link from "next/link"; -import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react"; import { useMrgnlendStore } from "~/store"; import { ActiveBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; diff --git a/apps/marginfi-v2-ui/src/config/index.ts b/apps/marginfi-v2-ui/src/config/index.ts new file mode 100644 index 0000000000..b47bd8c065 --- /dev/null +++ b/apps/marginfi-v2-ui/src/config/index.ts @@ -0,0 +1,26 @@ +const environment = process.env.NEXT_PUBLIC_MARGINFI_ENVIRONMENT; +const rpcEndpointOverride = process.env.NEXT_PUBLIC_MARGINFI_RPC_ENDPOINT_OVERRIDE; + +let rpcEndpoint; +switch (environment) { + case "production": + rpcEndpoint = rpcEndpointOverride || "https://mrgn.rpcpool.com/"; + break; + case "alpha": + rpcEndpoint = rpcEndpointOverride || "https://mrgn.rpcpool.com/"; + break; + case "staging": + rpcEndpoint = rpcEndpointOverride || "https://mrgn.rpcpool.com/"; + break; + case "dev": + rpcEndpoint = rpcEndpointOverride || "https://devnet.rpcpool.com/"; + break; + default: + rpcEndpoint = rpcEndpointOverride || "https://devnet.rpcpool.com/"; +} + +const config = { + rpcEndpoint, +}; + +export default config; diff --git a/apps/marginfi-v2-ui/src/config.ts b/apps/marginfi-v2-ui/src/config/marginfi.ts similarity index 82% rename from apps/marginfi-v2-ui/src/config.ts rename to apps/marginfi-v2-ui/src/config/marginfi.ts index c4d432f12f..ba46b954f7 100644 --- a/apps/marginfi-v2-ui/src/config.ts +++ b/apps/marginfi-v2-ui/src/config/marginfi.ts @@ -6,11 +6,10 @@ import { getConfig as getLipConfig } from "@mrgnlabs/lip-client"; // MAIN APP CONFIG // ================ -let mfiConfig, rpcEndpoint, devFaucetAddress, lipConfig; +let mfiConfig, devFaucetAddress, lipConfig; let campaignWhitelist: { icon: string; size: number; publicKey: string }[]; const environment = process.env.NEXT_PUBLIC_MARGINFI_ENVIRONMENT; -const rpcEndpointOverride = process.env.NEXT_PUBLIC_MARGINFI_RPC_ENDPOINT_OVERRIDE; const groupOverride = process.env.NEXT_PUBLIC_MARGINFI_GROUP_OVERRIDE; switch (environment) { @@ -20,7 +19,6 @@ switch (environment) { if (groupOverride) { mfiConfig.groupPk = new PublicKey(groupOverride); } - rpcEndpoint = rpcEndpointOverride || "https://mrgn.rpcpool.com/"; campaignWhitelist = [ { icon: "https://s2.coinmarketcap.com/static/img/coins/64x64/23095.png", @@ -35,7 +33,6 @@ switch (environment) { if (groupOverride) { mfiConfig.groupPk = new PublicKey(groupOverride); } - rpcEndpoint = rpcEndpointOverride || "https://mrgn.rpcpool.com/"; campaignWhitelist = []; break; case "staging": @@ -44,7 +41,6 @@ switch (environment) { if (groupOverride) { mfiConfig.groupPk = new PublicKey(groupOverride); } - rpcEndpoint = rpcEndpointOverride || "https://mrgn.rpcpool.com/"; campaignWhitelist = []; break; case "dev": @@ -53,14 +49,12 @@ switch (environment) { if (groupOverride) { mfiConfig.groupPk = new PublicKey(groupOverride); } - rpcEndpoint = rpcEndpointOverride || "https://devnet.rpcpool.com/"; devFaucetAddress = new PublicKey("B87AhxX6BkBsj3hnyHzcerX2WxPoACC7ZyDr8E7H9geN"); campaignWhitelist = []; break; default: mfiConfig = getConfig("dev"); lipConfig = getLipConfig("dev"); - rpcEndpoint = rpcEndpointOverride || "https://devnet.rpcpool.com/"; devFaucetAddress = new PublicKey("57hG7dDLXUg6GYDzAw892V4qLm6FhKxd86vMLazyFL98"); campaignWhitelist = [ { @@ -78,7 +72,6 @@ switch (environment) { const config = { mfiConfig, - rpcEndpoint, devFaucetAddress, lipConfig, campaignWhitelist, diff --git a/apps/marginfi-v2-ui/src/context/LipClient.tsx b/apps/marginfi-v2-ui/src/context/LipClient.tsx index 3b2bafba09..b7c8dc84e1 100644 --- a/apps/marginfi-v2-ui/src/context/LipClient.tsx +++ b/apps/marginfi-v2-ui/src/context/LipClient.tsx @@ -1,5 +1,5 @@ import React, { createContext, FC, useCallback, useContext, useEffect, useState } from "react"; -import config from "~/config"; +import config from "~/config/marginfi"; import { LipClient } from "@mrgnlabs/lip-client"; import { useMrgnlendStore } from "~/store"; diff --git a/apps/marginfi-v2-ui/src/pages/_app.tsx b/apps/marginfi-v2-ui/src/pages/_app.tsx index a8644bab2e..cc97a8f461 100644 --- a/apps/marginfi-v2-ui/src/pages/_app.tsx +++ b/apps/marginfi-v2-ui/src/pages/_app.tsx @@ -13,13 +13,11 @@ import { import { OKXWalletAdapter } from "~/utils"; import { init, push } from "@socialgouv/matomo-next"; import config from "~/config"; -import { Navbar, Footer } from "~/components"; import "react-toastify/dist/ReactToastify.min.css"; import { ToastContainer } from "react-toastify"; import { Analytics } from "@vercel/analytics/react"; -import { OverlaySpinner } from "~/components/OverlaySpinner"; -import { useMrgnlendStore } from "~/store"; +import dynamic from "next/dynamic"; // Use require instead of import since order matters require("@solana/wallet-adapter-react-ui/styles.css"); @@ -27,6 +25,9 @@ require("~/styles/globals.css"); require("~/styles/fonts.css"); require("~/styles/asset-borders.css"); +const Navbar = dynamic(async () => (await import("~/components/Navbar")).Navbar, { ssr: false }); +const Footer = dynamic(async () => (await import("~/components/Footer")).Footer, { ssr: false }); + // Matomo const MATOMO_URL = "https://mrgn.matomo.cloud"; @@ -52,11 +53,6 @@ const MyApp = ({ Component, pageProps }: AppProps) => { [] ); - const [isStoreInitialized, isRefreshingStore] = useMrgnlendStore((state) => [ - state.initialized, - state.isRefreshingStore, - ]); - return ( @@ -76,7 +72,6 @@ const MyApp = ({ Component, pageProps }: AppProps) => { - ); }; diff --git a/apps/marginfi-v2-ui/src/pages/earn.tsx b/apps/marginfi-v2-ui/src/pages/earn.tsx index 5b76e2c0aa..a5ed12e3fb 100644 --- a/apps/marginfi-v2-ui/src/pages/earn.tsx +++ b/apps/marginfi-v2-ui/src/pages/earn.tsx @@ -1,32 +1,8 @@ -import React, { FC, MouseEventHandler, ReactNode, useCallback, useEffect, useMemo, useState } from "react"; -import { useConnection, useWallet } from "@solana/wallet-adapter-react"; -import { PageHeader } from "~/components/PageHeader"; -import { LipClientProvider, useLipClient } from "~/context"; -import { Button, ButtonProps, Card, CircularProgress, InputAdornment, LinearProgress, TextField } from "@mui/material"; -import { NumberFormatValues, NumericFormat } from "react-number-format"; import dynamic from "next/dynamic"; -import Radio from "@mui/material/Radio"; -import RadioGroup from "@mui/material/RadioGroup"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import FormControl from "@mui/material/FormControl"; -import Image from "next/image"; -import LipAccount, { Campaign, Deposit } from "@mrgnlabs/lip-client/src/account"; -import config from "~/config"; -import { - BankMetadataMap, - floor, - groupedNumberFormatterDyn, - percentFormatterDyn, - shortenAddress, - usdFormatter, - usdFormatterDyn, -} from "@mrgnlabs/mrgn-common"; -import { Bank, PriceBias } from "@mrgnlabs/marginfi-client-v2"; -import { Countdown } from "~/components/Countdown"; -import { toast } from "react-toastify"; -import BigNumber from "bignumber.js"; -import { useWalletWithOverride } from "~/components/useWalletWithOverride"; -import { useMrgnlendStore } from "../store"; +import React from "react"; +import { LipClientProvider } from "~/context"; + +const Earn = dynamic(async () => (await import("~/components/Earn")).Earn, { ssr: false }); const EarnPage = () => { return ( @@ -36,633 +12,4 @@ const EarnPage = () => { ); }; -const Earn = () => { - const walletContext = useWallet(); - const { wallet, isOverride } = useWalletWithOverride(); - const { connection } = useConnection(); - const { lipClient } = useLipClient(); - - const setIsRefreshingStore = useMrgnlendStore((state) => state.setIsRefreshingStore); - const [mfiClient, bankMetadataMap, fetchMrgnlendState] = useMrgnlendStore((state) => [ - state.marginfiClient, - state.bankMetadataMap, - state.fetchMrgnlendState, - ]); - - const [initialFetchDone, setInitialFetchDone] = useState(false); - const [reloading, setReloading] = useState(false); - const [selectedCampaign, setSelectedCampaign] = useState(null); - const [amount, setAmount] = useState(0); - const [progressPercent, setProgressPercent] = useState(0); - const [lipAccount, setLipAccount] = useState(null); - - useEffect(() => { - setIsRefreshingStore(true); - fetchMrgnlendState({ marginfiConfig: config.mfiConfig, connection, wallet, isOverride }).catch(console.error); - const id = setInterval(() => { - setIsRefreshingStore(true); - fetchMrgnlendState().catch(console.error); - }, 30_000); - return () => clearInterval(id); - }, [wallet, isOverride]); // eslint-disable-line react-hooks/exhaustive-deps - // ^ crucial to omit both `connection` and `fetchMrgnlendState` from the dependency array - // TODO: fix... - - const whitelistedCampaignsWithMeta = useMemo(() => { - if (!lipClient) return []; - const whitelistedCampaigns = - lipClient.campaigns.filter((c) => - config.campaignWhitelist.map((wc) => wc.publicKey).includes(c.publicKey.toBase58()) - ) || []; - return whitelistedCampaigns - .map((c) => { - const campaignFound = config.campaignWhitelist.find((wc) => wc.publicKey === c.publicKey.toBase58()); - if (!campaignFound) throw Error("Campaign not found"); - const { publicKey, ...meta } = campaignFound; - return { - campaign: c, - meta, - }; - }) - .sort((c1, c2) => { - if (c1.campaign.bank.mint.toBase58() < c2.campaign.bank.mint.toBase58()) return 1; - if (c1.campaign.bank.mint.toBase58() > c2.campaign.bank.mint.toBase58()) return -1; - return 0; - }); - //eslint-disable-next-line react-hooks/exhaustive-deps - }, [lipClient, lipAccount]); // the extra `lipAccount` dependency is on purpose - - const maxDepositAmount = useMemo( - () => (selectedCampaign ? selectedCampaign.campaign.remainingCapacity : 0), - [selectedCampaign] - ); - - const marks = [ - { value: 0, label: "CONNECT", color: progressPercent > 0 ? "#51B56A" : "#484848" }, - { value: 50, label: "SELECT", color: progressPercent >= 50 ? "#51B56A" : "#484848" }, - { value: 100, label: "READY", color: progressPercent >= 100 ? "#51B56A" : "#484848" }, - ]; - - useEffect(() => { - if (!selectedCampaign) return; - const campaign = whitelistedCampaignsWithMeta.find( - (c) => c.campaign.publicKey.toBase58() === selectedCampaign.campaign.publicKey.toBase58() - ); - if (!campaign) throw new Error("Campaign not found"); - setSelectedCampaign(campaign); - }, [selectedCampaign, whitelistedCampaignsWithMeta]); - - useEffect(() => { - if ( - whitelistedCampaignsWithMeta === null || - whitelistedCampaignsWithMeta.length === 0 || - selectedCampaign !== null - ) { - return; - } - setSelectedCampaign(whitelistedCampaignsWithMeta[0]); - }, [selectedCampaign, whitelistedCampaignsWithMeta]); - - useEffect(() => { - setAmount(0); - }, [selectedCampaign]); - - useEffect(() => { - (async function () { - setInitialFetchDone(true); - if (!mfiClient || !lipClient || !walletContext.publicKey) return; - const lipAccount = await LipAccount.fetch(walletContext.publicKey, lipClient, mfiClient); - setLipAccount(lipAccount); - })(); - }, [lipClient, mfiClient, walletContext.publicKey]); - - useEffect(() => { - if (walletContext.connected) { - setProgressPercent(50); - } else { - setProgressPercent(0); - } - }, [walletContext.connected]); - - useEffect(() => { - if (amount > 0) { - setProgressPercent(100); - } else { - if (walletContext.connected) { - setProgressPercent(50); - } else { - setProgressPercent(0); - } - } - }, [amount, walletContext.connected]); - - const depositAction = useCallback(async () => { - if (!lipAccount || !lipClient || !selectedCampaign || amount === 0 || whitelistedCampaignsWithMeta.length === 0) - return; - - setReloading(true); - try { - await lipClient.deposit( - selectedCampaign.campaign.publicKey, - floor(amount, selectedCampaign.campaign.bank.mintDecimals), - selectedCampaign.campaign.bank - ); - setLipAccount(await lipAccount.reloadAndClone()); - setAmount(0); - } catch (e) { - console.error(e); - } - setReloading(false); - }, [amount, lipAccount, lipClient, selectedCampaign, whitelistedCampaignsWithMeta]); - - const loadingSafetyCheck = useCallback(() => { - if (!mfiClient || !lipAccount || !lipClient) { - setInitialFetchDone(false); - } - }, [lipAccount, lipClient, mfiClient, setInitialFetchDone]); - - const closeDeposit = useCallback( - async (deposit: Deposit) => { - if (!lipAccount) return; - toast.loading(`Closing deposit`, { - toastId: "close-deposit", - }); - try { - await lipAccount.closePosition(deposit); - toast.update("close-deposit", { - render: `Closing deposit 👍`, - type: toast.TYPE.SUCCESS, - autoClose: 2000, - isLoading: false, - }); - } catch (e) { - console.error(e); - toast.update("close-deposit", { - render: `Error closing deposit: ${e}`, - type: toast.TYPE.ERROR, - autoClose: 2000, - isLoading: false, - }); - } - setReloading(true); - setLipAccount(await lipAccount.reloadAndClone()); - setReloading(false); - }, - [lipAccount] - ); - - return ( - <> - -
-
-
-
- {walletContext.connected && ( -
- Your total deposits: - - { - // Since users will only be able to deposit to the LIP, - // the balance of their account should match total deposits. - } - {usdFormatter.format(lipAccount?.getTotalBalance().toNumber() || 0)} - -
- )} -
-
- -
- -
-
-
- -
-
- FUNDS WILL BE LOCKED FOR: -
-
- ⚠️6 MONTHS⚠️ -
-
- FROM DEPOSIT DATE -
-
- -
- -
- -
- -
- -
- { - // You can only deposit right now. - // All funds will be locked up for 6 months, each from the date of its *own* deposit. - } - - Deposit - -
-
-
- {lipAccount && ( - <> -
- Your deposits -
-
- {lipAccount.deposits.map((deposit, index) => ( - - ))} -
- - )} - - ); -}; - -interface DepositTileProps { - deposit: Deposit; - closeDepositCb: (position: Deposit) => void; - bankMetadataMap: BankMetadataMap; -} - -const DepositTile: FC = ({ deposit, closeDepositCb, bankMetadataMap }) => { - const [isEnded, setIsEnded] = useState(false); - - useEffect(() => { - const interval = setInterval(() => { - const secondsRemaining = (deposit.endDate.getTime() - new Date().getTime()) / 1000; - if (secondsRemaining <= 0) { - setIsEnded(true); - } - }, 1000); - return () => clearInterval(interval); - }, [deposit.endDate]); - - return ( -
- -
- {!isEnded ? ( - - ) : ( -
READY
- )} -
-
- Start date: - {deposit.startDate.toLocaleString()} -
-
- End date: - {deposit.endDate.toLocaleString()} -
- -
- Lock-up: - {Math.floor(deposit.lockupPeriodInDays)} days -
-
- Minimum APY: - {percentFormatterDyn.format(deposit.campaign.guaranteedApy)} -
-
- Asset: - {getTokenSymbol(deposit.campaign.bank, bankMetadataMap)} -
-
-
-
-
- Amount locked: - {groupedNumberFormatterDyn.format(deposit.amount)} {getTokenSymbol(deposit.campaign.bank, bankMetadataMap)} ( - {usdFormatterDyn.format(deposit.computeUsdValue(deposit.campaign.oraclePrice, deposit.campaign.bank))}) -
-
- Minimum payout: - {groupedNumberFormatterDyn.format(deposit.maturityAmount)}{" "} - {getTokenSymbol(deposit.campaign.bank, bankMetadataMap)} ( - {usdFormatterDyn.format( - deposit.campaign.bank - .computeUsdValue( - deposit.campaign.oraclePrice, - new BigNumber(deposit.maturityAmount), - PriceBias.None, - undefined, - false - ) - .toNumber() - )} - ) -
- -
-
- ); -}; - -const Marks: FC<{ marks: { value: any; color: string; label: string }[] }> = ({ marks }) => ( - <> - {marks.map((mark, index) => ( -
-
-
- {mark.label} -
-
-
- ))} - -); - -// ================================ -// ASSET SELECTION -// ================================ - -interface WhitelistedCampaignWithMeta { - campaign: Campaign; - meta: { - icon: string; - size: number; - }; -} - -interface AssetSelectionProps { - setSelectedCampaign: (campaign: WhitelistedCampaignWithMeta) => void; - whitelistedCampaigns: WhitelistedCampaignWithMeta[]; - bankMetadataMap?: BankMetadataMap; -} - -const AssetSelection: FC = ({ whitelistedCampaigns, setSelectedCampaign, bankMetadataMap }) => { - if (whitelistedCampaigns.length === 0) return null; - const defaultCampaign = whitelistedCampaigns[0]; - - return ( - - { - const campaign = whitelistedCampaigns.find((b) => b.campaign.publicKey.toBase58() === event.target.value); - if (!campaign) throw new Error("Campaign not found"); - setSelectedCampaign(campaign); - }} - > - {whitelistedCampaigns.map(({ campaign, meta }) => { - return ( - - } - label={ -
-
{getTokenSymbol(campaign.bank, bankMetadataMap || {})}
-
-
- Min. APY: {percentFormatterDyn.format(campaign.computeGuaranteedApyForCampaign())} -
-
- {campaign.bank.mint.toBase58()} -
-
-
- } - className="w-full bg-[#000] ml-0 mr-0 rounded-[100px] p-1 h-12" - style={{ border: "solid #1C2125 1px" }} - /> - ); - })} -
-
- ); -}; - -// ================================ -// INPUT BOX -// ================================ - -interface EarnInputBox { - value: number; - setValue: (value: number) => void; - loadingSafetyCheck: () => void; - maxValue?: number; - maxDecimals?: number; - disabled?: boolean; -} - -export const EarnInputBox: FC = ({ - value, - setValue, - loadingSafetyCheck, - maxValue, - maxDecimals, - disabled, -}) => { - const onChange = useCallback( - (event: NumberFormatValues) => { - const updatedAmountStr = event.value; - if (updatedAmountStr !== "" && !/^\d*\.?\d*$/.test(updatedAmountStr)) return; - - const updatedAmount = Number(updatedAmountStr); - if (maxValue !== undefined && updatedAmount > maxValue) { - loadingSafetyCheck(); - setValue(maxValue); - return; - } - - loadingSafetyCheck(); - setValue(updatedAmount); - }, - [maxValue, setValue, loadingSafetyCheck] - ); - - return ( - // TODO: re-rendering after initial amount capping is messed up and lets anything you type through - { - if (maxValue !== undefined) { - setValue(maxValue); - } - }} - /> - ), - }} - /> - ); -}; - -export const MaxInputAdornment: FC<{ - onClick: MouseEventHandler; - disabled?: boolean; -}> = ({ onClick, disabled }) => ( - -
- max -
-
-); - -// ================================ -// ACTION BUTTON -// ================================ - -const WalletMultiButtonDynamic = dynamic( - async () => (await import("@solana/wallet-adapter-react-ui")).WalletMultiButton, - { ssr: false } -); - -interface EarnActionProps extends ButtonProps { - children: ReactNode; - spinning?: boolean; -} - -export const EarnAction: FC = ({ children, spinning, disabled, ...otherProps }) => { - const walletContext = useWallet(); - - return walletContext.connected ? ( - - ) : ( - - Connect - - ); -}; - -function getTokenSymbol(bank: Bank, bankMetadataMap: BankMetadataMap): string { - const bankMetadata = bankMetadataMap[bank.address.toBase58()]; - if (!bankMetadata) { - console.log("Bank metadata not found for %s", bank.address.toBase58()); - return shortenAddress(bank.mint); - } - - return bankMetadata.tokenSymbol; -} - export default EarnPage; diff --git a/apps/marginfi-v2-ui/src/pages/index.tsx b/apps/marginfi-v2-ui/src/pages/index.tsx index 7c735cc16e..7492ddf375 100644 --- a/apps/marginfi-v2-ui/src/pages/index.tsx +++ b/apps/marginfi-v2-ui/src/pages/index.tsx @@ -1,11 +1,19 @@ import React, { useEffect } from "react"; import { useConnection, useWallet } from "@solana/wallet-adapter-react"; -import { AccountSummary, AssetsList, Banner, UserPositions } from "~/components"; +import { Banner } from "~/components"; import { PageHeader } from "~/components/PageHeader"; import { useWalletWithOverride } from "~/components/useWalletWithOverride"; import { shortenAddress } from "@mrgnlabs/mrgn-common"; -import config from "~/config"; +import config from "~/config/marginfi"; import { useMrgnlendStore } from "../store"; +import dynamic from "next/dynamic"; +import { OverlaySpinner } from "~/components/OverlaySpinner"; + +const AccountSummary = dynamic(async () => (await import("~/components/AccountSummary")).AccountSummary, { + ssr: false, +}); +const AssetsList = dynamic(async () => (await import("~/components/AssetsList")).AssetsList, { ssr: false }); +const UserPositions = dynamic(async () => (await import("~/components/UserPositions")).UserPositions, { ssr: false }); const Home = () => { const walletContext = useWallet(); @@ -16,6 +24,11 @@ const Home = () => { const marginfiAccountCount = useMrgnlendStore((state) => state.marginfiAccountCount); const selectedAccount = useMrgnlendStore((state) => state.selectedAccount); + const [isStoreInitialized, isRefreshingStore] = useMrgnlendStore((state) => [ + state.initialized, + state.isRefreshingStore, + ]); + useEffect(() => { setIsRefreshingStore(true); fetchMrgnlendState({ marginfiConfig: config.mfiConfig, connection, wallet, isOverride }).catch(console.error); @@ -56,6 +69,7 @@ const Home = () => { {walletContext.connected && } + ); }; diff --git a/apps/marginfi-v2-xnft/package.json b/apps/marginfi-v2-xnft/package.json index 92114094a0..c1ae88b2a4 100644 --- a/apps/marginfi-v2-xnft/package.json +++ b/apps/marginfi-v2-xnft/package.json @@ -19,7 +19,6 @@ "@mrgnlabs/marginfi-client-v2": "*", "@mrgnlabs/marginfi-v2-ui-state": "*", "@mrgnlabs/mrgn-common": "*", - "@pythnetwork/client": "^2.19.0", "@react-navigation/bottom-tabs": "6.5.8", "@react-navigation/native": "6.1.7", "@react-navigation/native-stack": "6.9.13", diff --git a/apps/omni/src/pages/index.tsx b/apps/omni/src/pages/index.tsx index c7c97ecc83..b6eed7a1d0 100644 --- a/apps/omni/src/pages/index.tsx +++ b/apps/omni/src/pages/index.tsx @@ -1,11 +1,11 @@ import React, { FC, useState, useCallback, useMemo } from "react"; import Image from "next/image"; import axios from "axios"; -import { TextField } from "@mui/material"; +import TextField from "@mui/material/TextField"; +import InputAdornment from "@mui/material/InputAdornment"; import { useWallet } from "@solana/wallet-adapter-react"; import { TypeAnimation } from "react-type-animation"; -import { InputAdornment } from "@mui/material"; import { FormEventHandler } from "react"; import { useMrgnlendStore } from "~/store"; diff --git a/packages/lip-client/package.json b/packages/lip-client/package.json index b8c459f8d2..95c9b06f8b 100644 --- a/packages/lip-client/package.json +++ b/packages/lip-client/package.json @@ -16,7 +16,6 @@ "@mrgnlabs/marginfi-client-v2": "*", "@mrgnlabs/mrgn-common": "*", "@project-serum/anchor": "^0.26.0", - "@pythnetwork/client": "^2.9.0", "@solana/wallet-adapter-base": "^0.9.20", "@solana/web3.js": "^1.71.0", "bignumber.js": "^9.1.1", diff --git a/packages/marginfi-client-v2/package.json b/packages/marginfi-client-v2/package.json index 2921b5bc28..51aa9a8b3e 100644 --- a/packages/marginfi-client-v2/package.json +++ b/packages/marginfi-client-v2/package.json @@ -18,7 +18,6 @@ "@coral-xyz/anchor": "^0.26.0", "@mrgnlabs/mrgn-common": "*", "@project-serum/anchor": "^0.26.0", - "@pythnetwork/client": "^2.9.0", "@solana/wallet-adapter-base": "^0.9.20", "@solana/web3.js": "^1.71.0", "bignumber.js": "^9.1.1", diff --git a/packages/marginfi-client-v2/src/client.ts b/packages/marginfi-client-v2/src/client.ts index 9b0561e1e3..dc0dcc8843 100644 --- a/packages/marginfi-client-v2/src/client.ts +++ b/packages/marginfi-client-v2/src/client.ts @@ -213,6 +213,24 @@ class MarginfiClient { return this.program.programId; } + /** + * Retrieves the addresses of all marginfi accounts in the underlying group. + * + * @returns Account addresses + */ + async getAllMarginfiAccounts(): Promise { + return ( + await this.program.account.marginfiAccount.all([ + { + memcmp: { + bytes: this.config.groupPk.toBase58(), + offset: 8, // marginfiGroup is the first field in the account, so only offset is the discriminant + }, + }, + ]) + ).map((a) => MarginfiAccountWrapper.fromAccountParsed(a.publicKey, this, a.account as MarginfiAccountRaw)); + } + /** * Retrieves the addresses of all marginfi accounts in the underlying group. * diff --git a/packages/marginfi-client-v2/src/models/account/pure.ts b/packages/marginfi-client-v2/src/models/account/pure.ts index 2461964f8c..7d38a0079d 100644 --- a/packages/marginfi-client-v2/src/models/account/pure.ts +++ b/packages/marginfi-client-v2/src/models/account/pure.ts @@ -659,7 +659,7 @@ class MarginfiAccount { isSigner: false, isWritable: false, }, - ...this.getHealthCheckAccounts(banks, [assetBank, liabilityBank]), + ...this.getHealthCheckAccounts(banks, [liabilityBank, assetBank]), ...liquidateeMarginfiAccount.getHealthCheckAccounts(banks), ] ); diff --git a/packages/marginfi-client-v2/src/models/price.ts b/packages/marginfi-client-v2/src/models/price.ts index c8b06f3ad6..d32f309459 100644 --- a/packages/marginfi-client-v2/src/models/price.ts +++ b/packages/marginfi-client-v2/src/models/price.ts @@ -1,4 +1,4 @@ -import { parsePriceData } from "@pythnetwork/client"; +import { parsePriceData } from "../vendor/pyth"; import BigNumber from "bignumber.js"; import { AggregatorAccountData, AggregatorAccount } from "../vendor/switchboard"; import { PYTH_PRICE_CONF_INTERVALS, SWB_PRICE_CONF_INTERVALS } from ".."; diff --git a/packages/marginfi-client-v2/src/vendor/pyth/index.ts b/packages/marginfi-client-v2/src/vendor/pyth/index.ts new file mode 100644 index 0000000000..52efd26113 --- /dev/null +++ b/packages/marginfi-client-v2/src/vendor/pyth/index.ts @@ -0,0 +1,252 @@ +import { readBigInt64LE, readBigUInt64LE } from './readBig' +import { PublicKey } from '@solana/web3.js' + +/** Number of slots that can pass before a publisher's price is no longer included in the aggregate. */ +export const MAX_SLOT_DIFFERENCE = 25 +const empty32Buffer = Buffer.alloc(32) +const PKorNull = (data: Buffer) => (data.equals(empty32Buffer) ? null : new PublicKey(data)) + +export interface Price { + priceComponent: bigint + price: number + confidenceComponent: bigint + confidence: number + status: PriceStatus + corporateAction: CorpAction + publishSlot: number +} + +export enum PriceStatus { + Unknown, + Trading, + Halted, + Auction, + Ignored, +} + +export enum CorpAction { + NoCorpAct, +} + +const parsePriceInfo = (data: Buffer, exponent: number): Price => { + // aggregate price + const priceComponent = readBigInt64LE(data, 0) + const price = Number(priceComponent) * 10 ** exponent + // aggregate confidence + const confidenceComponent = readBigUInt64LE(data, 8) + const confidence = Number(confidenceComponent) * 10 ** exponent + // aggregate status + const status: PriceStatus = data.readUInt32LE(16) + // aggregate corporate action + const corporateAction: CorpAction = data.readUInt32LE(20) + // aggregate publish slot. It is converted to number to be consistent with Solana's library interface (Slot there is number) + const publishSlot = Number(readBigUInt64LE(data, 24)) + return { + priceComponent, + price, + confidenceComponent, + confidence, + status, + corporateAction, + publishSlot, + } +} + +export interface PriceData extends Base { + priceType: PriceType + exponent: number + numComponentPrices: number + numQuoters: number + lastSlot: bigint + validSlot: bigint + emaPrice: Ema + emaConfidence: Ema + timestamp: bigint + minPublishers: number + drv2: number + drv3: number + drv4: number + productAccountKey: PublicKey + nextPriceAccountKey: PublicKey | null + previousSlot: bigint + previousPriceComponent: bigint + previousPrice: number + previousConfidenceComponent: bigint + previousConfidence: number + previousTimestamp: bigint + priceComponents: PriceComponent[] + aggregate: Price + // The current price and confidence and status. The typical use of this interface is to consume these three fields. + // If undefined, Pyth does not currently have price information for this product. This condition can + // happen for various reasons (e.g., US equity market is closed, or insufficient publishers), and your + // application should handle it gracefully. Note that other raw price information fields (such as + // aggregate.price) may be defined even if this is undefined; you most likely should not use those fields, + // as their value can be arbitrary when this is undefined. + price: number | undefined + confidence: number | undefined + status: PriceStatus +} + +export interface Base { + magic: number + version: number + type: AccountType + size: number +} + +export enum AccountType { + Unknown, + Mapping, + Product, + Price, + Test, + Permission, +} + +export enum PriceType { + Unknown, + Price, +} + +/** + * valueComponent = numerator / denominator + * value = valueComponent * 10 ^ exponent (from PriceData) + */ +export interface Ema { + valueComponent: bigint + value: number + numerator: bigint + denominator: bigint +} + +export interface PriceComponent { + publisher: PublicKey + aggregate: Price + latest: Price +} + +// Provide currentSlot when available to allow status to consider the case when price goes stale. It is optional because +// it requires an extra request to get it when it is not available which is not always efficient. +export const parsePriceData = (data: Buffer, currentSlot?: number): PriceData => { + // pyth magic number + const magic = data.readUInt32LE(0) + // program version + const version = data.readUInt32LE(4) + // account type + const type = data.readUInt32LE(8) + // price account size + const size = data.readUInt32LE(12) + // price or calculation type + const priceType: PriceType = data.readUInt32LE(16) + // price exponent + const exponent = data.readInt32LE(20) + // number of component prices + const numComponentPrices = data.readUInt32LE(24) + // number of quoters that make up aggregate + const numQuoters = data.readUInt32LE(28) + // slot of last valid (not unknown) aggregate price + const lastSlot = readBigUInt64LE(data, 32) + // valid on-chain slot of aggregate price + const validSlot = readBigUInt64LE(data, 40) + // exponential moving average price + const emaPrice = parseEma(data.slice(48, 72), exponent) + // exponential moving average confidence interval + const emaConfidence = parseEma(data.slice(72, 96), exponent) + // timestamp of the current price + const timestamp = readBigInt64LE(data, 96) + // minimum number of publishers for status to be TRADING + const minPublishers = data.readUInt8(104) + // space for future derived values + const drv2 = data.readInt8(105) + // space for future derived values + const drv3 = data.readInt16LE(106) + // space for future derived values + const drv4 = data.readInt32LE(108) + // product id / reference account + const productAccountKey = new PublicKey(data.slice(112, 144)) + // next price account in list + const nextPriceAccountKey = PKorNull(data.slice(144, 176)) + // valid slot of previous update + const previousSlot = readBigUInt64LE(data, 176) + // aggregate price of previous update + const previousPriceComponent = readBigInt64LE(data, 184) + const previousPrice = Number(previousPriceComponent) * 10 ** exponent + // confidence interval of previous update + const previousConfidenceComponent = readBigUInt64LE(data, 192) + const previousConfidence = Number(previousConfidenceComponent) * 10 ** exponent + // space for future derived values + const previousTimestamp = readBigInt64LE(data, 200) + const aggregate = parsePriceInfo(data.slice(208, 240), exponent) + + let status = aggregate.status + + if (currentSlot && status === PriceStatus.Trading) { + if (currentSlot - aggregate.publishSlot > MAX_SLOT_DIFFERENCE) { + status = PriceStatus.Unknown + } + } + + let price + let confidence + if (status === PriceStatus.Trading) { + price = aggregate.price + confidence = aggregate.confidence + } + + // price components - up to 32 + const priceComponents: PriceComponent[] = [] + let offset = 240 + while (priceComponents.length < numComponentPrices) { + const publisher = new PublicKey(data.slice(offset, offset + 32)) + offset += 32 + const componentAggregate = parsePriceInfo(data.slice(offset, offset + 32), exponent) + offset += 32 + const latest = parsePriceInfo(data.slice(offset, offset + 32), exponent) + offset += 32 + priceComponents.push({ publisher, aggregate: componentAggregate, latest }) + } + + return { + magic, + version, + type, + size, + priceType, + exponent, + numComponentPrices, + numQuoters, + lastSlot, + validSlot, + emaPrice, + emaConfidence, + timestamp, + minPublishers, + drv2, + drv3, + drv4, + productAccountKey, + nextPriceAccountKey, + previousSlot, + previousPriceComponent, + previousPrice, + previousConfidenceComponent, + previousConfidence, + previousTimestamp, + aggregate, + priceComponents, + price, + confidence, + status, + } +} + +const parseEma = (data: Buffer, exponent: number): Ema => { + // current value of ema + const valueComponent = readBigInt64LE(data, 0) + const value = Number(valueComponent) * 10 ** exponent + // numerator state for next update + const numerator = readBigInt64LE(data, 8) + // denominator state for next update + const denominator = readBigInt64LE(data, 16) + return { valueComponent, value, numerator, denominator } +} diff --git a/packages/marginfi-client-v2/src/vendor/pyth/readBig.ts b/packages/marginfi-client-v2/src/vendor/pyth/readBig.ts new file mode 100644 index 0000000000..5ec754f1be --- /dev/null +++ b/packages/marginfi-client-v2/src/vendor/pyth/readBig.ts @@ -0,0 +1,57 @@ +import { Buffer } from 'buffer' + +// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/errors.js#L758 +const ERR_BUFFER_OUT_OF_BOUNDS = () => new Error('Attempt to access memory outside buffer bounds') + +// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/errors.js#L968 +const ERR_INVALID_ARG_TYPE = (name: string, expected: string, actual: any) => + new Error(`The "${name}" argument must be of type ${expected}. Received ${actual}`) + +// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/errors.js#L1262 +const ERR_OUT_OF_RANGE = (str: string, range: string, received: number) => + new Error(`The value of "${str} is out of range. It must be ${range}. Received ${received}`) + +// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/validators.js#L127-L130 +function validateNumber(value: any, name: string) { + if (typeof value !== 'number') throw ERR_INVALID_ARG_TYPE(name, 'number', value) +} + +// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/buffer.js#L68-L80 +function boundsError(value: number, length: number) { + if (Math.floor(value) !== value) { + validateNumber(value, 'offset') + throw ERR_OUT_OF_RANGE('offset', 'an integer', value) + } + + if (length < 0) throw ERR_BUFFER_OUT_OF_BOUNDS() + + throw ERR_OUT_OF_RANGE('offset', `>= 0 and <= ${length}`, value) +} + +// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/buffer.js#L129-L145 +export function readBigInt64LE(buffer: Buffer, offset = 0): bigint { + validateNumber(offset, 'offset') + const first = buffer[offset] + const last = buffer[offset + 7] + if (first === undefined || last === undefined) boundsError(offset, buffer.length - 8) + // tslint:disable-next-line:no-bitwise + const val = buffer[offset + 4] + buffer[offset + 5] * 2 ** 8 + buffer[offset + 6] * 2 ** 16 + (last << 24) // Overflow + return ( + (BigInt(val) << BigInt(32)) + // tslint:disable-line:no-bitwise + BigInt(first + buffer[++offset] * 2 ** 8 + buffer[++offset] * 2 ** 16 + buffer[++offset] * 2 ** 24) + ) +} + +// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/buffer.js#L89-L107 +export function readBigUInt64LE(buffer: Buffer, offset = 0): bigint { + validateNumber(offset, 'offset') + const first = buffer[offset] + const last = buffer[offset + 7] + if (first === undefined || last === undefined) boundsError(offset, buffer.length - 8) + + const lo = first + buffer[++offset] * 2 ** 8 + buffer[++offset] * 2 ** 16 + buffer[++offset] * 2 ** 24 + + const hi = buffer[++offset] + buffer[++offset] * 2 ** 8 + buffer[++offset] * 2 ** 16 + last * 2 ** 24 + + return BigInt(lo) + (BigInt(hi) << BigInt(32)) // tslint:disable-line:no-bitwise +} diff --git a/packages/marginfi-v2-ui-state/src/store/mrgnlendStore.ts b/packages/marginfi-v2-ui-state/src/store/mrgnlendStore.ts index 6df440bc82..63d4a98233 100644 --- a/packages/marginfi-v2-ui-state/src/store/mrgnlendStore.ts +++ b/packages/marginfi-v2-ui-state/src/store/mrgnlendStore.ts @@ -158,7 +158,7 @@ const stateCreator: StateCreator = (set, get) => ({ const emissionTokenPriceData = priceMap[bank.emissionsMint.toBase58()]; let userData; - if (wallet && selectedAccount && nativeSolBalance) { + if (wallet?.publicKey && selectedAccount && nativeSolBalance) { const tokenAccount = tokenAccountMap!.get(bank.mint.toBase58()); if (!tokenAccount) throw new Error(`Token account not found for ${bank.mint.toBase58()}`); userData = { @@ -196,7 +196,7 @@ const stateCreator: StateCreator = (set, get) => ({ ); let accountSummary: AccountSummary = DEFAULT_ACCOUNT_SUMMARY; - if (wallet && selectedAccount) { + if (wallet?.publicKey && selectedAccount) { accountSummary = computeAccountSummary(selectedAccount, extendedBankInfos); } diff --git a/yarn.lock b/yarn.lock index e5b14b34a2..e58280560d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1877,10 +1877,10 @@ human-id "^1.0.2" prettier "^2.7.1" -"@coral-xyz/anchor@0.28.1-beta.1", "@coral-xyz/anchor@=0.28.1-beta.1", "@coral-xyz/anchor@^0.28.1-beta.1": - version "0.28.1-beta.1" - resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.28.1-beta.1.tgz" - integrity sha512-JdKr4IQqY719wts0wzhHIffpAo4fdJ25gPLfFN75d6LMfCKvwPupyDUigaT+ac6UWTw/bDdBbJFY6QSRvlTnrA== +"@coral-xyz/anchor@0.28.0", "@coral-xyz/anchor@^0.28.0": + version "0.28.0" + resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.28.0.tgz" + integrity sha512-kQ02Hv2ZqxtWP30WN1d4xxT4QqlOXYDxmEd3k/bbneqhV3X5QMO4LAtoUFs7otxyivOgoqam5Il5qx81FuI4vw== dependencies: "@coral-xyz/borsh" "^0.28.0" "@solana/web3.js" "^1.68.0" @@ -1898,12 +1898,12 @@ superstruct "^0.15.4" toml "^3.0.0" -"@coral-xyz/anchor@^0.26.0": - version "0.26.0" - resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.26.0.tgz" - integrity sha512-PxRl+wu5YyptWiR9F2MBHOLLibm87Z4IMUBPreX+DYBtPM+xggvcPi0KAN7+kIL4IrIhXI8ma5V0MCXxSN1pHg== +"@coral-xyz/anchor@0.28.1-beta.1", "@coral-xyz/anchor@=0.28.1-beta.1", "@coral-xyz/anchor@^0.28.1-beta.1": + version "0.28.1-beta.1" + resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.28.1-beta.1.tgz" + integrity sha512-JdKr4IQqY719wts0wzhHIffpAo4fdJ25gPLfFN75d6LMfCKvwPupyDUigaT+ac6UWTw/bDdBbJFY6QSRvlTnrA== dependencies: - "@coral-xyz/borsh" "^0.26.0" + "@coral-xyz/borsh" "^0.28.0" "@solana/web3.js" "^1.68.0" base64-js "^1.5.1" bn.js "^5.1.2" @@ -1919,12 +1919,12 @@ superstruct "^0.15.4" toml "^3.0.0" -"@coral-xyz/anchor@^0.27.0": - version "0.27.0" - resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.27.0.tgz" - integrity sha512-+P/vPdORawvg3A9Wj02iquxb4T0C5m4P6aZBVYysKl4Amk+r6aMPZkUhilBkD6E4Nuxnoajv3CFykUfkGE0n5g== +"@coral-xyz/anchor@^0.26.0": + version "0.26.0" + resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.26.0.tgz" + integrity sha512-PxRl+wu5YyptWiR9F2MBHOLLibm87Z4IMUBPreX+DYBtPM+xggvcPi0KAN7+kIL4IrIhXI8ma5V0MCXxSN1pHg== dependencies: - "@coral-xyz/borsh" "^0.27.0" + "@coral-xyz/borsh" "^0.26.0" "@solana/web3.js" "^1.68.0" base64-js "^1.5.1" bn.js "^5.1.2" @@ -1940,12 +1940,12 @@ superstruct "^0.15.4" toml "^3.0.0" -"@coral-xyz/anchor@^0.28.0": - version "0.28.0" - resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.28.0.tgz" - integrity sha512-kQ02Hv2ZqxtWP30WN1d4xxT4QqlOXYDxmEd3k/bbneqhV3X5QMO4LAtoUFs7otxyivOgoqam5Il5qx81FuI4vw== +"@coral-xyz/anchor@^0.27.0": + version "0.27.0" + resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.27.0.tgz" + integrity sha512-+P/vPdORawvg3A9Wj02iquxb4T0C5m4P6aZBVYysKl4Amk+r6aMPZkUhilBkD6E4Nuxnoajv3CFykUfkGE0n5g== dependencies: - "@coral-xyz/borsh" "^0.28.0" + "@coral-xyz/borsh" "^0.27.0" "@solana/web3.js" "^1.68.0" base64-js "^1.5.1" bn.js "^5.1.2" @@ -4139,27 +4139,28 @@ resolved "https://registry.npmjs.org/@jup-ag/api/-/api-4.0.1-alpha.0.tgz" integrity sha512-H9hyf9K7sXMPd0++/hQ5Fs2eVgi2w9wtJP0YmGFLJMjF8zHYDYqQ0ZCdgmLLSp/vrEjmOHbbiLJT2psuV5W+bw== -"@jup-ag/common@4.0.0-beta.20": - version "4.0.0-beta.20" - resolved "https://registry.npmjs.org/@jup-ag/common/-/common-4.0.0-beta.20.tgz" - integrity sha512-TmyOqlRI22R5SuS7CKETdJAwWYJLQTtHb+h45i/vBqo/p4BiJxwQVXZf+Y9SYswnfppynQtp0XeW0GMCDGvtyA== +"@jup-ag/common@4.0.0-beta.21": + version "4.0.0-beta.21" + resolved "https://registry.yarnpkg.com/@jup-ag/common/-/common-4.0.0-beta.21.tgz#416b3fc75a3e0a3055dfb5fc404cb71edcd6a60b" + integrity sha512-C6W+bDfCDSQ0hEehmEHDVsRTQfXp/UBXWHZ0sFUM8wtv2JSPgTe4HG3ZoZzuMzwukVOQxKPRzxzeywWbZyJ8mg== dependencies: + "@coral-xyz/anchor" "0.28.0" "@jup-ag/api" "4.0.1-alpha.0" "@mercurial-finance/optimist" "0.2.1" - "@project-serum/anchor" "0.24.2" "@solana/spl-token" "0.1.8" "@solana/wallet-adapter-base" "0.9.22" - "@solana/web3.js" "~1.75.0" + "@solana/web3.js" "~1.77.3" "@types/jest" "27.0.2" bs58 "^4.0.1" jsbi "4.3.0" -"@jup-ag/core@^4.0.0-beta.18": - version "4.0.0-beta.20" - resolved "https://registry.npmjs.org/@jup-ag/core/-/core-4.0.0-beta.20.tgz" - integrity sha512-39o+50WzDAFwNq1t/HqdmQ+hI95JvIEPAzds9E9fxa4NpsiuslaTGju1O312Za3Ko5HveV+yF7+Y9w8ny1I5Hw== +"@jup-ag/core@^4.0.0-beta.20": + version "4.0.0-beta.21" + resolved "https://registry.yarnpkg.com/@jup-ag/core/-/core-4.0.0-beta.21.tgz#fefb14871222dd2435271e566a4c5a260158dc0b" + integrity sha512-GoVlnv6FC9T1Oh8BjNDM0lNuRSLiunJG0SgsRcMGdOZvscwqz6EP5kVOZ6c8k8eMLDz0stOYO+SP4dOqWg6MlQ== dependencies: - "@jup-ag/common" "4.0.0-beta.20" + "@coral-xyz/anchor" "0.28.0" + "@jup-ag/common" "4.0.0-beta.21" "@jup-ag/crema-sdk-v2" "2.1.6" "@jup-ag/cykura-sdk" "0.1.25" "@jup-ag/cykura-sdk-core" "0.1.8" @@ -4170,14 +4171,13 @@ "@jup-ag/invariant" "0.9.35" "@jup-ag/lifinity-sdk" "0.1.72" "@jup-ag/lifinity-sdk-v2" "1.0.8" - "@jup-ag/math" "4.0.0-beta.20" + "@jup-ag/math" "4.0.0-beta.21" "@jup-ag/phoenix-sdk" "1.4.4" "@jup-ag/raydium-clmm-sdk" "1.0.6" "@jup-ag/whirlpools-sdk" "0.7.2" "@mercurial-finance/dynamic-amm-sdk" "0.3.10" "@mercurial-finance/optimist" "0.2.1" "@noble/hashes" "1.1.2" - "@project-serum/anchor" "0.24.2" "@project-serum/serum" "0.13.65" "@pythnetwork/client" "2.7.3" "@saberhq/stableswap-sdk" "1.13.6" @@ -4282,10 +4282,10 @@ "@solana/web3.js" "1.31.0" decimal.js "^10.3.1" -"@jup-ag/math@4.0.0-beta.20": - version "4.0.0-beta.20" - resolved "https://registry.npmjs.org/@jup-ag/math/-/math-4.0.0-beta.20.tgz" - integrity sha512-k9l20K2LVGMnlkYi1RWWt2IYq4hu6d0iLTvx/6p6GfWnyqNIGJVLbFW4zhzsPX5foZ/tkeNONo5hulZrPtTQ6g== +"@jup-ag/math@4.0.0-beta.21": + version "4.0.0-beta.21" + resolved "https://registry.yarnpkg.com/@jup-ag/math/-/math-4.0.0-beta.21.tgz#30df494670dcf2fd68d68d39d508a87a08d2975f" + integrity sha512-5dPbmOkOvCbWGl2YCaDfNxTT54YIVc7NOVgybhgSwOu7evUNKBMUPEzEJZy2vexyxqOzHk+U5pO4hj+lzf8Bgg== dependencies: decimal.js "10.4.2" jsbi "4.3.0" @@ -5156,6 +5156,13 @@ "@solana/buffer-layout" "=4.0.0" "@solana/buffer-layout-utils" "=0.2.0" +"@next/bundle-analyzer@^13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-13.4.19.tgz#aaa423b029a4c7af04c7da9ba5980b267946a5fb" + integrity sha512-nXKHz63dM0Kn3XPFOKrv2wK+hP9rdBg2iR1PsxuXLHVBoZhMyS1/ldRcX80YFsm2VUws9zM4a0E/1HlLI+P92g== + dependencies: + webpack-bundle-analyzer "4.7.0" + "@next/env@13.1.1": version "13.1.1" resolved "https://registry.npmjs.org/@next/env/-/env-13.1.1.tgz" @@ -5424,6 +5431,11 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@polka/url@^1.0.0-next.20": + version "1.0.0-next.21" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" + integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== + "@popperjs/core@^2.11.8": version "2.11.8" resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" @@ -7128,15 +7140,14 @@ rpc-websockets "^7.5.0" superstruct "^0.14.2" -"@solana/web3.js@~1.75.0": - version "1.75.0" - resolved "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.75.0.tgz" - integrity sha512-rHQgdo1EWfb+nPUpHe4O7i8qJPELHKNR5PAZRK+a7XxiykqOfbaAlPt5boDWAGPnYbSv0ziWZv5mq9DlFaQCxg== +"@solana/web3.js@~1.77.3": + version "1.77.3" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.77.3.tgz#2cbeaa1dd24f8fa386ac924115be82354dfbebab" + integrity sha512-PHaO0BdoiQRPpieC1p31wJsBaxwIOWLh8j2ocXNKX8boCQVldt26Jqm2tZE4KlrvnCIV78owPLv1pEUgqhxZ3w== dependencies: "@babel/runtime" "^7.12.5" - "@noble/ed25519" "^1.7.0" - "@noble/hashes" "^1.1.2" - "@noble/secp256k1" "^1.6.3" + "@noble/curves" "^1.0.0" + "@noble/hashes" "^1.3.0" "@solana/buffer-layout" "^4.0.0" agentkeepalive "^4.2.1" bigint-buffer "^1.1.5" @@ -7145,7 +7156,7 @@ bs58 "^4.0.1" buffer "6.0.3" fast-stable-stringify "^1.0.0" - jayson "^3.4.4" + jayson "^4.1.0" node-fetch "^2.6.7" rpc-websockets "^7.5.1" superstruct "^0.14.2" @@ -8714,7 +8725,7 @@ acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.1.1: +acorn-walk@^8.0.0, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -8724,7 +8735,7 @@ acorn@^7.4.0: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.0.4, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: version "8.10.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== @@ -11959,7 +11970,7 @@ duplexer3@^0.1.4: resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz" integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== -duplexer@~0.1.1: +duplexer@^0.1.2, duplexer@~0.1.1: version "0.1.2" resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== @@ -14080,6 +14091,13 @@ gtoken@^6.1.0: google-p12-pem "^4.0.0" jws "^4.0.0" +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + hamt_plus@1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz" @@ -17268,6 +17286,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mrmime@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" + integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -17880,6 +17903,11 @@ openapi-types@~11.0.1: resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-11.0.1.tgz" integrity sha512-P2pGRlHFXgP8z6vrp5P/MtftOXYtlIY1A+V0VmioOoo85NN6RSPgGbEprRAUNMIsbfRjnCPdx/r8mi8QRR7grQ== +opener@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + optimism@^0.17.5: version "0.17.5" resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.17.5.tgz#a4c78b3ad12c58623abedbebb4f2f2c19b8e8816" @@ -20429,6 +20457,15 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sirv@^1.0.7: + version "1.0.19" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" + integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ== + dependencies: + "@polka/url" "^1.0.0-next.20" + mrmime "^1.0.0" + totalist "^1.0.0" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" @@ -21540,6 +21577,11 @@ toml@^3.0.0: resolved "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz" integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== +totalist@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" + integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== + tr46@~0.0.3: version "0.0.3" resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" @@ -22313,6 +22355,21 @@ webidl-conversions@^3.0.0: resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +webpack-bundle-analyzer@4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz#33c1c485a7fcae8627c547b5c3328b46de733c66" + integrity sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg== + dependencies: + acorn "^8.0.4" + acorn-walk "^8.0.0" + chalk "^4.1.0" + commander "^7.2.0" + gzip-size "^6.0.0" + lodash "^4.17.20" + opener "^1.5.2" + sirv "^1.0.7" + ws "^7.3.1" + webpack-dev-middleware@^5.3.1: version "5.3.3" resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz" @@ -22595,7 +22652,7 @@ ws@^6.2.2: dependencies: async-limiter "~1.0.0" -ws@^7, ws@^7.2.0, ws@^7.4.0, ws@^7.4.5, ws@^7.4.6, ws@^7.5.1: +ws@^7, ws@^7.2.0, ws@^7.3.1, ws@^7.4.0, ws@^7.4.5, ws@^7.4.6, ws@^7.5.1: version "7.5.9" resolved "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==