Skip to content

Commit

Permalink
feat: slippage feature & lst improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
k0beLeenders committed Feb 20, 2024
1 parent 480de54 commit a718587
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 103 deletions.
2 changes: 2 additions & 0 deletions apps/marginfi-v2-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
Expand Down Expand Up @@ -78,6 +79,7 @@
"react-cookie": "^6.1.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.50.1",
"react-hotkeys-hook": "^4.4.1",
"react-katex": "^3.0.1",
"react-number-format": "^5.2.2",
Expand Down
53 changes: 39 additions & 14 deletions apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBox.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react";

import { AddressLookupTableAccount, Connection, PublicKey, TransactionInstruction } from "@solana/web3.js";
import { createJupiterApiClient, QuoteResponse } from "@jup-ag/api";
import { createJupiterApiClient, QuoteGetRequest, QuoteResponse } from "@jup-ag/api";

import { WSOL_MINT, nativeToUi, uiToNative } from "@mrgnlabs/mrgn-common";
import { WSOL_MINT, nativeToUi, percentFormatter, uiToNative } from "@mrgnlabs/mrgn-common";
import { ActionType, ActiveBankInfo, ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state";

import { useLstStore, useMrgnlendStore, useUiStore } from "~/store";
Expand Down Expand Up @@ -31,6 +31,7 @@ import { IconAlertTriangle, IconWallet, IconSettings } from "~/components/ui/ico
import { ActionBoxPreview } from "./ActionBoxPreview";
import { ActionBoxTokens } from "./ActionBoxTokens";
import { ActionBoxHeader } from "./ActionBoxHeader";
import { ActionBoxSlippage } from "./ActionBoxSlippage";

type ActionBoxProps = {
requestedAction?: ActionType;
Expand Down Expand Up @@ -94,6 +95,8 @@ export const ActionBox = ({
[lendingModeFromStore, requestedLendingMode]
);

const [slippageBps, setSlippageBps] = React.useState<number>(100);

const [amountRaw, setAmountRaw] = React.useState<string>("");
const [maxAmountCollat, setMaxAmountCollat] = React.useState<number>();

Expand All @@ -102,6 +105,7 @@ export const ActionBox = ({
const [selectedTokenBank, setSelectedTokenBank] = React.useState<PublicKey | null>(null);
const [selectedRepayTokenBank, setSelectedRepayTokenBank] = React.useState<PublicKey | null>(null);
const [isPriorityFeesMode, setIsPriorityFeesMode] = React.useState<boolean>(false);
const [isSlippageMode, setIsSlippageMode] = React.useState<boolean>(false);
const [isLoading, setIsLoading] = React.useState(false);
const [isLSTDialogOpen, setIsLSTDialogOpen] = React.useState(false);
const [lstDialogVariant, setLSTDialogVariant] = React.useState<LSTDialogVariants | null>(null);
Expand Down Expand Up @@ -331,7 +335,7 @@ export const ActionBox = ({
amount: uiToNative(repayBank.userInfo.maxWithdraw, repayBank.info.state.mintDecimals).toNumber(),
inputMint: repayBank.info.state.mint.toBase58(),
outputMint: bank.info.state.mint.toBase58(),
slippageBps: 100,
slippageBps: slippageBps,
swapMode: "ExactIn" as any,
};

Expand All @@ -346,10 +350,9 @@ export const ActionBox = ({
amount: uiToNative(amount, bank.info.state.mintDecimals).toNumber(),
inputMint: repayBank.info.state.mint.toBase58(),
outputMint: bank.info.state.mint.toBase58(),
slippageBps: 100,
slippageBps: slippageBps,
swapMode: "ExactOut" as any,
// maxAccounts: 20,
};
} as QuoteGetRequest;

const swapQuote = await jupiterQuoteApi.quoteGet(quoteParams);
const withdrawAmount = nativeToUi(swapQuote.otherAmountThreshold, repayBank.info.state.mintDecimals);
Expand Down Expand Up @@ -643,15 +646,29 @@ export const ActionBox = ({
isDialog && "border border-background-gray-light/50"
)}
>
<ActionBoxHeader
actionType={actionMode}
repayType={repayMode}
changeRepayType={(repayType: RepayType) => setRepayMode(repayType)}
/>
{isPriorityFeesMode ? (
<ActionBoxPriorityFees mode={actionMode} setIsPriorityFeesMode={setIsPriorityFeesMode} />
{isSlippageMode || isPriorityFeesMode ? (
<>
{isSlippageMode && (
<ActionBoxSlippage
mode={actionMode}
setSlippageBps={(value) => {
setSlippageBps(value * 100);
setIsSlippageMode(false);
}}
slippageBps={slippageBps / 100}
/>
)}
{isPriorityFeesMode && (
<ActionBoxPriorityFees mode={actionMode} setIsPriorityFeesMode={setIsPriorityFeesMode} />
)}
</>
) : (
<>
<ActionBoxHeader
actionType={actionMode}
repayType={repayMode}
changeRepayType={(repayType: RepayType) => setRepayMode(repayType)}
/>
<div className="flex flex-row items-center justify-between mb-3">
{!isDialog || actionMode === ActionType.MintLST || actionMode === ActionType.Repay ? (
<div className="text-lg font-normal flex items-center">{titleText}</div>
Expand Down Expand Up @@ -776,13 +793,21 @@ export const ActionBox = ({
actionMode={actionMode}
/>

<div className="flex justify-end mt-3">
<div className="flex justify-end gap-2 mt-3">
<button
onClick={() => setIsPriorityFeesMode(true)}
className="text-xs gap-1 ml-1 h-6 py-1 px-2 flex flex-row items-center justify-center rounded-full border border-background-gray-light bg-transparent hover:bg-background-gray-light text-muted-foreground"
>
Txn priority: {priorityFeeLabel} <IconSettings size={16} />
</button>
{(actionMode === ActionType.MintLST || repayMode === RepayType.RepayCollat) && (
<button
onClick={() => setIsSlippageMode(true)}
className="text-xs gap-1 ml-1 h-6 py-1 px-2 flex flex-row items-center justify-center rounded-full border border-background-gray-light bg-transparent hover:bg-background-gray-light text-muted-foreground"
>
Txn slippage: {percentFormatter.format(slippageBps / 10_000)} <IconSettings size={16} />
</button>
)}
</div>
</ActionBoxPreview>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,56 @@
import React from "react";

import { ActionType } from "@mrgnlabs/marginfi-v2-ui-state";
import { useForm } from "react-hook-form";

import { useUiStore } from "~/store";
import { cn } from "~/utils";

import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { IconInfoCircle, IconArrowLeft } from "~/components/ui/icons";
import { IconArrowLeft } from "~/components/ui/icons";
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
import { Label } from "~/components/ui/label";
import { Form, FormControl, FormField, FormItem, FormMessage } from "~/components/ui/form";

type ActionBoxSlippageProps = {
mode: ActionType;
setIsPriorityFeesMode: (value: boolean) => void;
slippageBps: number;
setSlippageBps: (value: number) => void;
};

const DEFAULT_SLIPPAGE_BPS = 100;

const priorityFeeOptions = [
const slippageOptions = [
{
label: "Low",
value: 100,
value: 0.5,
},
{
label: "Normal",
value: 500,
value: 1,
},
{
label: "High",
value: 1000,
value: 5,
},
];

export const ActionBoxSlippage = ({ mode, setIsPriorityFeesMode }: ActionBoxSlippageProps) => {
const [priorityFee, setPriorityFee] = useUiStore((state) => [state.priorityFee, state.setPriorityFee]);
const [selectedSlippage, setSelectedSlippage] = React.useState<number | null>(DEFAULT_SLIPPAGE_BPS);
interface SlippageForm {
slippageBps: number;
}

const slippageRef = React.useRef<HTMLInputElement>(null);
const [isCustomPriorityFeeMode, setIsCustomPriorityFeeMode] = React.useState<boolean>(false);
const [customSlippage, setCustomSlippage] = React.useState<number | null>(null);
export const ActionBoxSlippage = ({ mode, slippageBps, setSlippageBps }: ActionBoxSlippageProps) => {
const form = useForm<SlippageForm>({
defaultValues: {
slippageBps: slippageBps,
},
});
const formWatch = form.watch();

const isCustomSlippage = React.useMemo(
() => (slippageOptions.find((value) => value.value === formWatch.slippageBps) ? false : true),
[formWatch.slippageBps]
);

const modeLabel = React.useMemo(() => {
let label = "";
Expand All @@ -52,83 +64,83 @@ export const ActionBoxSlippage = ({ mode, setIsPriorityFeesMode }: ActionBoxSlip
return label;
}, [mode]);

function onSubmit(data: SlippageForm) {
setSlippageBps(data.slippageBps);
}

return (
<div className="space-y-6">
<button className="flex items-center gap-1.5 text-sm" onClick={() => setIsPriorityFeesMode(false)}>
<IconArrowLeft size={18} /> Back {modeLabel}
</button>
<h2 className="text-lg font-normal mb-2 flex items-center gap-2">
Set transaction priority{" "}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<IconInfoCircle size={16} />
</TooltipTrigger>
<TooltipContent>
<div className="space-y-2">
<p>Priority fees are paid to the Solana network.</p>
<p>This additional fee helps boost how a transaction is prioritized.</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</h2>
<ul className="grid grid-cols-3 gap-3 mb-6">
{priorityFeeOptions.map((option) => (
<li key={option.value}>
<Button
className={cn(
"flex flex-col gap-0.5 h-auto w-full font-light border border-transparent bg-background/50 transition-colors hover:bg-background-gray-hover",
selectedSlippage === option.value &&
customSlippage === null &&
"bg-background-gray-hover border-chartreuse"
)}
variant="secondary"
onClick={() => {
setSelectedSlippage(option.value);
setCustomSlippage(null);
setIsCustomPriorityFeeMode(false);
}}
>
{option.label} <strong className="font-medium">{option.value} SOL</strong>
</Button>
</li>
))}
</ul>
<h2 className="font-normal mb-2">or set manually</h2>
<div className="relative mb-6">
<Input
ref={slippageRef}
type="number"
className={cn(
"h-auto bg-background/50 py-3 px-4 border border-transparent text-white transition-colors focus-visible:ring-0",
selectedSlippage !== 100 && selectedSlippage !== 500 && selectedSlippage !== 1000 && "border-chartreuse"
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<button className="flex items-center gap-1.5 text-sm" onClick={() => setSlippageBps(slippageBps)}>
<IconArrowLeft size={18} /> Back {modeLabel}
</button>
<h2 className="text-lg font-normal mb-2 flex items-center gap-2">Set transaction slippage </h2>
<FormField
control={form.control}
name="slippageBps"
render={({ field }) => (
<FormItem className="space-y-3">
<FormControl>
<RadioGroup
onValueChange={(value) => field.onChange(Number(value))}
defaultValue={field.value.toString()}
className="flex justify-between"
>
{slippageOptions.map((option) => (
<div
className={cn(
"w-full font-light border border-transparent rounded p-3 bg-background/50 transition-colors hover:bg-background-gray-hover",
field.value === option.value && "bg-background-gray-hover border-chartreuse"
)}
>
<RadioGroupItem value={option.value.toString()} id={option.value.toString()} className="hidden" />
<Label
className={"flex flex-col gap-2 h-auto w-full text-center"}
htmlFor={option.value.toString()}
>
{" "}
{option.label} <strong className="font-medium">{option.value} %</strong>
</Label>
</div>
))}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
value={customSlippage && !isCustomPriorityFeeMode ? customSlippage?.toString() : undefined}
min={0}
placeholder={
selectedSlippage !== 100 && selectedSlippage !== 500 && selectedSlippage !== 1000
? priorityFee.toString()
: "0"
}
onFocus={() => setIsCustomPriorityFeeMode(true)}
onChange={() => setCustomSlippage(parseFloat(slippageRef.current?.value || "0"))}
/>
<span className="absolute inset-y-0 right-3 text-sm flex items-center">SOL</span>
</div>
<Button
onClick={() => {
if (customSlippage) {
setPriorityFee(customSlippage);
} else if (selectedSlippage !== null) {
setPriorityFee(selectedSlippage);
}
setIsPriorityFeesMode(false);
}}
className="w-full py-6"
>
Save Settings
</Button>
</div>
<h2 className="font-normal mb-2">or set manually</h2>

<FormField
control={form.control}
name="slippageBps"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="relative mb-6">
<Input
type="number"
min={0}
value={isCustomSlippage ? field.value : undefined}
placeholder={isCustomSlippage ? field.value.toString() : "0"}
onChange={(e) => field.onChange(e)}
className={cn(
"h-auto bg-background/50 py-3 px-4 border border-transparent text-white transition-colors focus-visible:ring-0",
isCustomSlippage && "border-chartreuse"
)}
/>
<span className="absolute inset-y-0 right-3 text-sm flex items-center">%</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<Button type="submit" className="w-full py-6">
Save Settings
</Button>
</form>
</Form>
);
};
Loading

0 comments on commit a718587

Please sign in to comment.