Skip to content

Commit

Permalink
Merge pull request #1003 from mrgnlabs/feat/tradingbox-v2
Browse files Browse the repository at this point in the history
feat(mfi-trading): tradingbox v2
  • Loading branch information
k0beLeenders authored Dec 16, 2024
2 parents 3b467c6 + 8224aee commit b77ba8a
Show file tree
Hide file tree
Showing 54 changed files with 2,745 additions and 57 deletions.
20 changes: 11 additions & 9 deletions apps/marginfi-v2-trading/src/components/common/Yield/YieldRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,26 +123,28 @@ const YieldItem = ({
<div className={cn("grid gap-4items-center", className, connected ? "grid-cols-7" : "grid-cols-6")}>
<div className="flex items-center gap-2">
<Image
src={bank.meta.tokenLogoUri}
alt={bank.meta.tokenSymbol}
src={pool.tokenBank.meta.tokenLogoUri}
alt={pool.tokenBank.meta.tokenSymbol}
width={24}
height={24}
className="rounded-full"
/>
{bank.meta.tokenSymbol}
{pool.tokenBank.meta.tokenSymbol}
</div>
<div className="flex flex-col xl:gap-2 xl:flex-row xl:items-baseline">
<span className="text-xl">{numeralFormatter(bank.info.state.totalDeposits)}</span>
<span className="text-xl">{numeralFormatter(pool.tokenBank.info.state.totalDeposits)}</span>
<span className="text-sm text-muted-foreground">
{usdFormatter.format(bank.info.state.totalDeposits * bank.info.oraclePrice.priceRealtime.price.toNumber())}
{usdFormatter.format(
pool.tokenBank.info.state.totalDeposits * pool.tokenBank.info.oraclePrice.priceRealtime.price.toNumber()
)}
</span>
</div>

<div className="text-mrgn-success text-right w-32">
{percentFormatter.format(aprToApy(bank.info.state.lendingRate))}
{percentFormatter.format(aprToApy(pool.tokenBank.info.state.lendingRate))}
</div>
<div className="text-mrgn-warning text-right w-32">
{percentFormatter.format(aprToApy(bank.info.state.borrowingRate))}
{percentFormatter.format(aprToApy(pool.tokenBank.info.state.borrowingRate))}
</div>
<div className="flex justify-center">
<Link href="https://x.com/marginfi" target="_blank">
Expand All @@ -159,8 +161,8 @@ const YieldItem = ({
<div className="pl-2 text-lg flex flex-col xl:gap-1 xl:flex-row xl:items-baseline">
{isProvidingLiquidity && bank.isActive && (
<>
{numeralFormatter(bank.position.amount)}
<span className="text-muted-foreground text-sm">{bank.meta.tokenSymbol}</span>
{numeralFormatter(pool.tokenBank.position.amount)}
<span className="text-muted-foreground text-sm">{pool.tokenBank.meta.tokenSymbol}</span>
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { IconLoader2 } from "@tabler/icons-react";
import React from "react";

import { WalletButton } from "~/components/wallet-v2";
import { Button } from "~/components/ui/button";
import { cn } from "@mrgnlabs/mrgn-utils";

type ActionButtonProps = {
isLoading: boolean;
isEnabled: boolean;
buttonLabel: string;
connected?: boolean;
handleAction: () => void;
tradeState: "long" | "short";
};

export const ActionButton = ({
isLoading,
isEnabled,
buttonLabel,
connected = false,
handleAction,
tradeState,
}: ActionButtonProps) => {
if (!connected) {
return <WalletButton className="w-full py-5 bg-muted-foreground" showWalletInfo={false} />;
}

return (
<Button
disabled={isLoading || !isEnabled}
className={cn("w-full", tradeState === "long" && "bg-success", tradeState === "short" && "bg-error")}
onClick={handleAction}
>
{isLoading ? <IconLoader2 className="animate-spin" /> : buttonLabel}
</Button>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./action-button";
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from "react";

import { IconCheck, IconX } from "@tabler/icons-react";

import { IconLoader } from "~/components/ui/icons";
import { SimulationStatus } from "~/components/common/trade-box-v2/utils";

type ActionSimulationStatusProps = {
simulationStatus: SimulationStatus;
hasErrorMessages: boolean;
isActive: boolean;
};

enum SimulationCompleteStatus {
NULL = "NULL",
LOADING = "LOADING",
SUCCESS = "SUCCESS",
ERROR = "ERROR",
}

const ActionSimulationStatus = ({
simulationStatus,
hasErrorMessages = false,
isActive = false,
}: ActionSimulationStatusProps) => {
const [simulationCompleteStatus, setSimulationCompleteStatus] = React.useState<SimulationCompleteStatus>(
SimulationCompleteStatus.NULL
);
const [isNewSimulation, setIsNewSimulation] = React.useState(false);

React.useEffect(() => {
if (simulationStatus === SimulationStatus.SIMULATING || simulationStatus === SimulationStatus.PREPARING) {
setSimulationCompleteStatus(SimulationCompleteStatus.LOADING);
setIsNewSimulation(false);
} else if (hasErrorMessages && !isNewSimulation) {
setSimulationCompleteStatus(SimulationCompleteStatus.ERROR);
} else if (simulationStatus === SimulationStatus.COMPLETE && !isNewSimulation) {
setSimulationCompleteStatus(SimulationCompleteStatus.SUCCESS);
}
}, [simulationStatus, hasErrorMessages, isNewSimulation]);

React.useEffect(() => {
if (!isActive) {
setIsNewSimulation(true);
setSimulationCompleteStatus(SimulationCompleteStatus.NULL);
}
}, [isActive]);

if (!isActive) {
return <div />; // Return empty div to align the settings button
}

return (
<div>
{simulationCompleteStatus === SimulationCompleteStatus.LOADING && (
<p className="text-xs text-muted-foreground/75 flex items-center gap-1 mr-auto">
<IconLoader size={14} /> Simulating transaction...
</p>
)}

{simulationCompleteStatus === SimulationCompleteStatus.SUCCESS && (
<p className="text-xs flex items-center gap-1 mr-auto text-success">
<IconCheck size={14} /> Simulation complete!
</p>
)}

{simulationCompleteStatus === SimulationCompleteStatus.ERROR && (
<p className="text-xs flex items-center gap-1 mr-auto text-error">
<IconX size={14} /> Simulation failed
</p>
)}
</div>
);
};

export { ActionSimulationStatus };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./action-simuation-status";
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { TradeSide } from "~/components/common/trade-box-v2/utils";
import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group";

interface ActionToggleProps {
tradeState: TradeSide;
setTradeState: (value: TradeSide) => void;
}

export const ActionToggle = ({ tradeState, setTradeState }: ActionToggleProps) => {
return (
<ToggleGroup
type="single"
className="w-full gap-4 p-0"
value={tradeState}
onValueChange={(value) => {
value && setTradeState(value as TradeSide);
}}
>
<ToggleGroupItem className="w-full border" value="long" aria-label="Toggle long">
Long
</ToggleGroupItem>
<ToggleGroupItem className="w-full border" value="short" aria-label="Toggle short">
Short
</ToggleGroupItem>
</ToggleGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./action-toggle";
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from "react";
import Image from "next/image";
import { Input } from "~/components/ui/input";
import { MaxAction } from "./components";
import { ArenaBank } from "~/store/tradeStoreV2";

interface AmountInputProps {
maxAmount: number;
amount: string;
collateralBank: ArenaBank | null;

handleAmountChange: (value: string) => void;
}

export const AmountInput = ({
amount,
collateralBank,
maxAmount,

handleAmountChange,
}: AmountInputProps) => {
const amountInputRef = React.useRef<HTMLInputElement>(null);

return (
<div className="bg-accent p-2.5 border border-accent/150 rounded-lg">
<div className="flex justify-center gap-1 items-center font-medium ">
<span className="w-full flex items-center gap-1 max-w-[162px] text-muted-foreground text-base">
{collateralBank?.meta.tokenLogoUri && (
<Image
src={collateralBank?.meta.tokenLogoUri}
alt={collateralBank?.meta.tokenSymbol}
width={24}
height={24}
className="bg-background border rounded-full"
/>
)}
{collateralBank?.meta.tokenSymbol.toUpperCase()}
</span>
<div>
<Input
type="text"
ref={amountInputRef}
inputMode="decimal"
value={amount}
onChange={(e) => handleAmountChange(e.target.value)}
placeholder="0"
className="bg-transparent shadow-none min-w-[130px] h-auto py-0 pr-0 text-right outline-none focus-visible:outline-none focus-visible:ring-0 border-none text-base font-medium"
/>
</div>
</div>
<MaxAction maxAmount={maxAmount} collateralBank={collateralBank} setAmount={handleAmountChange} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./token-select";
export * from "./max-action";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./max-action";
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { dynamicNumeralFormatter } from "@mrgnlabs/mrgn-common";
import React from "react";
import { ArenaBank } from "~/store/tradeStoreV2";

interface TradeActionProps {
maxAmount: number;
collateralBank: ArenaBank | null;

setAmount: (amount: string) => void;
}

export const MaxAction = ({ maxAmount, collateralBank, setAmount }: TradeActionProps) => {
const maxLabel = React.useMemo((): {
amount: string;
showWalletIcon?: boolean;
label?: string;
} => {
if (!collateralBank) {
return {
amount: "-",
showWalletIcon: false,
};
}

const formatAmount = (maxAmount?: number, symbol?: string) =>
maxAmount !== undefined ? `${dynamicNumeralFormatter(maxAmount)} ${symbol?.toUpperCase()}` : "-";

return {
amount: formatAmount(maxAmount, collateralBank.meta.tokenSymbol),
label: "Wallet: ",
};
}, [collateralBank, maxAmount]);
return (
<>
{collateralBank && (
<ul className="flex flex-col gap-0.5 mt-2 text-xs w-full text-muted-foreground">
<li className="flex justify-between items-center gap-1.5">
<strong className="mr-auto">{maxLabel.label}</strong>
<div className="flex space-x-1">
<div>{maxLabel.amount}</div>

<button
className="cursor-pointer border-b border-transparent transition text-mfi-action-box-highlight hover:border-mfi-action-box-highlight"
disabled={maxAmount === 0}
onClick={() => {
setAmount(maxAmount.toString());
}}
>
MAX
</button>
</div>
</li>
</ul>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./token-select";
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const TokenSelect = () => {
return <div>TokenSelect</div>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./amount-input";
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react";

import { dynamicNumeralFormatter } from "@mrgnlabs/mrgn-common";
import { cn } from "@mrgnlabs/mrgn-utils";

import { IconLoader } from "~/components/ui/icons";
import { ArenaBank } from "~/store/tradeStoreV2";

interface AmountPreviewProps {
tradeSide: "long" | "short";
selectedBank: ArenaBank | null;
amount: number;
isLoading?: boolean;
}

export const AmountPreview = ({ tradeSide, amount, isLoading, selectedBank }: AmountPreviewProps) => {
return (
<div className="flex flex-col gap-6">
<dl className="grid grid-cols-2 gap-y-2 text-sm">
<Stat label={`Size of ${tradeSide}`}>
{isLoading ? <IconLoader size={16} /> : dynamicNumeralFormatter(amount)}{" "}
{selectedBank?.meta.tokenSymbol.toUpperCase()}
</Stat>
</dl>
</div>
);
};

interface StatProps {
label: string;
classNames?: string;
children: React.ReactNode;
style?: React.CSSProperties;
}
const Stat = ({ label, classNames, children, style }: StatProps) => {
return (
<>
<dt className="text-muted-foreground">{label}</dt>
<dd className={cn("flex justify-end text-right items-center gap-2", classNames)} style={style}>
{children}
</dd>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./amount-preview";
Loading

0 comments on commit b77ba8a

Please sign in to comment.