From 0d628e18dc91c28e430bdd6beae696e659b0e5c7 Mon Sep 17 00:00:00 2001 From: DaveVodrazka Date: Wed, 14 Aug 2024 15:06:31 +0200 Subject: [PATCH 1/3] feat: redesign price protection --- public/index.html | 2 + src/App.tsx | 2 +- src/components/Header/Header.tsx | 4 +- .../PriceGuard/BuyPriceGuardBox.tsx | 254 ++++++++++++------ .../PriceGuard/priceguard.module.css | 196 +++++++++----- src/pages/priceGuard.tsx | 23 +- src/utils/utils.ts | 26 +- 7 files changed, 339 insertions(+), 168 deletions(-) diff --git a/public/index.html b/public/index.html index 54fe89fc..df92fed9 100644 --- a/public/index.html +++ b/public/index.html @@ -18,6 +18,8 @@ + diff --git a/src/App.tsx b/src/App.tsx index 8d2e1fe5..17401232 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -73,7 +73,7 @@ const App = () => { } /> } /> } /> - } /> + } /> } /> diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 4b6d3b1c..7dbd989b 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -18,8 +18,8 @@ const navLinks = [ link: "/staking", }, { - title: "Price Guard", - link: "/priceguard", + title: "Price Protect", + link: "/priceprotect", }, { title: "Trade", diff --git a/src/components/PriceGuard/BuyPriceGuardBox.tsx b/src/components/PriceGuard/BuyPriceGuardBox.tsx index a78f0db8..a64b6a58 100644 --- a/src/components/PriceGuard/BuyPriceGuardBox.tsx +++ b/src/components/PriceGuard/BuyPriceGuardBox.tsx @@ -57,7 +57,7 @@ const BuyPriceGuardButton = ({ option, size }: BuyButtonProps) => { return ( + ); + })} + -
+
- Total coverage price - + Protection price +
-
-
- -
-
-
- - {displayBalance && ( - - Available: {displayBalance} {token.symbol}{" "} - - - )} - {displayBalance === undefined && Loading...} +
+ {strikes.map((strike) => { + const className = + strike === currentStrike + ? `${styles.datetime} ${styles.active}` + : styles.datetime; + return ( + + ); + })}
-
- -
-
- +
+
+ Final coverage price + + {priceLoading || price === undefined ? "---" : "$" + price.toFixed(3)} +
-
-
- {priceLoading || price === undefined - ? "Loading..." - : `$${price.toFixed(2)}`} - {account === undefined && ( - - )} - {!priceLoading && account && ( - - )} -
+
+ {account === undefined ? ( + + ) : price === undefined ? ( + + ) : ( + + )}
); + + // return ( + //
+ //
+ //
+ // Asset to protect + // + //
+ //
+ //
+ //
+ // Amount to protect + // + //
+ //
+ //
+ //
+ // Price to secure + // + //
+ //
+ //
+ //
+ // Duration/Until + // + //
+ //
+ //
+ //
+ // Total coverage price + // + //
+ //
+ //
+ // + //
+ //
+ //
+ // + // {displayBalance && ( + // + // Available: {displayBalance} {token.symbol}{" "} + // + // + // )} + // {displayBalance === undefined && Loading...} + //
+ //
+ //
+ // + //
+ //
+ // + //
+ //
+ //
+ // {priceLoading || price === undefined + // ? "Loading..." + // : `$${price.toFixed(2)}`} + // {account === undefined && ( + // + // )} + // {!priceLoading && account && ( + // + // )} + //
+ //
+ //
+ // ); }; diff --git a/src/components/PriceGuard/priceguard.module.css b/src/components/PriceGuard/priceguard.module.css index f843a4df..7ff3bdd1 100644 --- a/src/components/PriceGuard/priceguard.module.css +++ b/src/components/PriceGuard/priceguard.module.css @@ -1,10 +1,126 @@ .container { - display: grid; - grid-template-columns: repeat(5, 1fr); - grid-template-rows: repeat(2, 100px); + display: flex; + flex-flow: column; + gap: 18px; + max-width: 812px; + font-family: "IBM Plex Sans Condensed", sans; + font-size: 18px; + font-weight: 500; + line-height: 23.4px; + margin: 50px auto; +} + +.container button { + transition: 0s; + border-color: var(--LIGHT-GREY); + color: var(--LIGHT-GREY); + height: 25px; + padding: 3px 18px; + min-width: 0; +} + +.container button.active { + background: var(--GOLD); + color: black; + border-color: var(--GOLD); +} + +.container button span { + border-color: var(--LIGHT-GREY); + color: var(--LIGHT-GREY); +} + +.container button.active span { + background: var(--GOLD); + color: black; +} + +.assetamount { + display: flex; + flex-flow: column; +} + +.assetamount>div>div { + display: flex; + flex-flow: row; + height: 65px; + background: #1A1A1A; + margin: 8px 0; +} + +.assetamount>div>div>input { width: 100%; - padding: 20px; - box-sizing: border-box; + font-size: 24px; + border: none; +} + +.assetamount>div>div>select { + border: none; +} + +.row { + display: flex; +} + +.row>div:first-child { + width: 180px; +} + +.row>div:nth-child(2) { + display: flex; + gap: 15px; +} + +.balance { + display: flex; + flex-flow: row; + justify-content: flex-end; + align-items: baseline; + gap: 8px; +} + +.balance span { + font-size: 15px; +} + +.balance span:first-child { + text-transform: uppercase; + font-size: 12px; + color: var(--LIGHT-GREY); +} + +.datetime>div { + display: flex; + align-items: baseline; + gap: 8px; +} + +.datetime>div>span { + font-size: 15px; +} + +.datetime>div>span:first-child { + font-size: 18px; +} + +.divider { + height: 1px; + border-top: 1px solid var(--LIGHT-GREY); +} + +.coverage { + display: flex; + align-items: flex-start; + justify-content: space-between; +} + +button.buybutton { + width: 100%; + height: 36px; + text-align: center; + background: var(--GOLD); + border: none; + color: black; } .column { @@ -37,8 +153,7 @@ } .info { - width: 20px; - height: 20px; + display: flex; } .info svg { @@ -49,9 +164,14 @@ .title { display: flex; align-items: center; + font-size: 15px; gap: 5px; } +.finalprice { + font-size: 20px; +} + .container select { background: none; border: 2px solid var(--GOLD); @@ -83,66 +203,4 @@ .container input:focus { outline: 1px solid var(--GOLD); -} - - -.item { - display: flex; - justify-content: center; - align-items: center; -} - -@media (min-width: 781px) { - .item:nth-child(-n + 5) { - border-bottom: solid 2px white; - } - - .item:nth-child(-n+4) { - border-right: solid 2px white; - } - - .item:nth-child(n+6):nth-child(-n+9) { - border-right: solid 2px white; - } -} - -@media (max-width: 780px) { - .container { - grid-template-columns: repeat(2, 1fr); - grid-template-rows: repeat(5, 100px); - } - - .item:nth-child(1) { - grid-row: 1; - grid-column: 1; - border-right: solid 2px white; - } - - .item:nth-child(2) { - grid-row: 2; - grid-column: 1; - border-right: solid 2px white; - } - - .item:nth-child(3) { - grid-row: 3; - grid-column: 1; - border-right: solid 2px white; - } - - .item:nth-child(4) { - grid-row: 4; - grid-column: 1; - border-right: solid 2px white; - } - - .item:nth-child(5) { - grid-row: 5; - grid-column: 1; - border-right: solid 2px white; - } - - .item:nth-child(n) { - border-bottom: solid 2px white; - } } \ No newline at end of file diff --git a/src/pages/priceGuard.tsx b/src/pages/priceGuard.tsx index 4bdfbc7b..3d5869e8 100644 --- a/src/pages/priceGuard.tsx +++ b/src/pages/priceGuard.tsx @@ -10,17 +10,23 @@ const PriceGuard = () => { return ( - Price Guard | Carmine Options AMM + Price Protect | Carmine Options AMM -

Price Guard

+

Price Protect

- Choose how much of your STRK holdings you'd like to protect, set your - safety threshold, and select a duration for your coverage. + Safeguard your holdings from major price movement.{" "} + + Learn more +

@@ -33,13 +39,6 @@ const PriceGuard = () => {
-

- Note: This feature is designed to help manage the risk of STRK price - volatility by allowing you to set a protective value. It offers a way to - safeguard your holdings from significant declines but does not eliminate - all risks or guarantee against losses. This is not an insurance product, - but a tool for managing potential downside exposure. -

); }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index e9e782d2..2ecbf5e3 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -54,16 +54,24 @@ export const timestampToShortTimeDate = (ts: number): string => day: "numeric", }).format(ts); -export const timestampToPriceGuardDate = (ts: number): string => - new Intl.DateTimeFormat("en-US", { - weekday: "short", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", +export const timestampToPriceGuardDate = (ts: number): [string, string] => { + const date = new Date(ts * 1000); + + const day = date.getDate(); + const month = date.toLocaleString("en-US", { month: "short" }); + const year = date.getFullYear(); + + const formattedDate = `${day} ${month}, ${year}`; + + // Format the time as "2:30 PM" + const formattedTime = date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "numeric", hour12: true, - }).format(ts); + }); + + return [formattedDate, formattedTime]; +}; export const timestampToDateAndTime = (ts: number): [string, string] => { const date = new Intl.DateTimeFormat("default", { From df0947276ccd4bf91e9d41cb8641baed674c8c77 Mon Sep 17 00:00:00 2001 From: DaveVodrazka Date: Wed, 14 Aug 2024 18:05:54 +0200 Subject: [PATCH 2/3] feat: add my price protect --- src/App.tsx | 4 +- .../{BuyPriceGuardBox.tsx => PriceGuard.tsx} | 93 +---------- src/components/PriceGuard/UserPriceGuard.tsx | 152 ++++++++++++++++++ src/components/PriceGuard/index.ts | 4 + .../PriceGuard/priceguard.module.css | 2 +- .../PriceGuard/user_priceguard.module.css | 117 ++++++++++++++ src/pages/priceGuard.tsx | 22 +-- 7 files changed, 282 insertions(+), 112 deletions(-) rename src/components/PriceGuard/{BuyPriceGuardBox.tsx => PriceGuard.tsx} (73%) create mode 100644 src/components/PriceGuard/UserPriceGuard.tsx create mode 100644 src/components/PriceGuard/index.ts create mode 100644 src/components/PriceGuard/user_priceguard.module.css diff --git a/src/App.tsx b/src/App.tsx index 17401232..4a5fff5c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,7 @@ import StakingExplainedPage from "./pages/stakeInfo"; import LeaderboardPage from "./pages/leaderboard"; import StarknetRewards from "./pages/starknetRewards"; import BattlechartsPage from "./pages/battlecharts"; -import PriceGuard from "./pages/priceGuard"; +import PriceGuardPage from "./pages/priceGuard"; import "./style/base.css"; @@ -73,7 +73,7 @@ const App = () => { } /> } /> } /> - } /> + } /> } /> diff --git a/src/components/PriceGuard/BuyPriceGuardBox.tsx b/src/components/PriceGuard/PriceGuard.tsx similarity index 73% rename from src/components/PriceGuard/BuyPriceGuardBox.tsx rename to src/components/PriceGuard/PriceGuard.tsx index a64b6a58..4b92c4b5 100644 --- a/src/components/PriceGuard/BuyPriceGuardBox.tsx +++ b/src/components/PriceGuard/PriceGuard.tsx @@ -23,7 +23,6 @@ import { ToastType } from "../../redux/reducers/ui"; import { Option } from "../../classes/Option"; import { useTxPending } from "../../hooks/useRecentTxs"; import { TransactionAction } from "../../redux/reducers/transactions"; -import buttonStyles from "../../style/button.module.css"; import { Token, TokenKey } from "../../classes/Token"; import { getPremia } from "../../calls/getPremia"; import { math64toDecimal } from "../../utils/units"; @@ -76,7 +75,7 @@ const InfoIcon = ({ msg }: { msg: string }) => { ); }; -export const BuyPriceGuardBox = () => { +export const PriceGuard = () => { const account = useAccount(); const [currency, setCurrency] = useState(TokenKey.STRK); const token = Token.byKey(currency); @@ -342,94 +341,4 @@ export const BuyPriceGuardBox = () => {
); - - // return ( - //
- //
- //
- // Asset to protect - // - //
- //
- //
- //
- // Amount to protect - // - //
- //
- //
- //
- // Price to secure - // - //
- //
- //
- //
- // Duration/Until - // - //
- //
- //
- //
- // Total coverage price - // - //
- //
- //
- // - //
- //
- //
- // - // {displayBalance && ( - // - // Available: {displayBalance} {token.symbol}{" "} - // - // - // )} - // {displayBalance === undefined && Loading...} - //
- //
- //
- // - //
- //
- // - //
- //
- //
- // {priceLoading || price === undefined - // ? "Loading..." - // : `$${price.toFixed(2)}`} - // {account === undefined && ( - // - // )} - // {!priceLoading && account && ( - // - // )} - //
- //
- //
- // ); }; diff --git a/src/components/PriceGuard/UserPriceGuard.tsx b/src/components/PriceGuard/UserPriceGuard.tsx new file mode 100644 index 00000000..4038d882 --- /dev/null +++ b/src/components/PriceGuard/UserPriceGuard.tsx @@ -0,0 +1,152 @@ +import { LoadingAnimation } from "../Loading/Loading"; +import { useQuery } from "react-query"; +import { QueryKeys } from "../../queries/keys"; +import { useAccount } from "../../hooks/useAccount"; +import { AccountInterface } from "starknet"; +import { fetchPositions } from "../PositionTable/fetchPositions"; +import { OptionWithPosition } from "../../classes/Option"; +import { openCloseOptionDialog, setCloseOption } from "../../redux/actions"; +import { useTxPending } from "../../hooks/useRecentTxs"; +import { TransactionAction } from "../../redux/reducers/transactions"; +import styles from "./user_priceguard.module.css"; +import { openWalletConnectDialog } from "../ConnectWallet/Button"; +import { ReactNode, useState } from "react"; +import { TokenKey } from "../../classes/Token"; +import { timestampToPriceGuardDate } from "../../utils/utils"; + +const PriceGuardDisplay = ({ option }: { option: OptionWithPosition }) => { + const txPending = useTxPending(option.optionId, TransactionAction.TradeClose); + const symbol = option.baseToken.symbol; + const [date, time] = timestampToPriceGuardDate(option.maturity); + const status = option.isFresh + ? "active" + : option.isInTheMoney + ? "claimable" + : "expired"; + + const handleButtonClick = () => { + setCloseOption(option); + openCloseOptionDialog(); + }; + return ( +
+
{symbol}
+
+ {option.size} {symbol} +
+
${option.strike}
+
+
+ {date} + {time} +
+
+
{status}
+
+ {status === "claimable" && ( + + )} +
+
+ ); +}; + +const WithAccount = ({ account }: { account: AccountInterface }) => { + const { isLoading, isError, data } = useQuery( + [QueryKeys.position, account.address], + fetchPositions + ); + const [asset, setAsset] = useState("all"); + + const Header = ({ children }: { children: ReactNode }) => { + return ( +
+

My Price Protect

+
+ + + + +
+
+
asset
+
amount
+
price secured
+
duration
+
status
+
+
+ {children} +
+ ); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError || !data) { + return ( +
+

Something went wrong, please try again

+
+ ); + } + + const priceGuard = data.filter((o) => o.isPut && o.isLong); + + const currentChoice = + asset === "all" + ? priceGuard + : priceGuard.filter((o) => o.baseToken.id === asset); + + return ( +
+
+ {currentChoice.map((o, i) => ( + + ))} +
+
+ ); +}; + +export const UserPriceGuard = () => { + const account = useAccount(); + + if (!account) { + return ( +
+

My Price Protect

+

Connect your wallet to voew active & claimable Price Protections

+ +
+ ); + } + + return ; +}; diff --git a/src/components/PriceGuard/index.ts b/src/components/PriceGuard/index.ts new file mode 100644 index 00000000..7113d275 --- /dev/null +++ b/src/components/PriceGuard/index.ts @@ -0,0 +1,4 @@ +import { UserPriceGuard } from "./UserPriceGuard"; +import { PriceGuard } from "./PriceGuard"; + +export { PriceGuard, UserPriceGuard }; diff --git a/src/components/PriceGuard/priceguard.module.css b/src/components/PriceGuard/priceguard.module.css index 7ff3bdd1..f95515aa 100644 --- a/src/components/PriceGuard/priceguard.module.css +++ b/src/components/PriceGuard/priceguard.module.css @@ -2,7 +2,7 @@ display: flex; flex-flow: column; gap: 18px; - max-width: 812px; + max-width: 860px; font-family: "IBM Plex Sans Condensed", sans; font-size: 18px; font-weight: 500; diff --git a/src/components/PriceGuard/user_priceguard.module.css b/src/components/PriceGuard/user_priceguard.module.css new file mode 100644 index 00000000..7b01b94e --- /dev/null +++ b/src/components/PriceGuard/user_priceguard.module.css @@ -0,0 +1,117 @@ +.wrapper { + display: flex; + flex-flow: column; + gap: 12px; + font-family: "IBM Plex Sans Condensed", sans; + font-size: 18px; + font-weight: 500; + line-height: 23.4px; + max-width: 860px; + margin: 50px auto; +} + +.wrapper button { + transition: 0s; + border-color: var(--LIGHT-GREY); + color: var(--LIGHT-GREY); + height: 25px; + padding: 3px 18px; + min-width: 0; +} + +.wrapper button.active { + background: var(--GOLD); + color: black; + border-color: var(--GOLD); +} + +.wrapper button span { + border-color: var(--LIGHT-GREY); + color: var(--LIGHT-GREY); +} + +.wrapper button.active span { + background: var(--GOLD); + color: black; +} + +.buttons { + display: flex; + gap: 8px; + margin: 8px 0; +} + +.tableheader { + display: flex; + justify-content: space-between; + margin: 8px 0; + padding: 4px 0; + border-top: 1px solid var(--LIGHT-GREY); + border-bottom: 1px solid var(--LIGHT-GREY); + text-align: left; +} + +.tableheader>div { + flex: 1; + box-sizing: border-box; + color: var(--LIGHT-GREY); + font-size: 12px; + text-transform: uppercase; +} + +.tablecontent { + display: flex; + flex-flow: column; + gap: 8px; +} + +.tableitem { + display: flex; + justify-content: space-between; + margin: 8px 0; + padding: 4px 0; + font-size: 15px; + font-weight: 500; + text-align: left; +} + +.tableitem>div { + flex: 1; + text-transform: uppercase; +} + +.datetime>div { + display: flex; + align-items: baseline; + gap: 4px; + letter-spacing: -1px; +} + +.datetime>div>span { + font-size: 11px; +} + +.datetime>div>span:first-child { + font-size: 15px; +} + +.active { + line-height: 18px; + letter-spacing: 0.04em; + text-align: left; + color: #37CB4F; +} + +.claimable { + line-height: 18px; + letter-spacing: 0.04em; + text-align: left; + color: #EAC12F; +} + +.expired { + line-height: 18px; + letter-spacing: 0.04em; + text-align: left; + color: #CB3737; +} \ No newline at end of file diff --git a/src/pages/priceGuard.tsx b/src/pages/priceGuard.tsx index 3d5869e8..12dc71bb 100644 --- a/src/pages/priceGuard.tsx +++ b/src/pages/priceGuard.tsx @@ -1,12 +1,9 @@ import { Helmet } from "react-helmet"; import { Layout } from "../components/Layout"; -import { BuyPriceGuardBox } from "../components/PriceGuard/BuyPriceGuardBox"; -import { ActivePriceGuard } from "../components/PriceGuard/ActivePriceGuard"; -import { ClaimPriceGuard } from "../components/PriceGuard/ClaimPriceGuard"; -import styles from "./priceGuard.module.css"; import { CrmBanner } from "../components/Banner"; +import { UserPriceGuard, PriceGuard } from "../components/PriceGuard"; -const PriceGuard = () => { +const PriceGuardPage = () => { return ( @@ -28,19 +25,10 @@ const PriceGuard = () => { Learn more

- -
-
-

Active Price Guard

- -
-
-

Claimable Price Guard

- -
-
+ +
); }; -export default PriceGuard; +export default PriceGuardPage; From cd57b4bf653619822af87d82eeeaab01301d5cb3 Mon Sep 17 00:00:00 2001 From: DaveVodrazka Date: Thu, 15 Aug 2024 19:43:51 +0200 Subject: [PATCH 3/3] feat: handle price protect tx state --- src/calls/tradeOpen.ts | 93 ++++++++- src/components/MultiDialog/MultiDialog.tsx | 6 - .../PriceGuard/ActivePriceGuard.tsx | 111 ---------- .../PriceGuard/BuyPriceGuardModal.tsx | 190 ------------------ src/components/PriceGuard/ClaimPriceGuard.tsx | 113 ----------- src/components/PriceGuard/PriceGuard.tsx | 129 +++++++----- src/components/PriceGuard/UserPriceGuard.tsx | 55 ++++- .../PriceGuard/priceguard.module.css | 32 ++- .../PriceGuard/user_priceguard.module.css | 9 + src/redux/reducers/transactions.ts | 1 + 10 files changed, 250 insertions(+), 489 deletions(-) delete mode 100644 src/components/PriceGuard/ActivePriceGuard.tsx delete mode 100644 src/components/PriceGuard/BuyPriceGuardModal.tsx delete mode 100644 src/components/PriceGuard/ClaimPriceGuard.tsx diff --git a/src/calls/tradeOpen.ts b/src/calls/tradeOpen.ts index 5eaba213..e8edfa57 100644 --- a/src/calls/tradeOpen.ts +++ b/src/calls/tradeOpen.ts @@ -8,13 +8,11 @@ import { AccountInterface } from "starknet"; import { Option } from "../classes/Option"; import { debug } from "../utils/debugger"; import { getToApprove, shortInteger } from "../utils/computations"; -import AmmAbi from "../abi/amm_abi.json"; -import LpAbi from "../abi/lptoken_abi.json"; import { afterTransaction } from "../utils/blockchain"; import { invalidatePositions } from "../queries/client"; import { TransactionAction } from "../redux/reducers/transactions"; import { ToastType } from "../redux/reducers/ui"; -import { math64ToInt } from "../utils/units"; +import { math64toDecimal, math64ToInt } from "../utils/units"; import { apiUrl } from "../api"; import { isMainnet } from "../constants/amm"; @@ -126,3 +124,92 @@ export const approveAndTradeOpen = async ( return true; }; + +export const approveAndTradeOpenNew = async ( + account: AccountInterface, + option: Option, + size: number, + premiaMath64: bigint, + balance: bigint, + updateTradeState: ( + state: "initial" | "processing" | "fail" | "success" + ) => void, + isPriceGuard = false +) => { + if (size === 0) { + showToast("Cannot open position with size 0", ToastType.Warn); + return; + } + updateTradeState("processing"); + const premiaNum = math64toDecimal(premiaMath64); + const premiaTokenCount = math64ToInt(premiaMath64, option.digits); + const toApprove = getToApprove(option, size, BigInt(premiaTokenCount)); + const toApproveNumber = shortInteger(toApprove, option.digits); + + if (balance < toApprove) { + const [has, needs] = [ + shortInteger(balance.toString(10), option.digits), + toApproveNumber, + ]; + showToast( + `To open this position you need ${option.symbol}\u00A0${Number( + needs + ).toFixed(4)}, but you only have ${option.symbol}\u00A0${has.toFixed(4)}`, + ToastType.Warn + ); + updateTradeState("fail"); + return; + } + + const approve = option.underlying.approveCalldata(toApprove); + const tradeOpen = option.tradeOpenCalldata(size, premiaMath64); + + option.sendBeginCheckoutEvent(size, premiaNum, isPriceGuard); + const res = await account.execute([approve, tradeOpen]).catch((e) => { + debug("Trade open rejected or failed", e.message); + }); + + if (res === undefined) { + updateTradeState("fail"); + return; + } + + option.sendPurchaseEvent(size, premiaNum, isPriceGuard); + + if (isPriceGuard && isMainnet) { + const options = { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user_address: account.address, + calldata: tradeOpen.calldata, + }), + }; + + fetch(apiUrl("priceGuard-event"), options) + .then((response) => { + debug("PriceGuard event sent", response); + }) + .catch((err) => { + debug("PriceGuard event failed", err); + console.error(err); + }); + } + + const hash = res.transaction_hash; + addTx(hash, option.optionId, TransactionAction.TradeOpen); + afterTransaction( + hash, + () => { + markTxAsDone(hash); + invalidatePositions(); + updateTradeState("success"); + showToast("Successfully opened position", ToastType.Success); + }, + () => { + markTxAsFailed(hash); + updateTradeState("fail"); + showToast("Failed to open position", ToastType.Error); + } + ); +}; diff --git a/src/components/MultiDialog/MultiDialog.tsx b/src/components/MultiDialog/MultiDialog.tsx index 9d02b24b..ef8f69df 100644 --- a/src/components/MultiDialog/MultiDialog.tsx +++ b/src/components/MultiDialog/MultiDialog.tsx @@ -17,7 +17,6 @@ import { Close } from "@mui/icons-material"; import { ClosePosition } from "../ClosePosition/ClosePosition"; import { WalletInfo } from "../WalletInfo/WalletInfo"; import { ReactNode } from "react"; -import { BuyPriceGuardModal } from "../PriceGuard/BuyPriceGuardModal"; import { TransferDialog } from "../Transfer"; import { BraavosDialog } from "./BraavosDialog"; @@ -171,11 +170,6 @@ export const MultiDialog = () => { )} - {dialogContent === DialogContentElem.BuyPriceGuard && ( - - - - )} {dialogContent === DialogContentElem.Account && ( diff --git a/src/components/PriceGuard/ActivePriceGuard.tsx b/src/components/PriceGuard/ActivePriceGuard.tsx deleted file mode 100644 index ec9590f3..00000000 --- a/src/components/PriceGuard/ActivePriceGuard.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { LoadingAnimation } from "../Loading/Loading"; -import { Box, Typography } from "@mui/material"; -import { useQuery } from "react-query"; -import { QueryKeys } from "../../queries/keys"; -import { useAccount } from "../../hooks/useAccount"; -import { AccountInterface } from "starknet"; -import { fetchPositions } from "../PositionTable/fetchPositions"; -import { OptionWithPosition } from "../../classes/Option"; -import { openCloseOptionDialog, setCloseOption } from "../../redux/actions"; -import { useTxPending } from "../../hooks/useRecentTxs"; -import { TransactionAction } from "../../redux/reducers/transactions"; -import styles from "../../style/button.module.css"; - -const PriceGuardDisplay = ({ option }: { option: OptionWithPosition }) => { - const txPending = useTxPending(option.optionId, TransactionAction.TradeClose); - const symbol = option.baseToken.symbol; - const handleButtonClick = () => { - setCloseOption(option); - openCloseOptionDialog(); - }; - return ( - - - {symbol} ${option.strike} - - - - Price Guard covers {option.size} {symbol} at price ${option.strike}{" "} - and expires {option.dateRich} - - - - - ); -}; - -const WithAccount = ({ account }: { account: AccountInterface }) => { - const { isLoading, isError, data } = useQuery( - [QueryKeys.position, account.address], - fetchPositions - ); - - if (isLoading) { - return ; - } - - if (isError || !data) { - return ( - - We are experiencing difficulties fetching your data. Please try again - later. - - ); - } - - const priceGuard = data.filter((o) => o.isPut && o.isLong && o.isFresh); - - if (priceGuard.length === 0) { - // no options for the given currency - return ( - - - You currently do not have any active Price Guard - - - ); - } - - return ( - - - Your Carmine portfolio consists of {priceGuard.length} Long Put option - {priceGuard.length > 1 ? "s" : ""} which insures these crypto assets - - {priceGuard.map((o, i) => ( - - ))} - - ); -}; - -export const ActivePriceGuard = () => { - const account = useAccount(); - - if (!account) { - return ( - Connect wallet to see your active Price Guard - ); - } - - return ; -}; diff --git a/src/components/PriceGuard/BuyPriceGuardModal.tsx b/src/components/PriceGuard/BuyPriceGuardModal.tsx deleted file mode 100644 index a87ce982..00000000 --- a/src/components/PriceGuard/BuyPriceGuardModal.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { Box, Tooltip, Typography } from "@mui/material"; -import { usePremiaQuery } from "../../hooks/usePremiaQuery"; -import { CustomDialogTitle } from "../MultiDialog/MultiDialog"; -import { math64toDecimal } from "../../utils/units"; -import { getPremiaWithSlippage } from "../../utils/computations"; -import { useAccount } from "../../hooks/useAccount"; -import { store } from "../../redux/store"; -import { Option } from "../../classes/Option"; -import { useBuyPriceGuardData } from "../../hooks/useBuyPriceGuardData"; -import { LoadingAnimation } from "../Loading/Loading"; -import { useState } from "react"; -import { TradeState } from "../TradeTable/TradeCard"; -import { approveAndTradeOpen } from "../../calls/tradeOpen"; -import { useUserBalance } from "../../hooks/useUserBalance"; -import buttonStyles from "../../style/button.module.css"; - -export type BuyPriceGuardModalData = { - option: Option; - size: number; -}; - -type Props = { - option: Option; - size: number; - updateTradeState: ({ - failed, - processing, - }: { - failed: boolean; - processing: boolean; - }) => void; -}; - -const WithOption = ({ option, size, updateTradeState }: Props) => { - const account = useAccount(); - const balance = useUserBalance(option.underlying.address); - - const { - data: premiaMath64, - error, - isFetching, - } = usePremiaQuery(option, size, false); - - if (isFetching || !balance) { - // loading... - return ; - } - - if (typeof premiaMath64 === "undefined" || error) { - // no data - return ( - - Something went wrong while fetching data, please try again - - ); - } - - const premiaNumber = math64toDecimal(premiaMath64); - const premiaWithSlippage = getPremiaWithSlippage( - premiaMath64, - option.side, - false - ); - const displayPremiaWithSlippage = math64toDecimal(premiaWithSlippage); - const slippage = store.getState().settings.slippage; - - const handleClick = () => - approveAndTradeOpen( - account!, - option, - size, - premiaNumber, - premiaWithSlippage, - balance, - updateTradeState, - true - ); - - return ( - - - - PriceGuard price - - - ${premiaNumber.toFixed(2)} - - - - - - Slippage {slippage}% limit - - - - ${displayPremiaWithSlippage.toFixed(2)} - - - - - - - ); -}; - -export const BuyPriceGuardModal = () => { - const data = useBuyPriceGuardData(); - const [tradeState, updateTradeState] = useState({ - failed: false, - processing: false, - }); - - const containerSx = { minWidth: "300px", p: 2 }; - - if (!data) { - return ( - - - There was a problem, please try again - - ); - } - - if (tradeState.failed) { - return ( - - - Transaction failed, please try again - - ); - } - - if (tradeState.processing) { - return ( - - - - Transaction is being processed, you can close this modal. - - - You can find the transaction in Recent Transactions by clicking - wallet button in the top right corner. - - - ); - } - - const { option, size } = data; - - const title = `Buy Price Guard for ${size} ${option.baseToken.symbol}`; - - return ( - - - Price Guard will expire on {option.dateRich} - - - ); -}; diff --git a/src/components/PriceGuard/ClaimPriceGuard.tsx b/src/components/PriceGuard/ClaimPriceGuard.tsx deleted file mode 100644 index f9569dfd..00000000 --- a/src/components/PriceGuard/ClaimPriceGuard.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { LoadingAnimation } from "../Loading/Loading"; -import { Box, Typography } from "@mui/material"; -import { useQuery } from "react-query"; -import { QueryKeys } from "../../queries/keys"; -import { useAccount } from "../../hooks/useAccount"; -import { AccountInterface } from "starknet"; -import { fetchPositions } from "../PositionTable/fetchPositions"; -import { OptionWithPosition } from "../../classes/Option"; -import { tradeSettle } from "../../calls/tradeSettle"; -import { useTxPending } from "../../hooks/useRecentTxs"; -import { TransactionAction } from "../../redux/reducers/transactions"; -import styles from "../../style/button.module.css"; - -const ClaimItem = ({ - option, - account, -}: { - option: OptionWithPosition; - account: AccountInterface; -}) => { - const txPending = useTxPending(option.optionId, TransactionAction.Settle); - const symbol = option.baseToken.symbol; - - const handleButtonClick = () => tradeSettle(account, option); - - return ( - - - {symbol} ${option.strike} - - - - You are eligible to claim ${option.value.toFixed(4)} - - - - - ); -}; - -const WithAccount = ({ account }: { account: AccountInterface }) => { - const { isLoading, isError, data } = useQuery( - [QueryKeys.position, account.address], - fetchPositions - ); - - if (isLoading) { - return ; - } - - if (isError || !data) { - return ( - - We are experiencing difficulties fetching your data. Please try again - later. - - ); - } - - const priceGuard = data.filter((o) => o.isPut && o.isLong && o.isInTheMoney); - - if (priceGuard.length === 0) { - // no options for the given currency - return ( - - - You currently do not have any claimable Price Guard - - - ); - } - - return ( - - - You have {priceGuard.length} claimable Price Guard event - {priceGuard.length > 1 ? "s" : ""}: - - {priceGuard.map((o, i) => ( - - ))} - - ); -}; - -export const ClaimPriceGuard = () => { - const account = useAccount(); - - if (!account) { - return Connect wallet to see claimable Price Guard; - } - - return ; -}; diff --git a/src/components/PriceGuard/PriceGuard.tsx b/src/components/PriceGuard/PriceGuard.tsx index 4b92c4b5..afefacd8 100644 --- a/src/components/PriceGuard/PriceGuard.tsx +++ b/src/components/PriceGuard/PriceGuard.tsx @@ -12,17 +12,7 @@ import { timestampToPriceGuardDate, uniquePrimitiveValues, } from "../../utils/utils"; -import { debug } from "../../utils/debugger"; import { handleNumericChangeFactory } from "../../utils/inputHandling"; -import { - openBuyPriceGuardDialog, - setBuyPriceGuardModal, - showToast, -} from "../../redux/actions"; -import { ToastType } from "../../redux/reducers/ui"; -import { Option } from "../../classes/Option"; -import { useTxPending } from "../../hooks/useRecentTxs"; -import { TransactionAction } from "../../redux/reducers/transactions"; import { Token, TokenKey } from "../../classes/Token"; import { getPremia } from "../../calls/getPremia"; import { math64toDecimal } from "../../utils/units"; @@ -31,39 +21,7 @@ import { Info } from "@mui/icons-material"; import styles from "./priceguard.module.css"; import { Tooltip } from "@mui/material"; - -type BuyButtonProps = { - option: Option; - size: number; -}; - -const BuyPriceGuardButton = ({ option, size }: BuyButtonProps) => { - const txPending = useTxPending(option.optionId, TransactionAction.TradeOpen); - const handleButtonClick = () => { - if (size === 0) { - showToast("Please select size greater than 0", ToastType.Warn); - return; - } - if (!option) { - showToast("Select a priceGuard first", ToastType.Warn); - return; - } - debug("Buying this option", option, "with size", size); - setBuyPriceGuardModal({ option: option, size }); - option.sendViewEvent(true); - openBuyPriceGuardDialog(); - }; - - return ( - - ); -}; +import { approveAndTradeOpenNew } from "../../calls/tradeOpen"; const InfoIcon = ({ msg }: { msg: string }) => { return ( @@ -80,9 +38,10 @@ export const PriceGuard = () => { const [currency, setCurrency] = useState(TokenKey.STRK); const token = Token.byKey(currency); const balance = useUserBalance(token.address); - const displayBalance = balance - ? shortInteger(balance.toString(10), token.decimals).toFixed(4) - : undefined; + const displayBalance = + balance === undefined + ? undefined + : shortInteger(balance.toString(10), token.decimals).toFixed(4); const valueInUsd = useCurrency(currency); const { isLoading, isError, data } = useQuery( QueryKeys.options, @@ -92,9 +51,12 @@ export const PriceGuard = () => { const [size, setSize] = useState(0); const [textSize, setTextSize] = useState(""); const [expiry, setExpiry] = useState(); - const [price, setPrice] = useState(); + const [priceMath64, setPrice] = useState(); const [priceLoading, setPriceLoading] = useState(false); + const price = + priceMath64 === undefined ? undefined : math64toDecimal(priceMath64); + // eslint-disable-next-line react-hooks/exhaustive-deps const callWithDelay = useCallback( debounce((size: number, controller: AbortController) => { @@ -128,7 +90,7 @@ export const PriceGuard = () => { return; } console.log("PAST CONTROLLER PRICE GUARD", res); - setPrice(math64toDecimal(res as bigint)); + setPrice(res as bigint); setPriceLoading(false); }) .catch(() => { @@ -238,6 +200,56 @@ export const PriceGuard = () => { (o) => o.maturity === expiry && o.strike === currentStrike )!; + const BuyPriceGuardButton = () => { + const [tradeState, updateTradeState] = useState< + "initial" | "processing" | "fail" | "success" + >("initial"); + const handleButtonClick = () => { + if ( + !account || + priceMath64 === undefined || + price === undefined || + balance === undefined + ) { + return; + } + const premiaWithSlippage = (priceMath64 * 105n) / 100n; // TODO: slippage 5%, change it to use argument + + approveAndTradeOpenNew( + account, + pickedOption, + size, + premiaWithSlippage, + balance, + updateTradeState, + true + ); + }; + + const className = `${styles.buybutton} ${styles[tradeState]}`; + const disabled = tradeState === "processing"; + const content = + tradeState === "initial" ? ( + `Protect my ${pickedOption.baseToken.symbol}` + ) : tradeState === "processing" ? ( + + ) : tradeState === "fail" ? ( + "Failed" + ) : ( + "Success!" + ); + + return ( + + ); + }; + return (
@@ -264,11 +276,16 @@ export const PriceGuard = () => {
-
+
balance - {displayBalance === undefined ? "---" : displayBalance}{" "} - {token.symbol} + {displayBalance === undefined ? ( +
+ {token.symbol} +
+ ) : ( + `${displayBalance} ${token.symbol}` + )}
@@ -322,7 +339,11 @@ export const PriceGuard = () => {
Final coverage price - {priceLoading || price === undefined ? "---" : "$" + price.toFixed(3)} + {priceLoading || price === undefined ? ( + + ) : ( + "$" + price.toFixed(3) + )}
@@ -333,10 +354,10 @@ export const PriceGuard = () => { > Connect wallet - ) : price === undefined ? ( + ) : price === undefined || priceMath64 === undefined ? ( ) : ( - + )}
diff --git a/src/components/PriceGuard/UserPriceGuard.tsx b/src/components/PriceGuard/UserPriceGuard.tsx index 4038d882..46cfde81 100644 --- a/src/components/PriceGuard/UserPriceGuard.tsx +++ b/src/components/PriceGuard/UserPriceGuard.tsx @@ -5,7 +5,7 @@ import { useAccount } from "../../hooks/useAccount"; import { AccountInterface } from "starknet"; import { fetchPositions } from "../PositionTable/fetchPositions"; import { OptionWithPosition } from "../../classes/Option"; -import { openCloseOptionDialog, setCloseOption } from "../../redux/actions"; +import { showToast } from "../../redux/actions"; import { useTxPending } from "../../hooks/useRecentTxs"; import { TransactionAction } from "../../redux/reducers/transactions"; import styles from "./user_priceguard.module.css"; @@ -13,9 +13,22 @@ import { openWalletConnectDialog } from "../ConnectWallet/Button"; import { ReactNode, useState } from "react"; import { TokenKey } from "../../classes/Token"; import { timestampToPriceGuardDate } from "../../utils/utils"; +import { afterTransaction } from "../../utils/blockchain"; +import { invalidatePositions } from "../../queries/client"; +import { ToastType } from "../../redux/reducers/ui"; -const PriceGuardDisplay = ({ option }: { option: OptionWithPosition }) => { - const txPending = useTxPending(option.optionId, TransactionAction.TradeClose); +const PriceGuardDisplay = ({ + option, + account, +}: { + option: OptionWithPosition; + account: AccountInterface; +}) => { + const txPending = useTxPending( + option.optionId, + TransactionAction.TradeSettle + ); + const [_settling, setSettling] = useState(false); const symbol = option.baseToken.symbol; const [date, time] = timestampToPriceGuardDate(option.maturity); const status = option.isFresh @@ -24,9 +37,25 @@ const PriceGuardDisplay = ({ option }: { option: OptionWithPosition }) => { ? "claimable" : "expired"; + const settling = txPending || _settling; + const handleButtonClick = () => { - setCloseOption(option); - openCloseOptionDialog(); + setSettling(true); + account + .execute(option.tradeSettleCalldata) + .then((res) => { + if (res?.transaction_hash) { + afterTransaction(res.transaction_hash, () => { + invalidatePositions(); + showToast("Successfully claimed Price Protect", ToastType.Success); + setSettling(false); + }); + } + }) + .catch(() => { + showToast("Failed claiming Price Protect", ToastType.Error); + setSettling(false); + }); }; return (
@@ -44,7 +73,19 @@ const PriceGuardDisplay = ({ option }: { option: OptionWithPosition }) => {
{status}
{status === "claimable" && ( - + )}
@@ -128,7 +169,7 @@ const WithAccount = ({ account }: { account: AccountInterface }) => {
{currentChoice.map((o, i) => ( - + ))}
diff --git a/src/components/PriceGuard/priceguard.module.css b/src/components/PriceGuard/priceguard.module.css index f95515aa..097cef61 100644 --- a/src/components/PriceGuard/priceguard.module.css +++ b/src/components/PriceGuard/priceguard.module.css @@ -12,10 +12,11 @@ .container button { transition: 0s; - border-color: var(--LIGHT-GREY); + border: 0.75px solid #727272; + border-radius: 3px; color: var(--LIGHT-GREY); - height: 25px; - padding: 3px 18px; + height: 29px; + padding: 5px 18px; min-width: 0; } @@ -77,6 +78,7 @@ justify-content: flex-end; align-items: baseline; gap: 8px; + cursor: pointer; } .balance span { @@ -89,6 +91,13 @@ color: var(--LIGHT-GREY); } +.balanceloading { + display: flex; + align-items: center; + flex-flow: row; + gap: 10px; +} + .datetime>div { display: flex; align-items: baseline; @@ -97,6 +106,7 @@ .datetime>div>span { font-size: 15px; + text-transform: uppercase; } .datetime>div>span:first-child { @@ -105,7 +115,7 @@ .divider { height: 1px; - border-top: 1px solid var(--LIGHT-GREY); + border-top: 1px solid #727272; } .coverage { @@ -121,6 +131,10 @@ button.buybutton { background: var(--GOLD); border: none; color: black; + font-family: IBM Plex Sans Condensed; + font-size: 15px; + font-weight: 500; + line-height: 19.5px; } .column { @@ -192,7 +206,7 @@ button.buybutton { background: none; border: 2px solid var(--GOLD); font-size: 19px; - padding: 5px; + padding: 5px 12px; min-width: 100px; width: auto; } @@ -203,4 +217,12 @@ button.buybutton { .container input:focus { outline: 1px solid var(--GOLD); +} + +button.success { + background-color: #37CB4F; +} + +button.fail { + background-color: #CB3737; } \ No newline at end of file diff --git a/src/components/PriceGuard/user_priceguard.module.css b/src/components/PriceGuard/user_priceguard.module.css index 7b01b94e..0f80feac 100644 --- a/src/components/PriceGuard/user_priceguard.module.css +++ b/src/components/PriceGuard/user_priceguard.module.css @@ -41,6 +41,11 @@ margin: 8px 0; } +.buttons button { + font-size: 12px; + font-weight: 500; +} + .tableheader { display: flex; justify-content: space-between; @@ -114,4 +119,8 @@ letter-spacing: 0.04em; text-align: left; color: #CB3737; +} + +.loading { + width: 5ch; } \ No newline at end of file diff --git a/src/redux/reducers/transactions.ts b/src/redux/reducers/transactions.ts index 5b540ac7..5df2c13b 100644 --- a/src/redux/reducers/transactions.ts +++ b/src/redux/reducers/transactions.ts @@ -4,6 +4,7 @@ import { constants } from "starknet"; export enum TransactionAction { TradeOpen = "TradeOpen", TradeClose = "TradeClose", + TradeSettle = "TradeSettle", Stake = "Stake", Withdraw = "Withdraw", Settle = "Settle",