feat: back to baseline but in a new flow. about to brign in some swap code to get leverage limits from amm.
This commit is contained in:
parent
61cf44e95a
commit
199253e52f
|
@ -1,11 +1,11 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { useLendingReserves } from "../../hooks";
|
import { useLendingReserves } from '../../hooks';
|
||||||
import { LendingMarket, LendingReserve } from "../../models";
|
import { LendingMarket, LendingReserve } from '../../models';
|
||||||
import { TokenIcon } from "../TokenIcon";
|
import { TokenIcon } from '../TokenIcon';
|
||||||
import { getTokenName } from "../../utils/utils";
|
import { getTokenName } from '../../utils/utils';
|
||||||
import { Select } from "antd";
|
import { Select } from 'antd';
|
||||||
import { useConnectionConfig } from "../../contexts/connection";
|
import { useConnectionConfig } from '../../contexts/connection';
|
||||||
import { cache, ParsedAccount } from "../../contexts/accounts";
|
import { cache, ParsedAccount } from '../../contexts/accounts';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
|
@ -18,19 +18,17 @@ export const CollateralSelector = (props: {
|
||||||
const { reserveAccounts } = useLendingReserves();
|
const { reserveAccounts } = useLendingReserves();
|
||||||
const { tokenMap } = useConnectionConfig();
|
const { tokenMap } = useConnectionConfig();
|
||||||
|
|
||||||
const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<
|
const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<LendingMarket>;
|
||||||
LendingMarket
|
if (!market) return null;
|
||||||
>;
|
|
||||||
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(
|
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(market?.info?.quoteMint);
|
||||||
market?.info?.quoteMint
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
size="large"
|
size='large'
|
||||||
showSearch
|
showSearch
|
||||||
style={{ minWidth: 300, margin: "5px 0px" }}
|
style={{ minWidth: 300, margin: '5px 0px' }}
|
||||||
placeholder="Collateral"
|
placeholder='Collateral'
|
||||||
value={props.collateralReserve}
|
value={props.collateralReserve}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
defaultValue={props.collateralReserve}
|
defaultValue={props.collateralReserve}
|
||||||
|
@ -39,27 +37,18 @@ export const CollateralSelector = (props: {
|
||||||
props.onCollateralReserve(item);
|
props.onCollateralReserve(item);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||||
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{reserveAccounts
|
{reserveAccounts
|
||||||
.filter((reserve) => reserve.info !== props.reserve)
|
.filter((reserve) => reserve.info !== props.reserve)
|
||||||
.filter(
|
.filter((reserve) => !onlyQuoteAllowed || reserve.info.liquidityMint.equals(market.info.quoteMint))
|
||||||
(reserve) =>
|
|
||||||
!onlyQuoteAllowed ||
|
|
||||||
reserve.info.liquidityMint.equals(market.info.quoteMint)
|
|
||||||
)
|
|
||||||
.map((reserve) => {
|
.map((reserve) => {
|
||||||
const mint = reserve.info.liquidityMint.toBase58();
|
const mint = reserve.info.liquidityMint.toBase58();
|
||||||
const address = reserve.pubkey.toBase58();
|
const address = reserve.pubkey.toBase58();
|
||||||
const name = getTokenName(tokenMap, mint);
|
const name = getTokenName(tokenMap, mint);
|
||||||
return (
|
return (
|
||||||
<Option key={address} value={address} name={name} title={address}>
|
<Option key={address} value={address} name={name} title={address}>
|
||||||
<div
|
<div key={address} style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
key={address}
|
|
||||||
style={{ display: "flex", alignItems: "center" }}
|
|
||||||
>
|
|
||||||
<TokenIcon mintAddress={mint} />
|
<TokenIcon mintAddress={mint} />
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -60,4 +60,7 @@ export const LABELS = {
|
||||||
TRADING_TABLE_TITLE_APY: 'APY',
|
TRADING_TABLE_TITLE_APY: 'APY',
|
||||||
TRADING_TABLE_TITLE_ACTIONS: 'Action',
|
TRADING_TABLE_TITLE_ACTIONS: 'Action',
|
||||||
TRADING_ADD_POSITION: 'Add Position',
|
TRADING_ADD_POSITION: 'Add Position',
|
||||||
|
MARGIN_TRADE_ACTION: 'Margin Trade',
|
||||||
|
MARGIN_TRADE_QUESTION: 'How much of this asset would you like?',
|
||||||
|
TABLE_TITLE_BUYING_POWER: 'Total Buying Power',
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { HashRouter, Route, Switch } from "react-router-dom";
|
import { HashRouter, Route, Switch } from 'react-router-dom';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { WalletProvider } from "./contexts/wallet";
|
import { WalletProvider } from './contexts/wallet';
|
||||||
import { ConnectionProvider } from "./contexts/connection";
|
import { ConnectionProvider } from './contexts/connection';
|
||||||
import { AccountsProvider } from "./contexts/accounts";
|
import { AccountsProvider } from './contexts/accounts';
|
||||||
import { MarketProvider } from "./contexts/market";
|
import { MarketProvider } from './contexts/market';
|
||||||
import { LendingProvider } from "./contexts/lending";
|
import { LendingProvider } from './contexts/lending';
|
||||||
import { AppLayout } from "./components/Layout";
|
import { AppLayout } from './components/Layout';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BorrowReserveView,
|
BorrowReserveView,
|
||||||
|
@ -21,12 +21,13 @@ import {
|
||||||
LiquidateView,
|
LiquidateView,
|
||||||
LiquidateReserveView,
|
LiquidateReserveView,
|
||||||
MarginTrading,
|
MarginTrading,
|
||||||
} from "./views";
|
} from './views';
|
||||||
|
import { NewPosition } from './views/marginTrading/newPosition';
|
||||||
|
|
||||||
export function Routes() {
|
export function Routes() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HashRouter basename={"/"}>
|
<HashRouter basename={'/'}>
|
||||||
<ConnectionProvider>
|
<ConnectionProvider>
|
||||||
<WalletProvider>
|
<WalletProvider>
|
||||||
<AccountsProvider>
|
<AccountsProvider>
|
||||||
|
@ -34,51 +35,22 @@ export function Routes() {
|
||||||
<LendingProvider>
|
<LendingProvider>
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/" component={() => <HomeView />} />
|
<Route exact path='/' component={() => <HomeView />} />
|
||||||
<Route
|
<Route exact path='/dashboard' children={<DashboardView />} />
|
||||||
exact
|
<Route path='/reserve/:id' children={<ReserveView />} />
|
||||||
path="/dashboard"
|
<Route exact path='/deposit' component={() => <DepositView />} />
|
||||||
children={<DashboardView />}
|
<Route path='/deposit/:id' children={<DepositReserveView />} />
|
||||||
/>
|
<Route path='/withdraw/:id' children={<WithdrawView />} />
|
||||||
<Route path="/reserve/:id" children={<ReserveView />} />
|
<Route exact path='/borrow' children={<BorrowView />} />
|
||||||
<Route
|
<Route path='/borrow/:id' children={<BorrowReserveView />} />
|
||||||
exact
|
<Route path='/repay/loan/:obligation' children={<RepayReserveView />} />
|
||||||
path="/deposit"
|
<Route path='/repay/:reserve' children={<RepayReserveView />} />
|
||||||
component={() => <DepositView />}
|
<Route exact path='/liquidate' children={<LiquidateView />} />
|
||||||
/>
|
<Route path='/liquidate/:id' children={<LiquidateReserveView />} />
|
||||||
<Route
|
<Route exact path='/marginTrading' children={<MarginTrading />} />
|
||||||
path="/deposit/:id"
|
|
||||||
children={<DepositReserveView />}
|
<Route path='/marginTrading/:id' children={<NewPosition />} />
|
||||||
/>
|
<Route exact path='/faucet' children={<FaucetView />} />
|
||||||
<Route path="/withdraw/:id" children={<WithdrawView />} />
|
|
||||||
<Route exact path="/borrow" children={<BorrowView />} />
|
|
||||||
<Route
|
|
||||||
path="/borrow/:id"
|
|
||||||
children={<BorrowReserveView />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/repay/loan/:obligation"
|
|
||||||
children={<RepayReserveView />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/repay/:reserve"
|
|
||||||
children={<RepayReserveView />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/liquidate"
|
|
||||||
children={<LiquidateView />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/liquidate/:id"
|
|
||||||
children={<LiquidateReserveView />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/marginTrading"
|
|
||||||
children={<MarginTrading />}
|
|
||||||
/>
|
|
||||||
<Route exact path="/faucet" children={<FaucetView />} />
|
|
||||||
</Switch>
|
</Switch>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</LendingProvider>
|
</LendingProvider>
|
||||||
|
|
|
@ -1,42 +1,41 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { useTokenName, useBorrowingPower } from "../../hooks";
|
import { useTokenName, useBorrowingPower } from '../../hooks';
|
||||||
import { calculateBorrowAPY, LendingReserve } from "../../models/lending";
|
import { calculateBorrowAPY, LendingReserve } from '../../models/lending';
|
||||||
import { TokenIcon } from "../../components/TokenIcon";
|
import { TokenIcon } from '../../components/TokenIcon';
|
||||||
import { formatNumber, formatPct } from "../../utils/utils";
|
import { formatNumber, formatPct } from '../../utils/utils';
|
||||||
import { Button } from "antd";
|
import { Button } from 'antd';
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from 'react-router-dom';
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import { LABELS } from "../../constants";
|
import { LABELS } from '../../constants';
|
||||||
import { useMidPriceInUSD } from "../../contexts/market";
|
import { useMidPriceInUSD } from '../../contexts/market';
|
||||||
|
|
||||||
export const BorrowItem = (props: {
|
export const BorrowItem = (props: { reserve: LendingReserve; address: PublicKey }) => {
|
||||||
reserve: LendingReserve;
|
|
||||||
address: PublicKey;
|
|
||||||
}) => {
|
|
||||||
const name = useTokenName(props.reserve.liquidityMint);
|
const name = useTokenName(props.reserve.liquidityMint);
|
||||||
const price = useMidPriceInUSD(props.reserve.liquidityMint.toBase58()).price;
|
const price = useMidPriceInUSD(props.reserve.liquidityMint.toBase58()).price;
|
||||||
|
|
||||||
const { borrowingPower, totalInQuote } = useBorrowingPower(props.address)
|
const { borrowingPower, totalInQuote } = useBorrowingPower(props.address);
|
||||||
|
|
||||||
const apr = calculateBorrowAPY(props.reserve);
|
const apr = calculateBorrowAPY(props.reserve);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/borrow/${props.address.toBase58()}`}>
|
<Link to={`/borrow/${props.address.toBase58()}`}>
|
||||||
<div className="borrow-item">
|
<div className='borrow-item'>
|
||||||
<span style={{ display: "flex" }}>
|
<span style={{ display: 'flex' }}>
|
||||||
<TokenIcon mintAddress={props.reserve.liquidityMint} />
|
<TokenIcon mintAddress={props.reserve.liquidityMint} />
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
<div>${formatNumber.format(price)}</div>
|
<div>${formatNumber.format(price)}</div>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div><em>{formatNumber.format(borrowingPower)}</em> {name}</div>
|
<div>
|
||||||
<div className="dashboard-amount-quote">${formatNumber.format(totalInQuote)}</div>
|
<em>{formatNumber.format(borrowingPower)}</em> {name}
|
||||||
|
</div>
|
||||||
|
<div className='dashboard-amount-quote'>${formatNumber.format(totalInQuote)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>{formatPct.format(apr)}</div>
|
<div>{formatPct.format(apr)}</div>
|
||||||
<div>
|
<div>
|
||||||
<Button type="primary">
|
<Button type='primary'>
|
||||||
<span>{LABELS.BORROW_ACTION}</span>
|
<span>{LABELS.BORROW_ACTION}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,119 +0,0 @@
|
||||||
import { Button, Select, Slider } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import { IPosition } from '.';
|
|
||||||
import { NumericInput } from '../../components/Input/numeric';
|
|
||||||
import { TokenIcon } from '../../components/TokenIcon';
|
|
||||||
import tokens from '../../config/tokens.json';
|
|
||||||
import { LABELS } from '../../constants/labels';
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
interface IEditableAssetProps {
|
|
||||||
label: string;
|
|
||||||
assetKey: string;
|
|
||||||
setItem: (item: any) => void;
|
|
||||||
item: any;
|
|
||||||
}
|
|
||||||
function EditableAsset({ label, assetKey, setItem, item }: IEditableAssetProps) {
|
|
||||||
if (!item[assetKey]?.type) {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
size='large'
|
|
||||||
showSearch
|
|
||||||
style={{ margin: '5px 0px' }}
|
|
||||||
placeholder={label}
|
|
||||||
onChange={(v) =>
|
|
||||||
setItem({ ...item, [assetKey]: { ...(item[assetKey] || {}), type: tokens.find((t) => t.mintAddress === v) } })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{tokens.map((token) => (
|
|
||||||
<Option key={token.mintAddress} value={token.mintAddress} name={token.tokenName} title={token.tokenName}>
|
|
||||||
<div key={token.mintAddress} style={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
<TokenIcon mintAddress={token.mintAddress} />
|
|
||||||
{token.tokenName}
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-start' }}>
|
|
||||||
<NumericInput
|
|
||||||
value={item[assetKey].value}
|
|
||||||
style={{
|
|
||||||
fontSize: 20,
|
|
||||||
boxShadow: 'none',
|
|
||||||
borderColor: 'transparent',
|
|
||||||
outline: 'transparent',
|
|
||||||
}}
|
|
||||||
onChange={(v: number) => {
|
|
||||||
setItem({ ...item, [assetKey]: { ...(item[assetKey] || {}), value: v } });
|
|
||||||
}}
|
|
||||||
placeholder='0.00'
|
|
||||||
/>
|
|
||||||
<TokenIcon mintAddress={item[assetKey]?.type?.mintAddress} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MarginTradePosition({ item, setItem }: { item: IPosition; setItem?: (item: any) => void }) {
|
|
||||||
return (
|
|
||||||
<div className='trading-item'>
|
|
||||||
<div>
|
|
||||||
{setItem && (
|
|
||||||
<Select
|
|
||||||
size='large'
|
|
||||||
showSearch
|
|
||||||
style={{ margin: '5px 0px' }}
|
|
||||||
placeholder={LABELS.TRADING_TABLE_TITLE_MY_COLLATERAL}
|
|
||||||
onChange={(v) => setItem({ ...item, collateral: tokens.find((t) => t.mintAddress === v) })}
|
|
||||||
>
|
|
||||||
{tokens.map((token) => (
|
|
||||||
<Option key={token.mintAddress} value={token.mintAddress} name={token.tokenName} title={token.tokenName}>
|
|
||||||
<div key={token.mintAddress} style={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
<TokenIcon mintAddress={token.mintAddress} />
|
|
||||||
{token.tokenName}
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{setItem && (
|
|
||||||
<EditableAsset
|
|
||||||
item={item}
|
|
||||||
setItem={setItem}
|
|
||||||
label={LABELS.TRADING_TABLE_TITLE_DESIRED_ASSET}
|
|
||||||
assetKey={'asset'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{setItem && (
|
|
||||||
<Slider
|
|
||||||
tooltipVisible={true}
|
|
||||||
defaultValue={1}
|
|
||||||
dots={true}
|
|
||||||
max={5}
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
tooltipPlacement={'top'}
|
|
||||||
onChange={(v: number) => {
|
|
||||||
setItem({ ...item, leverage: v });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>123</div>
|
|
||||||
<div>123</div>
|
|
||||||
<div>123</div>
|
|
||||||
<div>
|
|
||||||
<Button type='primary'>
|
|
||||||
<span>{LABELS.TRADING_ADD_POSITION}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,138 +1,26 @@
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { LABELS } from '../../constants';
|
import { LABELS } from '../../constants';
|
||||||
import './style.less';
|
import './itemStyle.less';
|
||||||
import { Card, Progress, Slider, Statistic } from 'antd';
|
import { Card } from 'antd';
|
||||||
import MarginTradePosition from './MarginTradePosition';
|
import { useLendingReserves } from '../../hooks/useLendingReserves';
|
||||||
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
|
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<number>(0);
|
|
||||||
const profitPart = (myPart + brokeragePart) * (myGain / 100);
|
|
||||||
let progressBar = null;
|
|
||||||
if (profitPart > 0) {
|
|
||||||
// normalize...
|
|
||||||
const total = profitPart + myPart + brokeragePart;
|
|
||||||
progressBar = (
|
|
||||||
<Progress
|
|
||||||
percent={(myPart / total) * 100 + (brokeragePart / total) * 100}
|
|
||||||
success={{ percent: (brokeragePart / total) * 100, strokeColor: brokerageColor }}
|
|
||||||
strokeColor={myColor}
|
|
||||||
trailColor={gains}
|
|
||||||
showInfo={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// now, we're eating away your profit...
|
|
||||||
myPart += profitPart; // profit is negative
|
|
||||||
const total = myPart + brokeragePart;
|
|
||||||
if (myPart < 0) {
|
|
||||||
progressBar = <p>Your position has been liquidated at this price swing.</p>;
|
|
||||||
} else
|
|
||||||
progressBar = (
|
|
||||||
<Progress
|
|
||||||
showInfo={false}
|
|
||||||
success={{ percent: (brokeragePart / total) * 100, strokeColor: brokerageColor }}
|
|
||||||
trailColor={myColor}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Slider
|
|
||||||
tooltipVisible={true}
|
|
||||||
defaultValue={0}
|
|
||||||
dots={true}
|
|
||||||
max={100}
|
|
||||||
min={-100}
|
|
||||||
step={5}
|
|
||||||
tooltipPlacement={'top'}
|
|
||||||
onChange={(v: number) => {
|
|
||||||
setMyGain(v);
|
|
||||||
}}
|
|
||||||
style={{ marginBottom: '20px' }}
|
|
||||||
/>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center' }}>
|
|
||||||
<Card>
|
|
||||||
<Statistic
|
|
||||||
title='Leverage'
|
|
||||||
value={brokeragePart}
|
|
||||||
precision={2}
|
|
||||||
valueStyle={{ color: brokerageColor }}
|
|
||||||
suffix={item.asset?.type.tokenName}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<Statistic
|
|
||||||
title='My Collateral Value'
|
|
||||||
value={myPart}
|
|
||||||
precision={2}
|
|
||||||
valueStyle={{ color: myColor }}
|
|
||||||
suffix={item.asset?.type.tokenName}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<Statistic
|
|
||||||
title='Profit/Loss'
|
|
||||||
value={profitPart}
|
|
||||||
precision={2}
|
|
||||||
valueStyle={{ color: profitPart > 0 ? gains : losses }}
|
|
||||||
suffix={item.asset?.type.tokenSymbol}
|
|
||||||
prefix={profitPart > 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
{progressBar}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export const MarginTrading = () => {
|
export const MarginTrading = () => {
|
||||||
const [newPosition, setNewPosition] = useState<IPosition>({ id: null, leverage: 1 });
|
const { reserveAccounts } = useLendingReserves();
|
||||||
|
|
||||||
const positions: any[] = [];
|
|
||||||
return (
|
return (
|
||||||
<div className='trading-container'>
|
<div className='flexColumn'>
|
||||||
<div className='flexColumn'>
|
<Card>
|
||||||
<Card>
|
<div className='choose-margin-item choose-margin-header'>
|
||||||
<div className='trading-item trading-header'>
|
<div>{LABELS.TABLE_TITLE_ASSET}</div>
|
||||||
<div>{LABELS.TRADING_TABLE_TITLE_MY_COLLATERAL}</div>
|
<div>Serum Dex Price</div>
|
||||||
<div>{LABELS.TRADING_TABLE_TITLE_DESIRED_ASSET}</div>
|
<div>{LABELS.TABLE_TITLE_BUYING_POWER}</div>
|
||||||
<div>{LABELS.TRADING_TABLE_TITLE_MULTIPLIER}</div>
|
<div>{LABELS.TABLE_TITLE_APY}</div>
|
||||||
<div>{LABELS.TRADING_TABLE_TITLE_ASSET_PRICE}</div>
|
<div></div>
|
||||||
<div>{LABELS.TRADING_TABLE_TITLE_LIQUIDATION_PRICE}</div>
|
</div>
|
||||||
<div>{LABELS.TRADING_TABLE_TITLE_APY}</div>
|
{reserveAccounts.map((account) => (
|
||||||
<div>{LABELS.TRADING_TABLE_TITLE_ACTIONS}</div>
|
<MarginTradeItem reserve={account.info} address={account.pubkey} />
|
||||||
</div>
|
))}
|
||||||
<MarginTradePosition key={newPosition.id} item={newPosition} setItem={setNewPosition} />
|
</Card>
|
||||||
<Breakdown item={newPosition} />
|
|
||||||
{positions.map((item) => (
|
|
||||||
<MarginTradePosition key={item.id} item={item} />
|
|
||||||
))}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 (
|
||||||
|
<Link to={`/marginTrading/${props.address.toBase58()}`}>
|
||||||
|
<div className='choose-margin-item'>
|
||||||
|
<span style={{ display: 'flex' }}>
|
||||||
|
<TokenIcon mintAddress={props.reserve.liquidityMint} />
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
<div>${formatNumber.format(price)}</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<em>{formatNumber.format(200)}</em> {name}
|
||||||
|
</div>
|
||||||
|
<div className='dashboard-amount-quote'>${formatNumber.format(300)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>{formatPct.format(apr)}</div>
|
||||||
|
<div>
|
||||||
|
<Button type='primary'>
|
||||||
|
<span>{LABELS.MARGIN_TRADE_ACTION}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<number>(0);
|
||||||
|
const profitPart = (myPart + brokeragePart) * (myGain / 100);
|
||||||
|
let progressBar = null;
|
||||||
|
if (profitPart > 0) {
|
||||||
|
// normalize...
|
||||||
|
const total = profitPart + myPart + brokeragePart;
|
||||||
|
progressBar = (
|
||||||
|
<Progress
|
||||||
|
percent={(myPart / total) * 100 + (brokeragePart / total) * 100}
|
||||||
|
success={{ percent: (brokeragePart / total) * 100, strokeColor: brokerageColor }}
|
||||||
|
strokeColor={myColor}
|
||||||
|
trailColor={gains}
|
||||||
|
showInfo={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// now, we're eating away your profit...
|
||||||
|
myPart += profitPart; // profit is negative
|
||||||
|
const total = myPart + brokeragePart;
|
||||||
|
if (myPart < 0) {
|
||||||
|
progressBar = <p>Your position has been liquidated at this price swing.</p>;
|
||||||
|
} else
|
||||||
|
progressBar = (
|
||||||
|
<Progress
|
||||||
|
showInfo={false}
|
||||||
|
success={{ percent: (brokeragePart / total) * 100, strokeColor: brokerageColor }}
|
||||||
|
trailColor={myColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Slider
|
||||||
|
tooltipVisible={true}
|
||||||
|
defaultValue={0}
|
||||||
|
dots={true}
|
||||||
|
max={100}
|
||||||
|
min={-100}
|
||||||
|
step={5}
|
||||||
|
tooltipPlacement={'top'}
|
||||||
|
onChange={(v: number) => {
|
||||||
|
setMyGain(v);
|
||||||
|
}}
|
||||||
|
style={{ marginBottom: '20px' }}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center' }}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title='Leverage'
|
||||||
|
value={brokeragePart}
|
||||||
|
precision={2}
|
||||||
|
valueStyle={{ color: brokerageColor }}
|
||||||
|
suffix={item.asset?.type?.tokenName}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title='My Collateral Value'
|
||||||
|
value={myPart}
|
||||||
|
precision={2}
|
||||||
|
valueStyle={{ color: myColor }}
|
||||||
|
suffix={item.asset?.type?.tokenName}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title='Profit/Loss'
|
||||||
|
value={profitPart}
|
||||||
|
precision={2}
|
||||||
|
valueStyle={{ color: profitPart > 0 ? gains : losses }}
|
||||||
|
suffix={item.asset?.type?.tokenSymbol}
|
||||||
|
prefix={profitPart > 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
{progressBar}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<Select
|
||||||
|
size='large'
|
||||||
|
showSearch
|
||||||
|
style={{ margin: '5px 0px' }}
|
||||||
|
placeholder={label}
|
||||||
|
onChange={(v) =>
|
||||||
|
setItem({ ...item, [assetKey]: { ...(item[assetKey] || {}), type: tokens.find((t) => t.mintAddress === v) } })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tokens.map((token) => (
|
||||||
|
<Option key={token.mintAddress} value={token.mintAddress} name={token.tokenName} title={token.tokenName}>
|
||||||
|
<div key={token.mintAddress} style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<TokenIcon mintAddress={token.mintAddress} />
|
||||||
|
{token.tokenName}
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-start' }}>
|
||||||
|
<NumericInput
|
||||||
|
value={item[assetKey].value}
|
||||||
|
style={{
|
||||||
|
fontSize: 20,
|
||||||
|
boxShadow: 'none',
|
||||||
|
borderColor: 'transparent',
|
||||||
|
outline: 'transparent',
|
||||||
|
}}
|
||||||
|
onChange={(v: number) => {
|
||||||
|
setItem({ ...item, [assetKey]: { ...(item[assetKey] || {}), value: v } });
|
||||||
|
}}
|
||||||
|
placeholder='0.00'
|
||||||
|
/>
|
||||||
|
<TokenIcon mintAddress={item[assetKey]?.type?.mintAddress} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<LendingReserve>;
|
||||||
|
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 (
|
||||||
|
<Card className='new-position-item new-position-item-left' bodyStyle={bodyStyle}>
|
||||||
|
{showConfirmation ? (
|
||||||
|
<ActionConfirmation onClose={() => setShowConfirmation(false)} />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='borrow-input-title'>{LABELS.SELECT_COLLATERAL}</div>
|
||||||
|
<CollateralSelector
|
||||||
|
reserve={lendingReserve.info}
|
||||||
|
onCollateralReserve={(key) => {
|
||||||
|
const id: string = cache.byParser(LendingReserveParser).find((acc) => acc === key) || '';
|
||||||
|
const parser = cache.get(id) as ParsedAccount<LendingReserve>;
|
||||||
|
const tokenMint = parser.info.collateralMint.toBase58();
|
||||||
|
setNewPosition({ ...newPosition, collateral: tokens.find((t) => t.mintAddress === tokenMint) });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='borrow-input-title'>{LABELS.MARGIN_TRADE_QUESTION}</div>
|
||||||
|
<div className='token-input'>
|
||||||
|
<TokenIcon mintAddress={newPosition.asset.type?.mintAddress} />
|
||||||
|
<NumericInput
|
||||||
|
value={newPosition.asset.value}
|
||||||
|
style={{
|
||||||
|
fontSize: 20,
|
||||||
|
boxShadow: 'none',
|
||||||
|
borderColor: 'transparent',
|
||||||
|
outline: 'transparent',
|
||||||
|
}}
|
||||||
|
onChange={(v: number) => {
|
||||||
|
setNewPosition({ ...newPosition, asset: { ...newPosition.asset, value: v } });
|
||||||
|
}}
|
||||||
|
placeholder='0.00'
|
||||||
|
/>
|
||||||
|
<div>{newPosition.asset.type?.tokenSymbol}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Radio.Group
|
||||||
|
defaultValue={newPosition.leverage}
|
||||||
|
size='large'
|
||||||
|
onChange={(e) => {
|
||||||
|
setNewPosition({ ...newPosition, leverage: e.target.value });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Radio.Button value={1}>1x</Radio.Button>
|
||||||
|
<Radio.Button value={2}>2x</Radio.Button>
|
||||||
|
<Radio.Button value={3}>3x</Radio.Button>
|
||||||
|
<Radio.Button value={4}>4x</Radio.Button>
|
||||||
|
<Radio.Button value={5}>5x</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
<NumericInput
|
||||||
|
value={newPosition.leverage}
|
||||||
|
style={{
|
||||||
|
fontSize: 20,
|
||||||
|
boxShadow: 'none',
|
||||||
|
borderColor: 'transparent',
|
||||||
|
outline: 'transparent',
|
||||||
|
}}
|
||||||
|
onChange={(leverage: number) => {
|
||||||
|
setNewPosition({ ...newPosition, leverage });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button type='primary'>
|
||||||
|
<span>{LABELS.TRADING_ADD_POSITION}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Breakdown item={newPosition} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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<Position>({
|
||||||
|
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 (
|
||||||
|
<div className='new-position'>
|
||||||
|
<div className='new-position-container'>
|
||||||
|
<NewPositionForm lendingReserve={lendingReserve} newPosition={newPosition} setNewPosition={setNewPosition} />
|
||||||
|
<SideReserveOverview
|
||||||
|
className='new-position-item new-position-item-right'
|
||||||
|
reserve={lendingReserve}
|
||||||
|
mode={SideReserveOverviewMode.Borrow}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue