Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/open-coats-make.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@venusprotocol/ui": minor
"@venusprotocol/evm": minor
---

improve Trade features
12 changes: 12 additions & 0 deletions apps/evm/src/__mocks__/models/trade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ export const tradePositions: TradePosition[] = [
value: xvsAsset.userSupplyBalanceTokens,
token: xvsAsset.vToken.underlyingToken,
}),
dsaUtilizedBalanceMantissa: convertTokensToMantissa({
value: xvsAsset.userSupplyBalanceTokens.minus(1),
token: xvsAsset.vToken.underlyingToken,
}),
longVTokenAddress: usdtAsset.vToken.address,
shortVTokenAddress: busdAsset.vToken.address,
leverageFactor: 2,
Expand All @@ -132,6 +136,10 @@ export const tradePositions: TradePosition[] = [
value: usdcAsset.userSupplyBalanceTokens,
token: usdcAsset.vToken.underlyingToken,
}),
dsaUtilizedBalanceMantissa: convertTokensToMantissa({
value: usdcAsset.userSupplyBalanceTokens.minus(1),
token: usdcAsset.vToken.underlyingToken,
}),
longVTokenAddress: usdtAsset.vToken.address,
shortVTokenAddress: busdAsset.vToken.address,
leverageFactor: 3,
Expand All @@ -147,6 +155,10 @@ export const tradePositions: TradePosition[] = [
value: usdcAsset.userSupplyBalanceTokens,
token: usdcAsset.vToken.underlyingToken,
}),
dsaUtilizedBalanceMantissa: convertTokensToMantissa({
value: usdcAsset.userSupplyBalanceTokens.minus(1),
token: usdcAsset.vToken.underlyingToken,
}),
longVTokenAddress: usdcAsset.vToken.address,
shortVTokenAddress: usdtAsset.vToken.address,
leverageFactor: 1.5,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import BigNumber from 'bignumber.js';
import type { PublicClient } from 'viem';
import type { Mock } from 'vitest';

import fakeAddress, { altAddress } from '__mocks__/models/address';
import { poolData } from '__mocks__/models/pools';
import tokens from '__mocks__/models/tokens';
import { apiTradePositions, tradePositions } from '__mocks__/models/trade';
import { apiTradePositions } from '__mocks__/models/trade';
import { logError } from 'libs/errors';
import { ChainId } from 'types';
import { restService } from 'utilities';
import { formatToTradePosition, restService } from 'utilities';
import { type GetRawTradePositionsInput, getRawTradePositions } from '..';
import { getPools } from '../../useGetPools/getPools';

Expand All @@ -33,6 +34,25 @@ const fakeInput: GetRawTradePositionsInput = {
tokens,
};

const formatApiTradePosition = (apiTradePosition: (typeof apiTradePositions)[number]) =>
formatToTradePosition({
pool: poolData[0],
chainId: fakeInput.chainId,
positionAccountAddress: apiTradePosition.positionAccountAddress,
dsaVTokenAddress: apiTradePosition.dsaVTokenAddress,
dsaBalanceMantissa: new BigNumber(
apiTradePosition.capitalUtilization.suppliedPrincipalMantissa || 0,
),
dsaUtilizedBalanceMantissa: new BigNumber(
apiTradePosition.capitalUtilization.capitalUtilizedMantissa || 0,
),
longVTokenAddress: apiTradePosition.longVTokenAddress,
shortVTokenAddress: apiTradePosition.shortVTokenAddress,
leverageFactor: Number(apiTradePosition.effectiveLeverageRatio ?? 0),
unrealizedPnlCents: Number(apiTradePosition.pnl?.unrealizedPnlUsd ?? 0) * 100,
unrealizedPnlPercentage: Number(apiTradePosition.pnl?.unrealizedPnlRatio ?? 0) * 100,
});

describe('getRawTradePositions', () => {
beforeEach(() => {
(restService as Mock).mockResolvedValue({
Expand Down Expand Up @@ -104,10 +124,10 @@ describe('getRawTradePositions', () => {
it('returns positions in the correct format on success', async () => {
const response = await getRawTradePositions(fakeInput);

const expectedPositions = tradePositions.slice(0, 2).map((position, index) => ({
...position,
unrealizedPnlPercentage: Number(apiTradePositions[index].pnl?.unrealizedPnlRatio ?? 0) * 100,
}));
const expectedPositions = apiTradePositions
.slice(0, 2)
.map(formatApiTradePosition)
.filter(position => position !== undefined);

expect(response).toEqual({
positions: expectedPositions,
Expand Down Expand Up @@ -164,7 +184,7 @@ describe('getRawTradePositions', () => {
const response = await getRawTradePositions(fakeInput);

expect(response).toEqual({
positions: [tradePositions[0]],
positions: [formatApiTradePosition(apiTradePositions[0])],
});
expect(logError).toHaveBeenCalledTimes(1);
expect(logError).toHaveBeenCalledWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,13 @@ export const getRawTradePositions = async ({
apiTradePosition.capitalUtilization.suppliedPrincipalMantissa || 0,
);

let dsaUtilizedBalanceMantissa = new BigNumber(
apiTradePosition.capitalUtilization.capitalUtilizedMantissa || 0,
);

if (userDsaAssetSupplyBalanceMantissa) {
dsaBalanceMantissa = BigNumber.min(userDsaAssetSupplyBalanceMantissa, dsaBalanceMantissa);
dsaUtilizedBalanceMantissa = BigNumber.min(dsaUtilizedBalanceMantissa, dsaBalanceMantissa);
}

const tradePosition = formatToTradePosition({
Expand All @@ -99,6 +104,7 @@ export const getRawTradePositions = async ({
longVTokenAddress: apiTradePosition.longVTokenAddress,
shortVTokenAddress: apiTradePosition.shortVTokenAddress,
dsaBalanceMantissa,
dsaUtilizedBalanceMantissa,
leverageFactor: Number(apiTradePosition.effectiveLeverageRatio ?? 0),
unrealizedPnlCents: apiTradePosition.pnl
? Number(apiTradePosition.pnl.unrealizedPnlUsd) * 100
Expand Down
2 changes: 1 addition & 1 deletion apps/evm/src/components/LayeredValues/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { cn } from '@venusprotocol/ui';

export interface LayeredValuesProps {
topValue: string | number;
topValue: React.ReactNode;
bottomValue?: string | number;
className?: string;
bottomValueClassName?: string;
Expand Down
28 changes: 28 additions & 0 deletions apps/evm/src/components/Slider/TickMark/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Slot } from '@radix-ui/react-slot';
import { cn } from '@venusprotocol/ui';

export interface TickMarkProps extends React.HTMLAttributes<HTMLDivElement> {
isActive?: boolean;
asChild?: boolean;
className?: string;
}

export const TickMark: React.FC<TickMarkProps> = ({
isActive = false,
className,
asChild,
...otherProps
}) => {
const Comp = asChild ? Slot : 'div';

return (
<Comp
className={cn(
'size-3 shrink-0 outline-hidden rounded-full border border-light-grey-disabled absolute',
isActive ? 'bg-light-grey-active' : 'bg-dark-blue-hover',
className,
)}
{...otherProps}
/>
);
};
92 changes: 62 additions & 30 deletions apps/evm/src/components/Slider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as SliderPrimitive from '@radix-ui/react-slider';
import { cn } from '@venusprotocol/ui';
import { useState } from 'react';
import { TickMark } from './TickMark';

const tickMarks = [25, 50, 75];

export interface SliderProps {
value: number;
Expand All @@ -21,34 +25,62 @@ export const Slider: React.FC<SliderProps> = ({
disabled = false,
className,
rangeClassName,
}) => (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={[min, max]}
value={[value]}
min={min}
max={max}
step={step}
onValueChange={([newValue]) => onChange(newValue)}
className={cn('relative flex w-full touch-none items-center select-none', className)}
disabled={disabled}
>
<SliderPrimitive.Track
data-slot="slider-track"
className="bg-lightGrey relative grow overflow-hidden rounded-full h-2 w-full"
}) => {
const [isValueIndicatorVisible, setIsValueIndicatorVisible] = useState(false);

const showValueIndicator = () => setIsValueIndicatorVisible(true);
const hideValueIndicator = () => setIsValueIndicatorVisible(false);

const valuePercentage = Math.round(((value - min) * 100) / (max - min));

return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={[min, max]}
value={[value]}
min={min}
max={max}
step={step}
onValueChange={([newValue]) => onChange(newValue)}
className={cn('relative flex w-full touch-none items-center select-none', className)}
disabled={disabled}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn('bg-blue absolute h-full', rangeClassName)}
/>
</SliderPrimitive.Track>

<SliderPrimitive.Thumb
data-slot="slider-thumb"
className={cn(
'block size-5 shrink-0 outline-hidden rounded-full border-white border-4 bg-blue shadow-sm transition-[color,box-shadow]',
!disabled && 'cursor-pointer',
)}
/>
</SliderPrimitive.Root>
);
<SliderPrimitive.Track
data-slot="slider-track"
className="bg-dark-blue-hover relative grow overflow-hidden rounded-full h-2 w-full"
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn('bg-blue absolute h-full', rangeClassName)}
/>
</SliderPrimitive.Track>

{tickMarks.map(tickMark => (
<TickMark
key={tickMark}
style={{
left: `${tickMark}%`,
marginLeft: `${4 - tickMark / 5}px`,
}}
isActive={valuePercentage >= tickMark}
/>
))}
Comment thread
therealemjy marked this conversation as resolved.

<SliderPrimitive.Thumb
data-slot="slider-thumb"
className={cn(
'block relative size-5 shrink-0 outline-hidden rounded-full border-white border-4 bg-blue shadow-sm',
!disabled && 'cursor-pointer',
)}
onMouseEnter={showValueIndicator}
onMouseLeave={hideValueIndicator}
>
{isValueIndicatorVisible && (
<div className="absolute left-[50%] translate-x-[-50%] bottom-5 px-1 py-0.5 bg-dark-blue-hover rounded-md text-center text-xs">
{valuePercentage}%
</div>
)}
</SliderPrimitive.Thumb>
</SliderPrimitive.Root>
);
};
8 changes: 6 additions & 2 deletions apps/evm/src/components/TokenListWrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { TextField } from '../TextField';
import { getTokenListItemTestId } from './testIdGetters';

export interface OptionalTokenBalance extends Omit<TokenBalance, 'balanceMantissa'> {
isDeemed?: boolean;
balanceMantissa?: BigNumber;
}

Expand Down Expand Up @@ -69,7 +70,7 @@ export const TokenListWrapper: React.FC<TokenListWrapperProps> = ({

// If b is non-negative and a is negative, b comes first
return 1;
}) as TokenBalance[],
}) as OptionalTokenBalance[],
[tokenBalances],
);

Expand Down Expand Up @@ -152,7 +153,10 @@ export const TokenListWrapper: React.FC<TokenListWrapperProps> = ({
})
}
>
<TokenIconWithSymbol token={tokenBalance.token} />
<TokenIconWithSymbol
token={tokenBalance.token}
className={cn(tokenBalance.isDeemed && 'text-light-grey')}
/>

{tokenBalance.balanceMantissa && (
<Typography variant="small2" className="text-white">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ exports[`useGetTradePositions > enriches positions with wallet balances from the
},
"dsaBalanceCents": 11508.0606,
"dsaBalanceTokens": "90",
"dsaUtilizedBalanceCents": 11380.19326,
"dsaUtilizedBalanceTokens": "89",
"entryPriceTokens": "0.5",
"leverageFactor": 2,
"liquidationPriceTokens": "-0.09424715604122129993",
Expand Down Expand Up @@ -1268,6 +1270,8 @@ exports[`useGetTradePositions > falls back to the raw Trade position balances wh
},
"dsaBalanceCents": 11508.0606,
"dsaBalanceTokens": "90",
"dsaUtilizedBalanceCents": 11380.19326,
"dsaUtilizedBalanceTokens": "89",
"entryPriceTokens": "0.5",
"leverageFactor": 2,
"liquidationPriceTokens": "-0.09424715604122129993",
Expand Down Expand Up @@ -2426,6 +2430,8 @@ exports[`useGetTradePositions > passes the expected query params when the wallet
},
"dsaBalanceCents": 11508.0606,
"dsaBalanceTokens": "90",
"dsaUtilizedBalanceCents": 11380.19326,
"dsaUtilizedBalanceTokens": "89",
"entryPriceTokens": "0.5",
"leverageFactor": 2,
"liquidationPriceTokens": "-0.09424715604122129993",
Expand Down
9 changes: 9 additions & 0 deletions apps/evm/src/hooks/useSimulateTradePositionMutations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export const useSimulateTradeMutations = ({
});
}

const dsaUtilizedBalanceTokens = BigNumber.min(
position.dsaUtilizedBalanceTokens,
dsaBalanceTokens,
);

const simulatedTradePosition =
simulatedPool &&
formatToTradePosition({
Expand All @@ -43,6 +48,10 @@ export const useSimulateTradeMutations = ({
value: dsaBalanceTokens,
token: position.dsaAsset.vToken.underlyingToken,
}),
dsaUtilizedBalanceMantissa: convertTokensToMantissa({
value: dsaUtilizedBalanceTokens,
token: position.dsaAsset.vToken.underlyingToken,
}),
positionAccountAddress: position.positionAccountAddress,
dsaVTokenAddress: position.dsaAsset.vToken.address,
longVTokenAddress: position.longAsset.vToken.address,
Expand Down
8 changes: 6 additions & 2 deletions apps/evm/src/libs/translations/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,9 @@
"collateralColumn": {
"label": "Collateral"
},
"collateralUtilizationColumn": {
"label": "Collateral utilization"
},
"healthFactor": {
"label": "Health factor",
"tooltip": "Liquidation at < 1.0"
Expand All @@ -1724,10 +1727,11 @@
"table": {
"entryPrice": {
"title": "EP",
"tooltip": "The entry price ratio of this position (short asset amount ÷ long asset amount), reflecting the relative value between the two tokens from entry"
"tooltip": "The entry price ratio of this position (short asset amount ÷ long asset amount, or long asset amount ÷ short asset amount, depending on the selected base token), reflecting the relative value between the two tokens at entry."
},
"liquidationPrice": {
"title": "Liq."
"title": "Liq.",
"tooltip": "The liquidation price is calculated based on the current base token price and may change as the base token price moves. Always keep an eye on the Health Factor (HF)."
},
"longColumn": {
"title": "Long"
Expand Down
8 changes: 6 additions & 2 deletions apps/evm/src/libs/translations/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,9 @@
"collateralColumn": {
"label": "担保"
},
"collateralUtilizationColumn": {
"label": "担保利用率"
},
"healthFactor": {
"label": "健全性ファクター",
"tooltip": "清算ライン: < 1.0"
Expand All @@ -1724,10 +1727,11 @@
"table": {
"entryPrice": {
"title": "EP",
"tooltip": "このポジションのエントリー価格比率(ショート資産量 ÷ ロング資産量)で、エントリー時点からの2つのトークン間の相対的な価値を示します"
"tooltip": "このポジションのエントリー価格比率です(ショート資産量 ÷ ロング資産量、または選択したベーストークンに応じてロング資産量 ÷ ショート資産量)。エントリー時点での2つのトークン間の相対的な価値を示します。"
},
"liquidationPrice": {
"title": "Liq."
"title": "Liq.",
"tooltip": "清算価格は現在のベーストークン価格に基づいて計算され、ベーストークン価格の変動に応じて変わる可能性があります。ヘルスファクター(HF)を常に確認してください。"
},
"longColumn": {
"title": "ロング"
Expand Down
Loading
Loading