Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(mfi-v2-tradsing): create pool updates #1013

Merged
merged 9 commits into from
Dec 26, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ export function Loader({ label = "Loading...", className, iconSize = 32, duratio
const [isVisible, setIsVisible] = React.useState(true);

React.useEffect(() => {
const interval = setInterval(
const timeout = setTimeout(
() => {
setIsVisible((prev) => !prev);
},
isVisible ? duration : 1500
);

return () => clearInterval(interval);
return () => clearTimeout(timeout);
}, [isVisible, duration]);

const containerVariants: Variants = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const CreatePoolDialog = ({ trigger }: CreatePoolDialogProps) => {

try {
const mint = new PublicKey(mintAddress);
const fetchTokenReq = await fetch(`/api/birdeye/token?address=${mint.toBase58()}`);
const fetchTokenReq = await fetch(`/api/token/overview?address=${mint.toBase58()}`);

if (!fetchTokenReq.ok) {
throw new Error("Failed to fetch token info");
Expand Down Expand Up @@ -150,9 +150,9 @@ export const CreatePoolDialog = ({ trigger }: CreatePoolDialogProps) => {
setIsOpen={setIsOpen}
/>
)}
{createPoolState === CreatePoolState.QUOTE && (
{createPoolState === CreatePoolState.QUOTE && poolData?.token && (
<CreatePoolQuote
tokenMintAddress={poolData?.token?.mint.toBase58() ?? ""}
tokenData={poolData.token}
isSearchingToken={isSearchingToken}
fetchTokenInfo={fetchTokenInfo}
setIsOpen={setIsOpen}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
import React from "react";
import Link from "next/link";
import { IconArrowRight, IconLoader2 } from "@tabler/icons-react";
import Image from "next/image";

import { IconArrowRight, IconLoader2, IconQuestionMark } from "@tabler/icons-react";
import { PublicKey } from "@solana/web3.js";

import { cn, getTokenImageURL } from "@mrgnlabs/mrgn-utils";

import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Skeleton } from "~/components/ui/skeleton";
import { useTradeStoreV2 } from "~/store";
import { ArenaPoolSummary } from "~/types/trade-store.types";
import type { PoolMintData } from "../types";

type CreatePoolMintProps = {
tokenMintAddress: string;
tokenData: PoolMintData;
isSearchingToken: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
fetchTokenInfo: (mintAddress: string) => void;
};

export const CreatePoolQuote = ({
tokenMintAddress,
isSearchingToken,
setIsOpen,
fetchTokenInfo,
}: CreatePoolMintProps) => {
const suggestedTokens = {
USDC: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
USDT: new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"),
SOL: new PublicKey("So11111111111111111111111111111111111111112"),
LST: new PublicKey("LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp"),
};

export const CreatePoolQuote = ({ tokenData, isSearchingToken, setIsOpen, fetchTokenInfo }: CreatePoolMintProps) => {
const [arenaPoolsSummary] = useTradeStoreV2((state) => [state.arenaPoolsSummary]);

const [mintAddress, setMintAddress] = React.useState("");
Expand All @@ -39,60 +45,70 @@ export const CreatePoolQuote = ({
return true;
};

const onSubmit = React.useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
React.useEffect(() => {
if (!mintAddress.length) return;
if (!mintAddress.length) return;

if (!mintAddress.length) return;

if (!verifyPublickey(mintAddress)) {
setError("Invalid mint address, please try again.");
return;
}
if (!verifyPublickey(mintAddress)) {
setError("Invalid mint address, please try again.");
return;
}

if (mintAddress === tokenMintAddress) {
setError("Quote token cannot be the same as the base token.");
return;
}
if (mintAddress === tokenData.mint.toBase58()) {
setError("Quote token cannot be the same as the base token.");
return;
}

const pools = Object.values(arenaPoolsSummary).filter((summary) =>
summary.tokenSummary.mint.equals(new PublicKey(tokenMintAddress))
);
const pools = Object.values(arenaPoolsSummary).filter((summary) =>
summary.tokenSummary.mint.equals(new PublicKey(tokenData.mint.toBase58()))
);

if (pools.length) {
const quotePool = pools.find((pool) => pool.quoteSummary.mint.equals(new PublicKey(mintAddress)));
if (pools.length) {
const quotePool = pools.find((pool) => pool.quoteSummary.mint.equals(new PublicKey(mintAddress)));

if (quotePool) {
setPoolExists(quotePool);
return;
}
if (quotePool) {
setPoolExists(quotePool);
return;
}
}

fetchTokenInfo(mintAddress);
},
[mintAddress, arenaPoolsSummary, fetchTokenInfo, tokenMintAddress]
);
fetchTokenInfo(mintAddress);
}, [mintAddress, arenaPoolsSummary, fetchTokenInfo, tokenData]);

return (
<>
<div className="text-center space-y-2 w-full mx-auto">
<h2 className="text-3xl font-medium">Create a Token Pair</h2>
<div className="flex flex-col items-center justify-center gap-1">
<div className="flex items-center translate-x-2.5">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={tokenData.icon} alt={tokenData.symbol} className="w-10 h-10 rounded-full" />
<div className="w-10 h-10 bg-muted opacity-80 rounded-full flex items-center justify-center -translate-x-4">
<IconQuestionMark size={18} />
</div>
</div>
<div className="text-3xl font-medium flex items-center gap-2">
<h2 className="shrink-0">{tokenData.symbol} /</h2> <Skeleton className="w-20 h-5 translate-y-[2px]" />
</div>
</div>
<p className="text-lg text-muted-foreground">
Enter the mint address of the token you&apos;d like to to pair with the base token.
Enter the mint address of the token you&apos;d like to to pair with {tokenData.symbol}.
</p>
</div>
<form
className={cn(
"space-y-8 w-full flex flex-col items-center",
isSearchingToken && "pointer-events-none animate-pulsate"
)}
onSubmit={onSubmit}
onSubmit={(e) => {
e.preventDefault();
setMintAddress(mintAddress);
}}
>
<Input
name="mintAddress"
disabled={isSearchingToken}
placeholder="Token mint address..."
className="w-5/6 mx-auto py-2 px-6 h-auto text-lg rounded-full bg-background outline-none focus-visible:ring-primary/75 disabled:opacity-100"
placeholder="Quote token mint address..."
className="md:w-5/6 mx-auto py-2 px-6 h-auto text-lg rounded-full bg-background outline-none focus-visible:ring-primary/75 disabled:opacity-100"
autoFocus
value={mintAddress}
onChange={(e) => {
Expand All @@ -101,10 +117,40 @@ export const CreatePoolQuote = ({
}}
/>

<div className="flex flex-col gap-2 justify-center items-center">
<p className="text-sm text-muted-foreground text-center">
Choose from popular quote tokens, or create something new.
</p>
<div className="flex items-center gap-2">
{Object.entries(suggestedTokens).map(([key, value]) => (
<Button
key={key}
type="button"
variant="outline"
size="sm"
className="gap-1 py-1 px-2"
onClick={(e) => {
setMintAddress(value.toBase58());
}}
>
<Image
src={getTokenImageURL(value)}
alt={`${key} icon`}
width={16}
height={16}
className="rounded-full"
/>
{key}
</Button>
))}
</div>
</div>

{error && <p className="text-red-500 text-sm">{error}</p>}

{poolExists ? (
<div className="flex flex-col justify-center items-center w-full max-w-sm">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getTokenImageURL(new PublicKey(poolExists.tokenSummary.mint))}
className="rounded-full"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export const CreatePoolReview = ({ poolData, setCreatePoolState }: CreatePoolRev
</div>

<div className="flex flex-col gap-2">
<div className="gap-4 w-full flex flex-col md:flex-row items-center">
<div className="flex-1 border rounded-lg p-4 w-full">
<div className="gap-4 w-full flex flex-col md:flex-row">
<div className="border rounded-lg p-4 w-full">
<h3 className="font-medium space-y-1">
<span className="text-muted-foreground">Base Token</span>
<div className="flex items-center gap-2 text-lg">
Expand All @@ -84,7 +84,7 @@ export const CreatePoolReview = ({ poolData, setCreatePoolState }: CreatePoolRev
</h3>
<TokenSummary mintData={poolData.token} bankConfig={poolData.tokenBankConfig} />
</div>
<div className="flex-1 border rounded-lg p-4 w-full">
<div className="border rounded-lg p-4 w-full">
<h3 className="font-medium space-y-1">
<span className="text-muted-foreground">Quote Token</span>
<div className="flex items-center gap-2 text-lg">
Expand Down Expand Up @@ -124,7 +124,7 @@ const TokenSummary = ({ mintData, bankConfig }: { mintData: PoolMintData; bankCo
}, [protocolIrFee]);

return (
<div className="flex flex-col mt-4 gap-1">
<div className="flex flex-col mt-4 gap-1 text-sm">
<div className="flex flex-row justify-between">
<p className="text-sm text-muted-foreground">Price</p>
<p className="text-sm">${dynamicNumeralFormatter(mintData.price)}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,33 @@ type AdminPoolCardProps = {
export const AdminPoolCard = ({ pool, last }: AdminPoolCardProps) => {
const extendedPool = useExtendedPool(pool);
return (
<div className={cn("grid grid-cols-5 py-2 w-full items-center", !last && "border-b pb-3 mb-2")}>
<div className="flex items-center gap-2">
<img
src={extendedPool.tokenBank.meta.tokenLogoUri}
alt={extendedPool.tokenBank.meta.tokenSymbol}
width={32}
height={32}
className="rounded-full bg-background"
/>
<h2>{extendedPool.tokenBank.meta.tokenSymbol}</h2>
<div
className={cn("grid grid-cols-2 md:grid-cols-6 py-2 gap-4 w-full items-center", !last && "border-b pb-3 mb-2")}
>
<div className="flex items-center md:col-span-2">
<div className="flex items-end">
<img
src={extendedPool.tokenBank.meta.tokenLogoUri}
alt={extendedPool.tokenBank.meta.tokenSymbol}
width={38}
height={38}
className="rounded-full bg-background"
/>
<img
src={extendedPool.quoteBank.meta.tokenLogoUri}
alt={extendedPool.quoteBank.meta.tokenSymbol}
width={24}
height={24}
className="rounded-full bg-background -translate-x-4"
/>
</div>
<h2>
{extendedPool.tokenBank.meta.tokenSymbol} / {extendedPool.quoteBank.meta.tokenSymbol}
</h2>
</div>
{extendedPool.tokenBank.tokenData && (
<>
<div>
<div className="hidden md:block">
{tokenPriceFormatter(extendedPool.tokenBank.info.oraclePrice.priceRealtime.price.toNumber())}{" "}
<span
className={cn(
Expand All @@ -42,7 +55,7 @@ export const AdminPoolCard = ({ pool, last }: AdminPoolCardProps) => {
{percentFormatter.format(extendedPool.tokenBank.tokenData.priceChange24hr / 100)}
</span>
</div>
<div>
<div className="hidden md:block">
${numeralFormatter(extendedPool.tokenBank.tokenData.volume24hr)}
{extendedPool.tokenBank.tokenData.volumeChange24hr && (
<span
Expand All @@ -60,7 +73,7 @@ export const AdminPoolCard = ({ pool, last }: AdminPoolCardProps) => {
{/* <div>{extendedPool.poolData && `$${numeralFormatter(extendedPool.quoteBa)}`}</div> */}
</>
)}
<div>
<div className="hidden md:block">
<Link
href={`https://solscan.io/account/${pool.groupPk.toBase58()}`}
className="flex items-center gap-1.5"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,6 @@ const YieldItem = ({
{percentFormatter.format(bankDepositData.capacity)} capacity.
</span>
{!bankDepositData.isBankFilled && <span>Available: {numeralFormatter(bankDepositData.available)}</span>}
<a href="https://docs.marginfi.com">
<u>Learn more.</u>
</a>
</TooltipContent>
</Tooltip>
</TooltipProvider>
Expand All @@ -250,9 +247,6 @@ const YieldItem = ({
{percentFormatter.format(bankBorrowData.capacity)} capacity.
</span>
{!bankBorrowData.isBankFilled && <span>Available: {numeralFormatter(bankBorrowData.available)}</span>}
<a href="https://docs.marginfi.com">
<u>Learn more.</u>
</a>
</TooltipContent>
</Tooltip>
</TooltipProvider>
Expand Down
Loading
Loading