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 { 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<LendingMarket>;
|
||||
if (!market) return null;
|
||||
|
||||
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(market?.info?.quoteMint);
|
||||
|
||||
return (
|
||||
<Select
|
||||
size="large"
|
||||
size='large'
|
||||
showSearch
|
||||
style={{ minWidth: 300, margin: "5px 0px" }}
|
||||
placeholder="Collateral"
|
||||
style={{ minWidth: 300, margin: '5px 0px' }}
|
||||
placeholder='Collateral'
|
||||
value={props.collateralReserve}
|
||||
disabled={props.disabled}
|
||||
defaultValue={props.collateralReserve}
|
||||
|
@ -39,27 +37,18 @@ export const CollateralSelector = (props: {
|
|||
props.onCollateralReserve(item);
|
||||
}
|
||||
}}
|
||||
filterOption={(input, option) =>
|
||||
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
>
|
||||
{reserveAccounts
|
||||
.filter((reserve) => reserve.info !== props.reserve)
|
||||
.filter(
|
||||
(reserve) =>
|
||||
!onlyQuoteAllowed ||
|
||||
reserve.info.liquidityMint.equals(market.info.quoteMint)
|
||||
)
|
||||
.filter((reserve) => !onlyQuoteAllowed || reserve.info.liquidityMint.equals(market.info.quoteMint))
|
||||
.map((reserve) => {
|
||||
const mint = reserve.info.liquidityMint.toBase58();
|
||||
const address = reserve.pubkey.toBase58();
|
||||
const name = getTokenName(tokenMap, mint);
|
||||
return (
|
||||
<Option key={address} value={address} name={name} title={address}>
|
||||
<div
|
||||
key={address}
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<div key={address} style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<TokenIcon mintAddress={mint} />
|
||||
{name}
|
||||
</div>
|
||||
|
|
|
@ -60,4 +60,7 @@ export const LABELS = {
|
|||
TRADING_TABLE_TITLE_APY: 'APY',
|
||||
TRADING_TABLE_TITLE_ACTIONS: 'Action',
|
||||
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 React from "react";
|
||||
import { WalletProvider } from "./contexts/wallet";
|
||||
import { ConnectionProvider } from "./contexts/connection";
|
||||
import { AccountsProvider } from "./contexts/accounts";
|
||||
import { MarketProvider } from "./contexts/market";
|
||||
import { LendingProvider } from "./contexts/lending";
|
||||
import { AppLayout } from "./components/Layout";
|
||||
import { HashRouter, Route, Switch } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { WalletProvider } from './contexts/wallet';
|
||||
import { ConnectionProvider } from './contexts/connection';
|
||||
import { AccountsProvider } from './contexts/accounts';
|
||||
import { MarketProvider } from './contexts/market';
|
||||
import { LendingProvider } from './contexts/lending';
|
||||
import { AppLayout } from './components/Layout';
|
||||
|
||||
import {
|
||||
BorrowReserveView,
|
||||
|
@ -21,12 +21,13 @@ import {
|
|||
LiquidateView,
|
||||
LiquidateReserveView,
|
||||
MarginTrading,
|
||||
} from "./views";
|
||||
} from './views';
|
||||
import { NewPosition } from './views/marginTrading/newPosition';
|
||||
|
||||
export function Routes() {
|
||||
return (
|
||||
<>
|
||||
<HashRouter basename={"/"}>
|
||||
<HashRouter basename={'/'}>
|
||||
<ConnectionProvider>
|
||||
<WalletProvider>
|
||||
<AccountsProvider>
|
||||
|
@ -34,51 +35,22 @@ export function Routes() {
|
|||
<LendingProvider>
|
||||
<AppLayout>
|
||||
<Switch>
|
||||
<Route exact path="/" component={() => <HomeView />} />
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard"
|
||||
children={<DashboardView />}
|
||||
/>
|
||||
<Route path="/reserve/:id" children={<ReserveView />} />
|
||||
<Route
|
||||
exact
|
||||
path="/deposit"
|
||||
component={() => <DepositView />}
|
||||
/>
|
||||
<Route
|
||||
path="/deposit/:id"
|
||||
children={<DepositReserveView />}
|
||||
/>
|
||||
<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 />} />
|
||||
<Route exact path='/' component={() => <HomeView />} />
|
||||
<Route exact path='/dashboard' children={<DashboardView />} />
|
||||
<Route path='/reserve/:id' children={<ReserveView />} />
|
||||
<Route exact path='/deposit' component={() => <DepositView />} />
|
||||
<Route path='/deposit/:id' children={<DepositReserveView />} />
|
||||
<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 path='/marginTrading/:id' children={<NewPosition />} />
|
||||
<Route exact path='/faucet' children={<FaucetView />} />
|
||||
</Switch>
|
||||
</AppLayout>
|
||||
</LendingProvider>
|
||||
|
|
|
@ -1,42 +1,41 @@
|
|||
import React from "react";
|
||||
import { useTokenName, useBorrowingPower } 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";
|
||||
import React from 'react';
|
||||
import { useTokenName, useBorrowingPower } 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 BorrowItem = (props: {
|
||||
reserve: LendingReserve;
|
||||
address: PublicKey;
|
||||
}) => {
|
||||
export const BorrowItem = (props: { reserve: LendingReserve; address: PublicKey }) => {
|
||||
const name = useTokenName(props.reserve.liquidityMint);
|
||||
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);
|
||||
|
||||
return (
|
||||
<Link to={`/borrow/${props.address.toBase58()}`}>
|
||||
<div className="borrow-item">
|
||||
<span style={{ display: "flex" }}>
|
||||
<div className='borrow-item'>
|
||||
<span style={{ display: 'flex' }}>
|
||||
<TokenIcon mintAddress={props.reserve.liquidityMint} />
|
||||
{name}
|
||||
</span>
|
||||
<div>${formatNumber.format(price)}</div>
|
||||
<div>
|
||||
<div>
|
||||
<div><em>{formatNumber.format(borrowingPower)}</em> {name}</div>
|
||||
<div className="dashboard-amount-quote">${formatNumber.format(totalInQuote)}</div>
|
||||
<div>
|
||||
<em>{formatNumber.format(borrowingPower)}</em> {name}
|
||||
</div>
|
||||
<div className='dashboard-amount-quote'>${formatNumber.format(totalInQuote)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>{formatPct.format(apr)}</div>
|
||||
<div>
|
||||
<Button type="primary">
|
||||
<Button type='primary'>
|
||||
<span>{LABELS.BORROW_ACTION}</span>
|
||||
</Button>
|
||||
</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 './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<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 = () => {
|
||||
const [newPosition, setNewPosition] = useState<IPosition>({ id: null, leverage: 1 });
|
||||
|
||||
const positions: any[] = [];
|
||||
const { reserveAccounts } = useLendingReserves();
|
||||
return (
|
||||
<div className='trading-container'>
|
||||
<div className='flexColumn'>
|
||||
<Card>
|
||||
<div className='trading-item trading-header'>
|
||||
<div>{LABELS.TRADING_TABLE_TITLE_MY_COLLATERAL}</div>
|
||||
<div>{LABELS.TRADING_TABLE_TITLE_DESIRED_ASSET}</div>
|
||||
<div>{LABELS.TRADING_TABLE_TITLE_MULTIPLIER}</div>
|
||||
<div>{LABELS.TRADING_TABLE_TITLE_ASSET_PRICE}</div>
|
||||
<div>{LABELS.TRADING_TABLE_TITLE_LIQUIDATION_PRICE}</div>
|
||||
<div>{LABELS.TRADING_TABLE_TITLE_APY}</div>
|
||||
<div>{LABELS.TRADING_TABLE_TITLE_ACTIONS}</div>
|
||||
</div>
|
||||
<MarginTradePosition key={newPosition.id} item={newPosition} setItem={setNewPosition} />
|
||||
<Breakdown item={newPosition} />
|
||||
{positions.map((item) => (
|
||||
<MarginTradePosition key={item.id} item={item} />
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
<div className='flexColumn'>
|
||||
<Card>
|
||||
<div className='choose-margin-item choose-margin-header'>
|
||||
<div>{LABELS.TABLE_TITLE_ASSET}</div>
|
||||
<div>Serum Dex Price</div>
|
||||
<div>{LABELS.TABLE_TITLE_BUYING_POWER}</div>
|
||||
<div>{LABELS.TABLE_TITLE_APY}</div>
|
||||
<div></div>
|
||||
</div>
|
||||
{reserveAccounts.map((account) => (
|
||||
<MarginTradeItem reserve={account.info} address={account.pubkey} />
|
||||
))}
|
||||
</Card>
|
||||
</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