Skip to content

Commit

Permalink
fix: mvp impermanent loss
Browse files Browse the repository at this point in the history
  • Loading branch information
DaveVodrazka committed Sep 17, 2024
1 parent cb4b3ba commit d5b3e14
Show file tree
Hide file tree
Showing 4 changed files with 375 additions and 118 deletions.
307 changes: 192 additions & 115 deletions src/components/ImpermanentLoss/ImpermanentLossWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,58 @@ import { fetchOptions } from "../TradeTable/fetchOptions";
import { LoadingAnimation } from "../Loading/Loading";
import { useCallback, useEffect, useState } from "react";
import { Pair, PairKey } from "../../classes/Pair";
import { uniquePrimitiveValues } from "../../utils/utils";
import { formatNumber, uniquePrimitiveValues } from "../../utils/utils";
import { debounce, MenuItem, Select, SelectChangeEvent } from "@mui/material";
import { StrkToken, UsdcToken, EthToken, BtcToken } from "../../classes/Token";
import { PairNamedBadge } from "../TokenBadge";
import { StrkToken, UsdcToken, EthToken } from "../../classes/Token";
import { PairNamedBadge, TokenNamedBadge } from "../TokenBadge";
import { handleNumericChangeFactory } from "../../utils/inputHandling";
import { debug } from "../../utils/debugger";
import { getPrice, ILPrice } from "./getPrice";
import { buyImpLoss, getPrice, ILPrice } from "./getPrice";
import { longInteger, shortInteger } from "../../utils/computations";
import { TransactionState } from "../../types/network";
import { useAccount } from "../../hooks/useAccount";
import { useCurrency } from "../../hooks/useCurrency";

import styles from "./imp_loss.module.css";

export const ImpermanentLossWidget = () => {
const { isLoading, isError, data } = useQuery(
QueryKeys.options,
fetchOptions
);
const [pair, setPair] = useState<PairKey>(PairKey.STRK_USDC);
const account = useAccount();
const [pair, setPair] = useState<PairKey>(PairKey.ETH_USDC);
const [selectedMaturity, setSelectedMaturity] = useState<
number | undefined
>();
const [loading, setLoading] = useState<boolean>(false);
const [amount, setAmount] = useState<number>(1);
const [amountText, setAmountText] = useState<string>("1");
const [price, setPrice] = useState<ILPrice | undefined>();
const [error, setError] = useState<string | undefined>();
const [txStatus, setTxStatus] = useState<TransactionState>(
TransactionState.Initial
);
const tokenPair = Pair.pairByKey(pair);
const baseTokenPrice = useCurrency(tokenPair.baseToken.id);
const quoteTokenPrice = useCurrency(tokenPair.quoteToken.id);

// eslint-disable-next-line react-hooks/exhaustive-deps
const callWithDelay = useCallback(
debounce((amount: number, controller: AbortController) => {
if (selectedMaturity === undefined) {
if (selectedMaturity === undefined || amount === 0) {
return;
}

if (
txStatus === TransactionState.Fail ||
txStatus === TransactionState.Success
) {
setTxStatus(TransactionState.Initial);
}

setLoading(true);
// unset old price
setPrice(undefined);
setError(undefined);

getPrice(
longInteger(amount, tokenPair.baseToken.decimals),
Expand All @@ -54,15 +72,13 @@ export const ImpermanentLossWidget = () => {
debug("Failed fetching modal data");
debug("warn", e.message);
setLoading(false);
setError(e.message);
});
}),
[selectedMaturity, amount, pair]
);

useEffect(() => {
const controller = new AbortController();
setLoading(true);
callWithDelay(amount, controller);
return () => {
controller.abort();
Expand All @@ -84,6 +100,8 @@ export const ImpermanentLossWidget = () => {
.map((o) => o.maturity)
.filter(uniquePrimitiveValues);

const sizeRaw = longInteger(amount, tokenPair.baseToken.decimals);

const handlePairChange = (event: SelectChangeEvent) => {
setPair(event.target.value as PairKey);
};
Expand All @@ -100,6 +118,21 @@ export const ImpermanentLossWidget = () => {
}
);

const handleBuy = () => {
if (!account || !selectedMaturity || !price) {
return;
}
buyImpLoss(
account,
sizeRaw,
tokenPair.baseToken.address,
tokenPair.quoteToken.address,
selectedMaturity,
price,
setTxStatus
);
};

const formatTimestamp = (timestamp: number) => {
const date = new Date(timestamp * 1000);
return date.toLocaleDateString("en-US", { day: "numeric", month: "short" });
Expand All @@ -115,126 +148,170 @@ export const ImpermanentLossWidget = () => {
}

return (
<div>
<h1>Impermanenct Loss</h1>
<h3>Pair</h3>
<div>
<Select
value={pair}
onChange={handlePairChange}
sx={{
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},
".css-1ly9a1d-MuiSelect-select-MuiInputBase-input-MuiOutlinedInput-input":
{ padding: 0 },
width: "270px",
}}
>
<MenuItem value={PairKey.STRK_USDC}>
<PairNamedBadge tokenA={StrkToken} tokenB={UsdcToken} />
</MenuItem>
<MenuItem value={PairKey.ETH_USDC}>
<PairNamedBadge tokenA={EthToken} tokenB={UsdcToken} />
</MenuItem>
<MenuItem value={PairKey.ETH_STRK}>
<PairNamedBadge tokenA={EthToken} tokenB={StrkToken} />
</MenuItem>
<MenuItem value={PairKey.BTC_USDC}>
<PairNamedBadge tokenA={BtcToken} tokenB={UsdcToken} />
</MenuItem>
</Select>
</div>
<h3>Maturity</h3>
<div className={styles.container}>
<h2>Impermanent Loss Protect</h2>
<div>
<Select
value={selectedMaturity}
onChange={handleMaturityChange}
sx={{
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},
".css-1ly9a1d-MuiSelect-select-MuiInputBase-input-MuiOutlinedInput-input":
{ padding: 0 },
}}
>
<p className="p4 secondary-col">Pair</p>
<div>
<Select
value={pair}
onChange={handlePairChange}
sx={{
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},
".css-1ly9a1d-MuiSelect-select-MuiInputBase-input-MuiOutlinedInput-input":
{ padding: 0 },
width: "270px",
}}
>
<MenuItem value={PairKey.STRK_USDC}>
<PairNamedBadge tokenA={StrkToken} tokenB={UsdcToken} />
</MenuItem>
<MenuItem value={PairKey.ETH_USDC}>
<PairNamedBadge tokenA={EthToken} tokenB={UsdcToken} />
</MenuItem>
<MenuItem value={PairKey.ETH_STRK}>
<PairNamedBadge tokenA={EthToken} tokenB={StrkToken} />
</MenuItem>
</Select>
</div>
{baseTokenPrice && quoteTokenPrice ? (
<p className="p4 secondary-col">
1 {tokenPair.baseToken.symbol} ~{" "}
{formatNumber(baseTokenPrice / quoteTokenPrice)}{" "}
{tokenPair.quoteToken.symbol}
</p>
) : (
<LoadingAnimation size={20} />
)}
<div className={styles.inputcontainer}>
<p className="p4 secondary-col">{tokenPair.baseToken.symbol}</p>
<div className={styles.tokeninput}>
<div>
<input
onChange={handleInputChange}
type="text"
placeholder="base size"
value={amountText}
className="white-col"
/>
<p className="p4 secondary-col">
{baseTokenPrice ? (
"$" + formatNumber(amount * baseTokenPrice)
) : (
<LoadingAnimation size={20} />
)}
</p>
</div>
<div>
<TokenNamedBadge token={tokenPair.baseToken} size="small" />
</div>
</div>
<p className="p4 secondary-col">{tokenPair.quoteToken.symbol}</p>

<div className={styles.tokeninput}>
<div>
<input
type="text"
placeholder="quote size"
value={
amount && baseTokenPrice && quoteTokenPrice
? formatNumber((amount * baseTokenPrice) / quoteTokenPrice)
: ""
}
disabled
/>
<p className="p4 secondary-col">
{baseTokenPrice ? (
"$" + formatNumber(amount * baseTokenPrice)
) : (
<LoadingAnimation size={20} />
)}
</p>
</div>
<div>
<TokenNamedBadge token={tokenPair.quoteToken} size="small" />
</div>
</div>
</div>

<p className="p4 secondary-col">Duration</p>
<div className={styles.buttoncontainer}>
{maturities.map((m, i) => {
const className =
m === selectedMaturity ? "active secondary" : "secondary";

return (
<MenuItem key={i} value={m}>
<button
className={className}
key={i}
onClick={() => setSelectedMaturity(m)}
>
{formatTimestamp(m)}
</MenuItem>
</button>
);
})}
</Select>
</div>
<h3>Size</h3>
<input
onChange={handleInputChange}
type="text"
placeholder="size"
value={amountText}
/>

{loading ? (
<LoadingAnimation />
) : (
price && (
</div>

<div className="divider topmargin botmargin" />

{loading || !price ? (
<LoadingAnimation />
) : (
<div>
<div>
<h3>Calldata</h3>
<div className={styles.coverageprice}>
<div>
<ul>
<li>
{longInteger(
amount,
tokenPair.baseToken.decimals
).toString()}
</li>
<li>{tokenPair.baseToken.address}</li>
<li>{tokenPair.quoteToken.address}</li>
<li>{selectedMaturity}</li>
</ul>
<p className="p4 secondary-col">Final coverage price</p>
</div>
</div>
<div>
<h3>Response</h3>
<div>
<h4>Raw</h4>
<p>
[{price.basePrice.toString()}, {price.quotePrice.toString()}]
{formatNumber(
shortInteger(price.basePrice, tokenPair.baseToken.decimals),
5
)}{" "}
{tokenPair.baseToken.symbol}
</p>
<p>
{formatNumber(
shortInteger(
price.quotePrice,
tokenPair.quoteToken.decimals
),
5
)}{" "}
{tokenPair.quoteToken.symbol}
</p>
</div>
<div>
<h4>Human Readable</h4>
<div>
<ul>
<li>
{tokenPair.baseToken.symbol}{" "}
{shortInteger(
price.basePrice,
tokenPair.baseToken.decimals
)}
</li>
<li>
{tokenPair.quoteToken.symbol}{" "}
{shortInteger(
price.quotePrice,
tokenPair.quoteToken.decimals
)}
</li>
</ul>
</div>
</div>
</div>
<div className="topmargin">
{txStatus === TransactionState.Initial && (
<button
className="primary active mainbutton"
onClick={handleBuy}
>
Protect
</button>
)}
{txStatus === TransactionState.Success && (
<button className="green active mainbutton" onClick={handleBuy}>
Success
</button>
)}
{txStatus === TransactionState.Fail && (
<button className="red active mainbutton" onClick={handleBuy}>
Fail
</button>
)}
{txStatus === TransactionState.Processing && (
<button className="primary active mainbutton" disabled>
<LoadingAnimation size={20} />
</button>
)}
</div>
</div>
)
)}
{error && (
<div>
<h3>Error</h3>
<p>{error}</p>
</div>
)}
)}
</div>
</div>
);
};
Loading

0 comments on commit d5b3e14

Please sign in to comment.