feat: THink we're getting very close to being done with this form.

This commit is contained in:
Mr. Dummy Tester 2020-12-28 17:36:45 -06:00
parent a0530957d5
commit b1dfe1676c
19 changed files with 736 additions and 161 deletions

View File

@ -25,6 +25,7 @@
"eventemitter3": "^4.0.7",
"identicon.js": "^2.3.3",
"jazzicon": "^1.5.0",
"lodash": "^4.17.20",
"react": "^16.13.1",
"react-chartjs-2": "^2.11.1",
"react-dom": "^16.13.1",

View File

@ -0,0 +1,154 @@
import React, { useEffect, useState } from 'react';
import { cache, ParsedAccount } from '../../contexts/accounts';
import { useConnectionConfig } from '../../contexts/connection';
import { useLendingReserves, useUserDeposits } from '../../hooks';
import { LendingReserve, LendingMarket, LendingReserveParser } from '../../models';
import { getTokenName } from '../../utils/utils';
import { Card, Select } from 'antd';
import { TokenIcon } from '../TokenIcon';
import { NumericInput } from '../Input/numeric';
import './style.less';
import { TokenDisplay } from '../TokenDisplay';
const { Option } = Select;
// User can choose a collateral they want to use, and then this will display the balance they have in Oyster's lending
// reserve for that collateral type.
export default function CollateralInput(props: {
title: string;
amount?: number | null;
reserve: LendingReserve;
disabled?: boolean;
onCollateralReserve?: (id: string) => void;
onLeverage?: (leverage: number) => void;
onInputChange: (value: number | null) => void;
hideBalance?: boolean;
showLeverageSelector?: boolean;
leverage?: number;
}) {
const { reserveAccounts } = useLendingReserves();
const { tokenMap } = useConnectionConfig();
const [collateralReserve, setCollateralReserve] = useState<string>();
const [balance, setBalance] = useState<number>(0);
const [lastAmount, setLastAmount] = useState<string>('');
const userDeposits = useUserDeposits();
useEffect(() => {
const id: string = cache.byParser(LendingReserveParser).find((acc) => acc === collateralReserve) || '';
const parser = cache.get(id) as ParsedAccount<LendingReserve>;
if (parser) {
const collateralDeposit = userDeposits.userDeposits.find(
(u) => u.reserve.info.liquidityMint.toBase58() === parser.info.liquidityMint.toBase58()
);
if (collateralDeposit) setBalance(collateralDeposit.info.amount);
else setBalance(0);
}
}, [collateralReserve, userDeposits]);
const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<LendingMarket>;
if (!market) return null;
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(market?.info?.quoteMint);
const renderReserveAccounts = reserveAccounts
.filter((reserve) => reserve.info !== props.reserve)
.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' }}>
<TokenIcon mintAddress={mint} />
{name}
</div>
</Option>
);
});
return (
<Card className='ccy-input' style={{ borderRadius: 20 }} bodyStyle={{ padding: 0 }}>
<div className='ccy-input-header'>
<div className='ccy-input-header-left'>{props.title}</div>
{!props.hideBalance && (
<div className='ccy-input-header-right' onClick={(e) => props.onInputChange && props.onInputChange(balance)}>
Balance: {balance.toFixed(6)}
</div>
)}
</div>
<div className='ccy-input-header' style={{ padding: '0px 10px 5px 7px' }}>
<NumericInput
value={parseFloat(lastAmount || '0.00') == props.amount ? lastAmount : props.amount?.toFixed(6)?.toString()}
onChange={(val: string) => {
if (props.onInputChange && parseFloat(val) != props.amount) {
if (!val || !parseFloat(val)) props.onInputChange(null);
else props.onInputChange(parseFloat(val));
}
setLastAmount(val);
}}
style={{
fontSize: 20,
boxShadow: 'none',
borderColor: 'transparent',
outline: 'transparent',
}}
placeholder='0.00'
/>
<div className='ccy-input-header-right' style={{ display: 'flex' }}>
{props.showLeverageSelector && (
<Select
size='large'
showSearch
style={{ minWidth: 150 }}
placeholder='CCY'
value={props.leverage}
onChange={(item: number) => {
if (props.onLeverage) props.onLeverage(item);
}}
notFoundContent={null}
onSearch={(item: string) => {
if (props.onLeverage && item.match(/^\d+$/)) {
props.onLeverage(parseFloat(item));
}
}}
filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
>
{[1, 2, 3, 4, 5].map((val) => (
<Option key={val} value={val} name={val + 'x'} title={val + 'x'}>
<div key={val} style={{ display: 'flex', alignItems: 'center' }}>
{val + 'x'}
</div>
</Option>
))}
</Select>
)}
{!props.disabled ? (
<Select
size='large'
showSearch
style={{ minWidth: 150 }}
placeholder='CCY'
value={collateralReserve}
onChange={(item) => {
if (props.onCollateralReserve) props.onCollateralReserve(item);
setCollateralReserve(item);
}}
filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
>
{renderReserveAccounts}
</Select>
) : (
<TokenDisplay
key={props.reserve.liquidityMint.toBase58()}
name={getTokenName(tokenMap, props.reserve.liquidityMint.toBase58())}
mintAddress={props.reserve.liquidityMint.toBase58()}
showBalance={false}
/>
)}
</div>
</div>
</Card>
);
}

View File

@ -0,0 +1,62 @@
.ccy-input {
margin-top: 10px;
margin-bottom: 10px;
.ant-select-selector,
.ant-select-selector:focus,
.ant-select-selector:active {
border-color: transparent !important;
box-shadow: none !important;
}
.ant-select-selection-item {
display: flex;
.token-balance {
display: none;
}
}
}
.token-balance {
color: grey;
}
.ccy-input-header {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 10px;
-webkit-box-pack: justify;
justify-content: space-between;
-webkit-box-align: center;
align-items: center;
flex-direction: row;
padding: 10px 20px 0px 20px;
}
.ccy-input-header-left {
width: 100%;
box-sizing: border-box;
margin: 0px;
min-width: 0px;
display: flex;
padding: 0px;
-webkit-box-align: center;
align-items: center;
width: fit-content;
}
.ccy-input-header-right {
width: 100%;
display: flex;
flex-direction: row;
-webkit-box-align: center;
align-items: center;
justify-self: flex-end;
justify-content: flex-end;
}
.ant-select-dropdown {
width: 150px !important;
}

View File

@ -0,0 +1,56 @@
import { Card, Row, Col } from 'antd';
import React, { useMemo } from 'react';
import { useMint } from '../../contexts/accounts';
import { useEnrichedPools } from '../../contexts/market';
import { useUserAccounts } from '../../hooks';
import { PoolInfo } from '../../models';
import { formatPriceNumber } from '../../utils/utils';
export const PoolPrice = (props: { pool: PoolInfo }) => {
const pool = props.pool;
const pools = useMemo(() => [props.pool].filter((p) => p) as PoolInfo[], [props.pool]);
const enriched = useEnrichedPools(pools)[0];
const { userAccounts } = useUserAccounts();
const lpMint = useMint(pool.pubkeys.mint);
const ratio =
userAccounts
.filter((f) => pool.pubkeys.mint.equals(f.info.mint))
.reduce((acc, item) => item.info.amount.toNumber() + acc, 0) / (lpMint?.supply.toNumber() || 0);
if (!enriched) {
return null;
}
return (
<Card
className='ccy-input'
style={{ borderRadius: 20, width: '100%' }}
bodyStyle={{ padding: '7px' }}
size='small'
title='Prices and pool share'
>
<Row style={{ width: '100%' }}>
<Col span={8}>
{formatPriceNumber.format(parseFloat(enriched.liquidityA) / parseFloat(enriched.liquidityB))}
</Col>
<Col span={8}>
{formatPriceNumber.format(parseFloat(enriched.liquidityB) / parseFloat(enriched.liquidityA))}
</Col>
<Col span={8}>
{ratio * 100 < 0.001 && ratio > 0 ? '<' : ''}
&nbsp;{formatPriceNumber.format(ratio * 100)}%
</Col>
</Row>
<Row style={{ width: '100%' }}>
<Col span={8}>
{enriched.names[0]} per {enriched.names[1]}
</Col>
<Col span={8}>
{enriched.names[1]} per {enriched.names[0]}
</Col>
<Col span={8}>Share of pool</Col>
</Row>
</Card>
);
};

View File

@ -0,0 +1,100 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { PoolInfo } from '../../models';
import echarts from 'echarts';
import { formatNumber, formatUSD } from '../../utils/utils';
import { useEnrichedPools } from '../../contexts/market';
export const SupplyOverview = (props: { pool?: PoolInfo }) => {
const { pool } = props;
const pools = useMemo(() => (pool ? [pool] : []), [pool]);
const enriched = useEnrichedPools(pools);
const chartDiv = useRef<HTMLDivElement>(null);
// dispose chart
useEffect(() => {
const div = chartDiv.current;
return () => {
let instance = div && echarts.getInstanceByDom(div);
instance && instance.dispose();
};
}, []);
useEffect(() => {
if (!chartDiv.current || enriched.length === 0) {
return;
}
let instance = echarts.getInstanceByDom(chartDiv.current);
if (!instance) {
instance = echarts.init(chartDiv.current as any);
}
const data = [
{
name: enriched[0].names[0],
value: enriched[0].liquidityAinUsd,
tokens: enriched[0].liquidityA,
},
{
name: enriched[0].names[1],
value: enriched[0].liquidityBinUsd,
tokens: enriched[0].liquidityB,
},
];
instance.setOption({
tooltip: {
trigger: 'item',
formatter: function (params: any) {
var val = formatUSD.format(params.value);
var tokenAmount = formatNumber.format(params.data.tokens);
return `${params.name}: \n${val}\n(${tokenAmount})`;
},
},
series: [
{
name: 'Liquidity',
type: 'pie',
top: 0,
bottom: 0,
left: 0,
right: 0,
animation: false,
label: {
fontSize: 14,
show: true,
formatter: function (params: any) {
var val = formatUSD.format(params.value);
var tokenAmount = formatNumber.format(params.data.tokens);
return `{c|${params.name}}\n{r|${tokenAmount}}\n{r|${val}}`;
},
rich: {
c: {
color: 'black',
lineHeight: 22,
align: 'center',
},
r: {
color: 'black',
align: 'right',
},
},
color: 'rgba(255, 255, 255, 0.5)',
},
itemStyle: {
normal: {
borderColor: '#000',
},
},
data,
},
],
});
}, [enriched]);
if (enriched.length === 0) {
return null;
}
return <div ref={chartDiv} style={{ height: 150, width: '100%' }} />;
};

View File

@ -0,0 +1,47 @@
import React from 'react';
import { useMint, useAccountByMint } from '../../contexts/accounts';
import { TokenIcon } from '../TokenIcon';
export const TokenDisplay = (props: {
name: string;
mintAddress: string;
icon?: JSX.Element;
showBalance?: boolean;
}) => {
const { showBalance, mintAddress, name, icon } = props;
const tokenMint = useMint(mintAddress);
const tokenAccount = useAccountByMint(mintAddress);
let balance: number = 0;
let hasBalance: boolean = false;
if (showBalance) {
if (tokenAccount && tokenMint) {
balance = tokenAccount.info.amount.toNumber() / Math.pow(10, tokenMint.decimals);
hasBalance = balance > 0;
}
}
return (
<>
<div
title={mintAddress}
key={mintAddress}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
{icon || <TokenIcon mintAddress={mintAddress} />}
{name}
</div>
{showBalance ? (
<span title={balance.toString()} key={mintAddress} className='token-balance'>
&nbsp; {hasBalance ? (balance < 0.001 ? '<0.001' : balance.toFixed(3)) : '-'}
</span>
) : null}
</div>
</>
);
};

View File

@ -65,7 +65,8 @@ export const LABELS = {
MARGIN_TRADE_QUESTION: 'Please choose how much of this asset you wish to purchase.',
TABLE_TITLE_BUYING_POWER: 'Total Buying Power',
NOT_ENOUGH_MARGIN_MESSAGE: 'Not enough buying power in oyster to make this trade at this leverage.',
LEVERAGE_LIMIT_MESSAGE:
'With liquidity pools in their current state, you are not allowed to use leverage at this multiple. You will need more margin to make this trade.',
SET_MORE_MARGIN_MESSAGE: 'You need more margin to match this leverage amount to make this trade.',
LEVERAGE_LIMIT_MESSAGE: 'You will need more margin to make this trade.',
NO_DEPOSIT_MESSAGE: 'You need to deposit coin of this type into oyster before trading with it on margin.',
NO_COLL_TYPE_MESSAGE: 'Choose Collateral CCY',
};

View File

@ -272,6 +272,7 @@ export const useEnrichedPools = (pools: PoolInfo[]) => {
dispose && dispose();
subscriptions.forEach((dispose) => dispose && dispose());
};
// Do not add pools here, causes a really bad infinite rendering loop. Use poolKeys instead.
}, [tokenMap, dailyVolume, poolKeys, subscribeToMarket, marketEmitter, marketsByMint]);
return enriched;

View File

@ -178,12 +178,10 @@ export const collateralExchangeRate = (reserve?: LendingReserve) => {
export const collateralToLiquidity = (collateralAmount: BN | number, reserve?: LendingReserve) => {
const amount = typeof collateralAmount === 'number' ? collateralAmount : collateralAmount.toNumber();
console.log('Exchange rate:', collateralExchangeRate(reserve));
return Math.floor(amount / collateralExchangeRate(reserve));
};
export const liquidityToCollateral = (liquidityAmount: BN | number, reserve?: LendingReserve) => {
const amount = typeof liquidityAmount === 'number' ? liquidityAmount : liquidityAmount.toNumber();
console.log('Exchange rate:', collateralExchangeRate(reserve));
return Math.floor(amount * collateralExchangeRate(reserve));
};

View File

@ -15,6 +15,12 @@ export interface KnownToken {
export type KnownTokenMap = Map<string, KnownToken>;
export const formatPriceNumber = new Intl.NumberFormat('en-US', {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 8,
});
export function useLocalStorageState(key: string, defaultState?: string) {
const [state, setState] = useState(() => {
// NOTE: Not sure if this is ok

View File

@ -4,17 +4,23 @@ import { Position } from './interfaces';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import tokens from '../../../config/tokens.json';
import GainsChart from './GainsChart';
import { usePoolAndTradeInfoFrom } from './utils';
export function Breakdown({ item }: { item: Position }) {
let myPart = parseFloat(item.asset?.value || '0') / item.leverage;
const brokeragePart = parseFloat(item.asset?.value || '0') - myPart;
export default function Breakdown({ item }: { item: Position }) {
const { enrichedPools, leverage } = usePoolAndTradeInfoFrom(item);
const exchangeRate = enrichedPools.length == 0 ? 1 : enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
let myPart = item.collateral.value || 0;
const brokeragePart = (item.collateral.value || 0) * leverage - myPart;
const brokerageColor = 'brown';
const myColor = 'blue';
const gains = 'green';
const losses = 'red';
const token = tokens.find((t) => t.mintAddress === item.asset.type?.info?.liquidityMint?.toBase58());
const collateralToken = tokens.find((t) => t.mintAddress === item.collateral.type?.info?.liquidityMint?.toBase58());
const [myGain, setMyGain] = useState<number>(0);
const [myGain, setMyGain] = useState<number>(10);
const profitPart = (myPart + brokeragePart) * (myGain / 100);
let progressBar = null;
if (profitPart > 0) {
@ -46,52 +52,64 @@ export function Breakdown({ item }: { item: Position }) {
}
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={token?.tokenSymbol}
/>
</Card>
<Card>
<Statistic
title='My Collateral Value'
value={myPart}
precision={2}
valueStyle={{ color: myColor }}
suffix={token?.tokenSymbol}
/>
</Card>
<Card>
<Statistic
title='Profit/Loss'
value={profitPart}
precision={2}
valueStyle={{ color: profitPart > 0 ? gains : losses }}
suffix={token?.tokenSymbol}
prefix={profitPart > 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
/>
</Card>
</div>
<GainsChart item={item} priceChange={myGain} />
{progressBar}
<div className='new-position-item new-position-item-top-right'>
<Card className='new-position-item new-position-item-top-right'>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center' }}>
<Card>
<Statistic
title='Leverage'
value={brokeragePart * exchangeRate}
precision={2}
valueStyle={{ color: brokerageColor }}
suffix={token?.tokenSymbol}
/>
</Card>
<Card>
<Statistic
title='My Collateral Value'
value={myPart}
precision={2}
valueStyle={{ color: myColor }}
suffix={collateralToken?.tokenSymbol}
/>
</Card>
<Card>
<Statistic
title='Profit/Loss'
value={profitPart * exchangeRate}
precision={2}
valueStyle={{ color: profitPart > 0 ? gains : losses }}
suffix={token?.tokenSymbol}
prefix={profitPart > 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
/>
</Card>
</div>
<br />
{progressBar}
</Card>
<Card className='new-position-item new-position-item-bottom-right'>
<GainsChart item={item} priceChange={myGain} />
<Slider
tooltipVisible={true}
defaultValue={10}
tipFormatter={(p) => <span>{p}%</span>}
max={100}
min={-100}
tooltipPlacement={'top'}
onChange={(v: number) => {
setMyGain(v);
}}
style={{ marginBottom: '20px' }}
/>
<span style={{ float: 'right', fontSize: '9px' }}>
<a
href='https://github.com/bZxNetwork/fulcrum_ui/blob/development/packages/fulcrum-website/assets/js/trading.js'
target='blank'
>
credit
</a>
</span>
</Card>
</div>
);
}

View File

@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Line } from 'react-chartjs-2';
import { Position } from './interfaces';
import { debounce } from 'lodash';
// Special thanks to
// https://github.com/bZxNetwork/fulcrum_ui/blob/development/packages/fulcrum-website/assets/js/trading.js
@ -126,7 +127,6 @@ function updateChartData({
}
function drawLabels(t: any, ctx: any, leverage: number, priceChange: number) {
console.log('drawing');
ctx.save();
ctx.font = 'normal normal bold 15px /1.5 Muli';
ctx.textBaseline = 'bottom';
@ -159,13 +159,14 @@ function drawLabels(t: any, ctx: any, leverage: number, priceChange: number) {
});
ctx.restore();
}
const debouncedUpdateChartData = debounce(updateChartData, 200);
export default function GainsChart({ item, priceChange }: { item: Position; priceChange: number }) {
const chartRef = useRef<any>();
const [booted, setBooted] = useState<boolean>(false);
const [canvas, setCanvas] = useState<any>();
useEffect(() => {
if (chartRef.current.chartInstance) updateChartData({ item, priceChange, chartRef });
if (chartRef.current.chartInstance) debouncedUpdateChartData({ item, priceChange, chartRef });
}, [priceChange, item.leverage]);
useEffect(() => {

View File

@ -1,16 +1,16 @@
import { Button, Card, Radio } from 'antd';
import { Button, Card } 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';
import { useLeverage } from './leverage';
import CollateralInput from '../../../components/CollateralInput';
import { usePoolAndTradeInfoFrom } from './utils';
import { UserDeposit } from '../../../hooks';
import { ArrowDownOutlined } from '@ant-design/icons';
import { useWallet } from '../../../contexts/wallet';
interface NewPositionFormProps {
lendingReserve: ParsedAccount<LendingReserve>;
@ -18,6 +18,49 @@ interface NewPositionFormProps {
setNewPosition: (pos: Position) => void;
}
export const generateActionLabel = (connected: boolean, newPosition: Position) => {
return !connected ? LABELS.CONNECT_LABEL : newPosition.error ? newPosition.error : LABELS.TRADING_ADD_POSITION;
};
function onUserChangesLeverageOrCollateralValue({
newPosition,
setNewPosition,
collateralDeposit,
enrichedPools,
}: {
newPosition: Position;
setNewPosition: (pos: Position) => void;
enrichedPools: any[];
collateralDeposit: UserDeposit | undefined;
}) {
setNewPosition(newPosition); // It has always changed, need to guarantee save
// if user changes leverage, we need to adjust the amount they desire up.
if (collateralDeposit && enrichedPools.length) {
const exchangeRate = enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
const convertedAmount = (newPosition.collateral.value || 0) * newPosition.leverage * exchangeRate;
setNewPosition({ ...newPosition, asset: { ...newPosition.asset, value: convertedAmount } });
}
}
function onUserChangesAssetValue({
newPosition,
setNewPosition,
collateralDeposit,
enrichedPools,
}: {
newPosition: Position;
setNewPosition: (pos: Position) => void;
enrichedPools: any[];
collateralDeposit: UserDeposit | undefined;
}) {
setNewPosition(newPosition); // It has always changed, need to guarantee save
if (collateralDeposit && enrichedPools.length) {
const exchangeRate = enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
const convertedAmount = (newPosition.asset.value || 0) / (exchangeRate * newPosition.leverage);
setNewPosition({ ...newPosition, collateral: { ...newPosition.collateral, value: convertedAmount } });
}
}
export default function NewPositionForm({ lendingReserve, newPosition, setNewPosition }: NewPositionFormProps) {
const bodyStyle: React.CSSProperties = {
display: 'flex',
@ -27,11 +70,12 @@ export default function NewPositionForm({ lendingReserve, newPosition, setNewPos
height: '100%',
};
const [showConfirmation, setShowConfirmation] = useState(false);
const { enrichedPools, collateralDeposit } = usePoolAndTradeInfoFrom(newPosition);
useLeverage({ newPosition, setNewPosition });
const { wallet, connected } = useWallet();
return (
<Card className='new-position-item new-position-item-left' bodyStyle={bodyStyle}>
<Card className='new-position-item new-position-item-top-left' bodyStyle={bodyStyle}>
{showConfirmation ? (
<ActionConfirmation onClose={() => setShowConfirmation(false)} />
) : (
@ -42,78 +86,76 @@ export default function NewPositionForm({ lendingReserve, newPosition, setNewPos
justifyContent: 'space-around',
}}
>
<p>{newPosition.error}</p>
<p>{LABELS.MARGIN_TRADE_CHOOSE_COLLATERAL_AND_LEVERAGE}</p>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center' }}>
<CollateralSelector
<CollateralInput
title='Collateral'
reserve={lendingReserve.info}
amount={newPosition.collateral.value}
onInputChange={(val: number | null) => {
const newPos = { ...newPosition, collateral: { ...newPosition.collateral, value: val } };
onUserChangesLeverageOrCollateralValue({
newPosition: newPos,
setNewPosition,
enrichedPools,
collateralDeposit,
});
}}
onCollateralReserve={(key) => {
const id: string = cache.byParser(LendingReserveParser).find((acc) => acc === key) || '';
const parser = cache.get(id) as ParsedAccount<LendingReserve>;
setNewPosition({ ...newPosition, collateral: parser });
const newPos = { ...newPosition, collateral: { value: newPosition.collateral.value, type: parser } };
onUserChangesLeverageOrCollateralValue({
newPosition: newPos,
setNewPosition,
enrichedPools,
collateralDeposit,
});
}}
showLeverageSelector={true}
onLeverage={(leverage: number) => {
const newPos = { ...newPosition, leverage };
onUserChangesLeverageOrCollateralValue({
newPosition: newPos,
setNewPosition,
enrichedPools,
collateralDeposit,
});
}}
leverage={newPosition.leverage}
/>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<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={{
maxWidth: '75px',
}}
onChange={(leverage: number) => {
setNewPosition({ ...newPosition, leverage });
}}
/>
</div>
</div>
<br />
<p>{LABELS.MARGIN_TRADE_QUESTION}</p>
<ArrowDownOutlined />
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'stretch' }}>
<div className='token-input'>
<TokenIcon mintAddress={newPosition.asset.type?.info?.liquidityMint?.toBase58()} />
<NumericInput
value={newPosition.asset.value}
style={{
fontSize: 20,
boxShadow: 'none',
borderColor: 'transparent',
outline: 'transparent',
}}
onChange={(v: string) => {
setNewPosition({
...newPosition,
asset: { ...newPosition.asset, value: v },
{newPosition.asset.type && (
<CollateralInput
title='Choose trade'
reserve={newPosition.asset.type.info}
amount={newPosition.asset.value}
onInputChange={(val: number | null) => {
const newPos = { ...newPosition, asset: { ...newPosition.asset, value: val } };
onUserChangesAssetValue({
newPosition: newPos,
setNewPosition,
enrichedPools,
collateralDeposit,
});
}}
placeholder='0.00'
disabled
hideBalance={true}
/>
<div>
{
tokens.find((t) => t.mintAddress === newPosition.asset.type?.info?.liquidityMint?.toBase58())
?.tokenSymbol
}
</div>
</div>
<Button type='primary'>
<span>{LABELS.TRADING_ADD_POSITION}</span>
)}
<Button
className='trade-button'
type='primary'
size='large'
onClick={connected ? null : wallet.connect}
style={{ width: '100%' }}
disabled={connected && !!newPosition.error}
>
<span>{generateActionLabel(connected, newPosition)}</span>
</Button>
</div>
<Breakdown item={newPosition} />
</div>
)}
</Card>

View File

@ -0,0 +1,21 @@
import Card from 'antd/lib/card';
import React from 'react';
import { PoolPrice } from '../../../components/PoolPrice';
import { SupplyOverview } from '../../../components/SupplyOverview';
import { Position } from './interfaces';
import { usePoolAndTradeInfoFrom } from './utils';
export default function PoolHealth({ newPosition }: { newPosition: Position }) {
const { pool } = usePoolAndTradeInfoFrom(newPosition);
return (
<Card className='new-position-item new-position-item-bottom-left'>
{!pool && <span>Choose a CCY to see exchange rate information.</span>}
{pool && (
<>
<PoolPrice pool={pool} />
<SupplyOverview pool={pool} />
</>
)}
</Card>
);
}

View File

@ -1,13 +1,12 @@
import React, { useState } from 'react';
import { useLendingReserve, useTokenName } from '../../../hooks';
import { useLendingReserve } 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';
import Breakdown from './Breakdown';
import PoolHealth from './PoolHealth';
export const NewPosition = () => {
const { id } = useParams<{ id: string }>();
@ -15,7 +14,8 @@ export const NewPosition = () => {
const [newPosition, setNewPosition] = useState<Position>({
id: null,
leverage: 1,
asset: { value: '0' },
collateral: {},
asset: {},
});
if (!lendingReserve) {
@ -29,12 +29,11 @@ export const NewPosition = () => {
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 className='new-position-item-left'>
<NewPositionForm lendingReserve={lendingReserve} newPosition={newPosition} setNewPosition={setNewPosition} />
<PoolHealth newPosition={newPosition} />
</div>
<Breakdown item={newPosition} />
</div>
</div>
);

View File

@ -10,10 +10,13 @@ export interface Token {
export interface Position {
id?: number | null;
leverage: number;
collateral?: ParsedAccount<LendingReserve>;
collateral: {
type?: ParsedAccount<LendingReserve>;
value?: number | null;
};
asset: {
type?: ParsedAccount<LendingReserve>;
value: string; // because NumericInput returns strings and I dont want to deal with fixing it right now
value?: number | null;
};
error?: string;
}

View File

@ -1,9 +1,7 @@
import { useEffect } from 'react';
import { LABELS } from '../../../constants';
import { useEnrichedPools } from '../../../contexts/market';
import { useUserDeposits } from '../../../hooks';
import { usePoolForBasket } from '../../../utils/pools';
import { Position } from './interfaces';
import { usePoolAndTradeInfoFrom } from './utils';
export function useLeverage({
newPosition,
@ -12,52 +10,57 @@ export function useLeverage({
newPosition: Position;
setNewPosition: (pos: Position) => void;
}) {
const collType = newPosition.collateral;
const desiredType = newPosition.asset.type;
const pool = usePoolForBasket([
collType?.info?.liquidityMint?.toBase58(),
desiredType?.info?.liquidityMint?.toBase58(),
]);
const userDeposits = useUserDeposits();
const collateralDeposit = userDeposits.userDeposits.find(
(u) => u.reserve.info.liquidityMint.toBase58() == collType?.info?.liquidityMint?.toBase58()
);
const enriched = useEnrichedPools(pool ? [pool] : []);
const {
enrichedPools,
collateralDeposit,
collType,
desiredType,
collValue,
desiredValue,
leverage,
} = usePoolAndTradeInfoFrom(newPosition);
// Leverage validation - if you choose this leverage, is it allowable, with your buying power and with
// the pool we have to cover you?
useEffect(() => {
if (!collType) {
setNewPosition({ ...newPosition, error: LABELS.NO_COLL_TYPE_MESSAGE });
return;
}
if (!collateralDeposit) {
setNewPosition({ ...newPosition, error: LABELS.NO_DEPOSIT_MESSAGE });
return;
}
if (!collType || !desiredType || !newPosition.asset.value || !enriched || enriched.length == 0) {
if (!desiredType || !newPosition.asset.value || !enrichedPools || enrichedPools.length == 0) {
return;
}
// If there is more of A than B
const exchangeRate = enriched[0].liquidityB / enriched[0].liquidityA;
const amountDesiredToPurchase = parseFloat(newPosition.asset.value);
const exchangeRate = enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
const leverageDesired = newPosition.leverage;
const amountAvailableInOysterForMargin = collateralDeposit.info.amount * exchangeRate;
const amountToDepositOnMargin = amountDesiredToPurchase / leverageDesired;
const amountToDepositOnMargin = desiredValue / leverageDesired;
if (amountToDepositOnMargin > amountAvailableInOysterForMargin) {
setNewPosition({ ...newPosition, error: LABELS.NOT_ENOUGH_MARGIN_MESSAGE });
return;
}
const liqA = enriched[0].liquidityA;
const liqB = enriched[0].liquidityB;
if (amountToDepositOnMargin > collValue) {
setNewPosition({ ...newPosition, error: LABELS.SET_MORE_MARGIN_MESSAGE });
return;
}
const liqA = enrichedPools[0].liquidityA;
const liqB = enrichedPools[0].liquidityB;
const supplyRatio = liqA / liqB;
// change in liquidity is amount desired (in units of B) converted to collateral units(A)
const chgLiqA = amountDesiredToPurchase / exchangeRate;
const chgLiqA = desiredValue / exchangeRate;
const newLiqA = liqA - chgLiqA;
const newLiqB = liqB + amountDesiredToPurchase;
const newLiqB = liqB + desiredValue;
const newSupplyRatio = newLiqA / newLiqB;
const priceImpact = Math.abs(100 - 100 * (newSupplyRatio / supplyRatio));
@ -69,5 +72,5 @@ export function useLeverage({
return;
}
setNewPosition({ ...newPosition, error: '' });
}, [collType, desiredType, newPosition.asset.value, newPosition.leverage, enriched]);
}, [collType, desiredType, desiredValue, leverage, enrichedPools, collValue, collateralDeposit?.info.amount]);
}

View File

@ -16,11 +16,25 @@
}
.new-position-item-left {
flex: 60%;
flex: 1;
flex-direction: column;
}
.new-position-item-top-left {
flex: 1;
}
.new-position-item-bottom-left {
flex: 1;
}
.new-position-item-top-right {
flex: 1;
}
.new-position-item-bottom-right {
flex: 1;
}
.new-position-item-right {
flex: 30%;
flex: 1;
flex-direction: column;
}
/* Responsive layout - makes a one column layout instead of a two-column layout */

View File

@ -0,0 +1,48 @@
import { useEffect } from 'react';
import { ParsedAccount } from '../../../contexts/accounts';
import { useEnrichedPools } from '../../../contexts/market';
import { UserDeposit, useUserDeposits } from '../../../hooks';
import { LendingReserve, PoolInfo } from '../../../models';
import { usePoolForBasket } from '../../../utils/pools';
import { Position } from './interfaces';
export function usePoolAndTradeInfoFrom(
newPosition: Position
): {
enrichedPools: any[];
collateralDeposit: UserDeposit | undefined;
collType: ParsedAccount<LendingReserve> | undefined;
desiredType: ParsedAccount<LendingReserve> | undefined;
collValue: number;
desiredValue: number;
leverage: number;
pool: PoolInfo | undefined;
} {
const collType = newPosition.collateral.type;
const desiredType = newPosition.asset.type;
const collValue = newPosition.collateral.value || 0;
const desiredValue = newPosition.asset.value || 0;
const pool = usePoolForBasket([
collType?.info?.liquidityMint?.toBase58(),
desiredType?.info?.liquidityMint?.toBase58(),
]);
const userDeposits = useUserDeposits();
const collateralDeposit = userDeposits.userDeposits.find(
(u) => u.reserve.info.liquidityMint.toBase58() == collType?.info?.liquidityMint?.toBase58()
);
const enrichedPools = useEnrichedPools(pool ? [pool] : []);
return {
enrichedPools,
collateralDeposit,
collType,
desiredType,
collValue,
desiredValue,
leverage: newPosition.leverage,
pool,
};
}