diff --git a/src/components/CollateralSelector/index.tsx b/src/components/CollateralSelector/index.tsx index 64d0f2f..2a727bc 100644 --- a/src/components/CollateralSelector/index.tsx +++ b/src/components/CollateralSelector/index.tsx @@ -1,11 +1,11 @@ -import React from "react"; -import { useLendingReserves } from "../../hooks"; -import { LendingMarket, LendingReserve } from "../../models"; -import { TokenIcon } from "../TokenIcon"; -import { getTokenName } from "../../utils/utils"; -import { Select } from "antd"; -import { useConnectionConfig } from "../../contexts/connection"; -import { cache, ParsedAccount } from "../../contexts/accounts"; +import React from 'react'; +import { useLendingReserves } from '../../hooks'; +import { LendingMarket, LendingReserve } from '../../models'; +import { TokenIcon } from '../TokenIcon'; +import { getTokenName } from '../../utils/utils'; +import { Select } from 'antd'; +import { useConnectionConfig } from '../../contexts/connection'; +import { cache, ParsedAccount } from '../../contexts/accounts'; const { Option } = Select; @@ -18,19 +18,17 @@ export const CollateralSelector = (props: { const { reserveAccounts } = useLendingReserves(); const { tokenMap } = useConnectionConfig(); - const market = cache.get(props.reserve.lendingMarket) as ParsedAccount< - LendingMarket - >; - const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals( - market?.info?.quoteMint - ); + const market = cache.get(props.reserve.lendingMarket) as ParsedAccount; + if (!market) return null; + + const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(market?.info?.quoteMint); return ( - setItem({ ...item, [assetKey]: { ...(item[assetKey] || {}), type: tokens.find((t) => t.mintAddress === v) } }) - } - > - {tokens.map((token) => ( - - ))} - - ); - } else { - return ( -
- { - setItem({ ...item, [assetKey]: { ...(item[assetKey] || {}), value: v } }); - }} - placeholder='0.00' - /> - -
- ); - } -} - -export default function MarginTradePosition({ item, setItem }: { item: IPosition; setItem?: (item: any) => void }) { - return ( -
-
- {setItem && ( - - )} -
-
- {setItem && ( - - )} -
-
- {setItem && ( - { - setItem({ ...item, leverage: v }); - }} - /> - )} -
-
123
-
123
-
123
-
- -
-
- ); -} diff --git a/src/views/marginTrading/index.tsx b/src/views/marginTrading/index.tsx index d9f855e..1fe2520 100644 --- a/src/views/marginTrading/index.tsx +++ b/src/views/marginTrading/index.tsx @@ -1,138 +1,26 @@ -import React, { useState } from 'react'; +import React from 'react'; import { LABELS } from '../../constants'; -import './style.less'; -import { Card, Progress, Slider, Statistic } from 'antd'; -import MarginTradePosition from './MarginTradePosition'; -import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; +import './itemStyle.less'; +import { Card } from 'antd'; +import { useLendingReserves } from '../../hooks/useLendingReserves'; +import { MarginTradeItem } from './item'; -export interface IToken { - mintAddress: string; - tokenName: string; - tokenSymbol: string; -} - -export interface IPosition { - id?: number | null; - leverage: number; - collateral?: IToken; - asset?: { - type: IToken; - value: number; - }; -} - -export function Breakdown({ item }: { item: IPosition }) { - let myPart = (item.asset?.value || 0) / item.leverage; - const brokeragePart = (item.asset?.value || 0) - myPart; - const brokerageColor = 'brown'; - const myColor = 'blue'; - const gains = 'green'; - const losses = 'red'; - - const [myGain, setMyGain] = useState(0); - const profitPart = (myPart + brokeragePart) * (myGain / 100); - let progressBar = null; - if (profitPart > 0) { - // normalize... - const total = profitPart + myPart + brokeragePart; - progressBar = ( - - ); - } else { - // now, we're eating away your profit... - myPart += profitPart; // profit is negative - const total = myPart + brokeragePart; - if (myPart < 0) { - progressBar =

Your position has been liquidated at this price swing.

; - } else - progressBar = ( - - ); - } - - return ( -
- { - setMyGain(v); - }} - style={{ marginBottom: '20px' }} - /> -
- - - - - - - - 0 ? gains : losses }} - suffix={item.asset?.type.tokenSymbol} - prefix={profitPart > 0 ? : } - /> - -
- {progressBar} -
- ); -} export const MarginTrading = () => { - const [newPosition, setNewPosition] = useState({ id: null, leverage: 1 }); - - const positions: any[] = []; + const { reserveAccounts } = useLendingReserves(); return ( -
-
- -
-
{LABELS.TRADING_TABLE_TITLE_MY_COLLATERAL}
-
{LABELS.TRADING_TABLE_TITLE_DESIRED_ASSET}
-
{LABELS.TRADING_TABLE_TITLE_MULTIPLIER}
-
{LABELS.TRADING_TABLE_TITLE_ASSET_PRICE}
-
{LABELS.TRADING_TABLE_TITLE_LIQUIDATION_PRICE}
-
{LABELS.TRADING_TABLE_TITLE_APY}
-
{LABELS.TRADING_TABLE_TITLE_ACTIONS}
-
- - - {positions.map((item) => ( - - ))} -
-
+
+ +
+
{LABELS.TABLE_TITLE_ASSET}
+
Serum Dex Price
+
{LABELS.TABLE_TITLE_BUYING_POWER}
+
{LABELS.TABLE_TITLE_APY}
+
+
+ {reserveAccounts.map((account) => ( + + ))} +
); }; diff --git a/src/views/marginTrading/item.tsx b/src/views/marginTrading/item.tsx new file mode 100644 index 0000000..4e2035e --- /dev/null +++ b/src/views/marginTrading/item.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useTokenName } from '../../hooks'; +import { calculateBorrowAPY, LendingReserve } from '../../models/lending'; +import { TokenIcon } from '../../components/TokenIcon'; +import { formatNumber, formatPct } from '../../utils/utils'; +import { Button } from 'antd'; +import { Link } from 'react-router-dom'; +import { PublicKey } from '@solana/web3.js'; +import { LABELS } from '../../constants'; +import { useMidPriceInUSD } from '../../contexts/market'; + +export const MarginTradeItem = (props: { reserve: LendingReserve; address: PublicKey }) => { + const name = useTokenName(props.reserve.liquidityMint); + const price = useMidPriceInUSD(props.reserve.liquidityMint.toBase58()).price; + + const apr = calculateBorrowAPY(props.reserve); + + return ( + +
+ + + {name} + +
${formatNumber.format(price)}
+
+
+
+ {formatNumber.format(200)} {name} +
+
${formatNumber.format(300)}
+
+
+
{formatPct.format(apr)}
+
+ +
+
+ + ); +}; diff --git a/src/views/marginTrading/itemStyle.less b/src/views/marginTrading/itemStyle.less new file mode 100644 index 0000000..4d15206 --- /dev/null +++ b/src/views/marginTrading/itemStyle.less @@ -0,0 +1,32 @@ +@import '~antd/es/style/themes/default.less'; + +.choose-margin-item { + display: flex; + justify-content: space-between; + align-items: center; + color: @text-color; + + & > :nth-child(n) { + flex: 20%; + text-align: right; + margin: 10px 0px; + } + + & > :first-child { + flex: 80px; + } + + border-bottom: 1px solid #eee; +} + +.choose-margin-header { + & > div { + flex: 20%; + text-align: right; + } + + & > :first-child { + text-align: left; + flex: 80px; + } +} diff --git a/src/views/marginTrading/newPosition/Breakdown.tsx b/src/views/marginTrading/newPosition/Breakdown.tsx new file mode 100644 index 0000000..0eed231 --- /dev/null +++ b/src/views/marginTrading/newPosition/Breakdown.tsx @@ -0,0 +1,93 @@ +import { Progress, Slider, Card, Statistic } from 'antd'; +import React, { useState } from 'react'; +import { Position } from './interfaces'; +import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; + +export function Breakdown({ item }: { item: Position }) { + let myPart = (item.asset?.value || 0) / item.leverage; + const brokeragePart = (item.asset?.value || 0) - myPart; + const brokerageColor = 'brown'; + const myColor = 'blue'; + const gains = 'green'; + const losses = 'red'; + + const [myGain, setMyGain] = useState(0); + const profitPart = (myPart + brokeragePart) * (myGain / 100); + let progressBar = null; + if (profitPart > 0) { + // normalize... + const total = profitPart + myPart + brokeragePart; + progressBar = ( + + ); + } else { + // now, we're eating away your profit... + myPart += profitPart; // profit is negative + const total = myPart + brokeragePart; + if (myPart < 0) { + progressBar =

Your position has been liquidated at this price swing.

; + } else + progressBar = ( + + ); + } + + return ( +
+ { + setMyGain(v); + }} + style={{ marginBottom: '20px' }} + /> +
+ + + + + + + + 0 ? gains : losses }} + suffix={item.asset?.type?.tokenSymbol} + prefix={profitPart > 0 ? : } + /> + +
+ {progressBar} +
+ ); +} diff --git a/src/views/marginTrading/newPosition/EditableAsset.tsx b/src/views/marginTrading/newPosition/EditableAsset.tsx new file mode 100644 index 0000000..d049ba8 --- /dev/null +++ b/src/views/marginTrading/newPosition/EditableAsset.tsx @@ -0,0 +1,56 @@ +import { Select } from 'antd'; +import React from 'react'; +import { NumericInput } from '../../../components/Input/numeric'; +import { TokenIcon } from '../../../components/TokenIcon'; +import tokens from '../../../config/tokens.json'; + +const { Option } = Select; +interface EditableAssetProps { + label: string; + assetKey: string; + setItem: (item: any) => void; + item: any; +} +export default function EditableAsset({ label, assetKey, setItem, item }: EditableAssetProps) { + if (!item[assetKey]?.type) { + return ( + + ); + } else { + return ( +
+ { + setItem({ ...item, [assetKey]: { ...(item[assetKey] || {}), value: v } }); + }} + placeholder='0.00' + /> + +
+ ); + } +} diff --git a/src/views/marginTrading/newPosition/NewPositionForm.tsx b/src/views/marginTrading/newPosition/NewPositionForm.tsx new file mode 100644 index 0000000..c628c3d --- /dev/null +++ b/src/views/marginTrading/newPosition/NewPositionForm.tsx @@ -0,0 +1,108 @@ +import { Button, Card, Radio } from 'antd'; +import React, { useState } from 'react'; +import { ActionConfirmation } from '../../../components/ActionConfirmation'; +import { NumericInput } from '../../../components/Input/numeric'; +import { TokenIcon } from '../../../components/TokenIcon'; +import { LABELS } from '../../../constants'; +import { cache, ParsedAccount } from '../../../contexts/accounts'; +import { LendingReserve, LendingReserveParser } from '../../../models/lending/reserve'; +import { Position } from './interfaces'; +import tokens from '../../../config/tokens.json'; +import { CollateralSelector } from '../../../components/CollateralSelector'; +import { Breakdown } from './Breakdown'; + +interface NewPositionFormProps { + lendingReserve: ParsedAccount; + newPosition: Position; + setNewPosition: (pos: Position) => void; +} + +export default function NewPositionForm({ lendingReserve, newPosition, setNewPosition }: NewPositionFormProps) { + const bodyStyle: React.CSSProperties = { + display: 'flex', + flex: 1, + justifyContent: 'center', + alignItems: 'center', + height: '100%', + }; + const [showConfirmation, setShowConfirmation] = useState(false); + return ( + + {showConfirmation ? ( + setShowConfirmation(false)} /> + ) : ( +
+
{LABELS.SELECT_COLLATERAL}
+ { + const id: string = cache.byParser(LendingReserveParser).find((acc) => acc === key) || ''; + const parser = cache.get(id) as ParsedAccount; + const tokenMint = parser.info.collateralMint.toBase58(); + setNewPosition({ ...newPosition, collateral: tokens.find((t) => t.mintAddress === tokenMint) }); + }} + /> + +
{LABELS.MARGIN_TRADE_QUESTION}
+
+ + { + setNewPosition({ ...newPosition, asset: { ...newPosition.asset, value: v } }); + }} + placeholder='0.00' + /> +
{newPosition.asset.type?.tokenSymbol}
+
+ +
+ { + setNewPosition({ ...newPosition, leverage: e.target.value }); + }} + > + 1x + 2x + 3x + 4x + 5x + + { + setNewPosition({ ...newPosition, leverage }); + }} + /> +
+
+ +
+ +
+ )} +
+ ); +} diff --git a/src/views/marginTrading/newPosition/index.tsx b/src/views/marginTrading/newPosition/index.tsx new file mode 100644 index 0000000..dbc1669 --- /dev/null +++ b/src/views/marginTrading/newPosition/index.tsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { useLendingReserve, useTokenName } from '../../../hooks'; +import { useParams } from 'react-router-dom'; +import './style.less'; +import tokens from '../../../config/tokens.json'; + +import { SideReserveOverview, SideReserveOverviewMode } from '../../../components/SideReserveOverview'; +import NewPositionForm from './NewPositionForm'; +import { Position } from './interfaces'; +import { useEffect } from 'react'; + +export const NewPosition = () => { + const { id } = useParams<{ id: string }>(); + const lendingReserve = useLendingReserve(id); + const [newPosition, setNewPosition] = useState({ + id: null, + leverage: 1, + asset: { value: 0 }, + }); + + const assetTokenType = tokens.find((t) => t.mintAddress === lendingReserve?.info?.liquidityMint?.toBase58()); + if (!lendingReserve) { + return null; + } + + if (!assetTokenType) { + return null; + } else { + if (newPosition.asset.type != assetTokenType) { + setNewPosition({ ...newPosition, asset: { value: newPosition.asset.value, type: assetTokenType } }); + } + } + + return ( +
+
+ + +
+
+ ); +}; diff --git a/src/views/marginTrading/newPosition/interfaces.tsx b/src/views/marginTrading/newPosition/interfaces.tsx new file mode 100644 index 0000000..5acb301 --- /dev/null +++ b/src/views/marginTrading/newPosition/interfaces.tsx @@ -0,0 +1,15 @@ +export interface Token { + mintAddress: string; + tokenName: string; + tokenSymbol: string; +} + +export interface Position { + id?: number | null; + leverage: number; + collateral?: Token; + asset: { + type?: Token; + value: number; + }; +} diff --git a/src/views/marginTrading/newPosition/style.less b/src/views/marginTrading/newPosition/style.less new file mode 100644 index 0000000..3aff82b --- /dev/null +++ b/src/views/marginTrading/newPosition/style.less @@ -0,0 +1,32 @@ +.new-position { + display: flex; + flex-direction: column; + flex: 1; +} + +.new-position-item { + margin: 4px; +} + +.new-position-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + flex: 1; +} + +.new-position-item-left { + flex: 60%; +} + +.new-position-item-right { + flex: 30%; +} + +/* Responsive layout - makes a one column layout instead of a two-column layout */ +@media (max-width: 600px) { + .new-position-item-right, + .new-position-item-left { + flex: 100%; + } +}