Skip to content

Commit 7419360

Browse files
committed
feat: improve Trade features
1 parent 25732f7 commit 7419360

29 files changed

Lines changed: 306 additions & 68 deletions

File tree

.changeset/open-coats-make.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@venusprotocol/ui": minor
3+
"@venusprotocol/evm": minor
4+
---
5+
6+
improve Trade features

apps/evm/src/__mocks__/models/trade.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ export const tradePositions: TradePosition[] = [
117117
value: xvsAsset.userSupplyBalanceTokens,
118118
token: xvsAsset.vToken.underlyingToken,
119119
}),
120+
dsaUtilizedBalanceMantissa: convertTokensToMantissa({
121+
value: xvsAsset.userSupplyBalanceTokens.minus(1),
122+
token: xvsAsset.vToken.underlyingToken,
123+
}),
120124
longVTokenAddress: usdtAsset.vToken.address,
121125
shortVTokenAddress: busdAsset.vToken.address,
122126
leverageFactor: 2,
@@ -132,6 +136,10 @@ export const tradePositions: TradePosition[] = [
132136
value: usdcAsset.userSupplyBalanceTokens,
133137
token: usdcAsset.vToken.underlyingToken,
134138
}),
139+
dsaUtilizedBalanceMantissa: convertTokensToMantissa({
140+
value: usdcAsset.userSupplyBalanceTokens.minus(1),
141+
token: usdcAsset.vToken.underlyingToken,
142+
}),
135143
longVTokenAddress: usdtAsset.vToken.address,
136144
shortVTokenAddress: busdAsset.vToken.address,
137145
leverageFactor: 3,
@@ -147,6 +155,10 @@ export const tradePositions: TradePosition[] = [
147155
value: usdcAsset.userSupplyBalanceTokens,
148156
token: usdcAsset.vToken.underlyingToken,
149157
}),
158+
dsaUtilizedBalanceMantissa: convertTokensToMantissa({
159+
value: usdcAsset.userSupplyBalanceTokens.minus(1),
160+
token: usdcAsset.vToken.underlyingToken,
161+
}),
150162
longVTokenAddress: usdcAsset.vToken.address,
151163
shortVTokenAddress: usdtAsset.vToken.address,
152164
leverageFactor: 1.5,

apps/evm/src/clients/api/queries/getRawTradePositions/__tests__/index.spec.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
import BigNumber from 'bignumber.js';
12
import type { PublicClient } from 'viem';
23
import type { Mock } from 'vitest';
34

45
import fakeAddress, { altAddress } from '__mocks__/models/address';
56
import { poolData } from '__mocks__/models/pools';
67
import tokens from '__mocks__/models/tokens';
7-
import { apiTradePositions, tradePositions } from '__mocks__/models/trade';
8+
import { apiTradePositions } from '__mocks__/models/trade';
89
import { logError } from 'libs/errors';
910
import { ChainId } from 'types';
10-
import { restService } from 'utilities';
11+
import { formatToTradePosition, restService } from 'utilities';
1112
import { type GetRawTradePositionsInput, getRawTradePositions } from '..';
1213
import { getPools } from '../../useGetPools/getPools';
1314

@@ -33,6 +34,25 @@ const fakeInput: GetRawTradePositionsInput = {
3334
tokens,
3435
};
3536

37+
const formatApiTradePosition = (apiTradePosition: (typeof apiTradePositions)[number]) =>
38+
formatToTradePosition({
39+
pool: poolData[0],
40+
chainId: fakeInput.chainId,
41+
positionAccountAddress: apiTradePosition.positionAccountAddress,
42+
dsaVTokenAddress: apiTradePosition.dsaVTokenAddress,
43+
dsaBalanceMantissa: new BigNumber(
44+
apiTradePosition.capitalUtilization.suppliedPrincipalMantissa || 0,
45+
),
46+
dsaUtilizedBalanceMantissa: new BigNumber(
47+
apiTradePosition.capitalUtilization.capitalUtilizedMantissa || 0,
48+
),
49+
longVTokenAddress: apiTradePosition.longVTokenAddress,
50+
shortVTokenAddress: apiTradePosition.shortVTokenAddress,
51+
leverageFactor: Number(apiTradePosition.effectiveLeverageRatio ?? 0),
52+
unrealizedPnlCents: Number(apiTradePosition.pnl?.unrealizedPnlUsd ?? 0) * 100,
53+
unrealizedPnlPercentage: Number(apiTradePosition.pnl?.unrealizedPnlRatio ?? 0) * 100,
54+
});
55+
3656
describe('getRawTradePositions', () => {
3757
beforeEach(() => {
3858
(restService as Mock).mockResolvedValue({
@@ -104,10 +124,10 @@ describe('getRawTradePositions', () => {
104124
it('returns positions in the correct format on success', async () => {
105125
const response = await getRawTradePositions(fakeInput);
106126

107-
const expectedPositions = tradePositions.slice(0, 2).map((position, index) => ({
108-
...position,
109-
unrealizedPnlPercentage: Number(apiTradePositions[index].pnl?.unrealizedPnlRatio ?? 0) * 100,
110-
}));
127+
const expectedPositions = apiTradePositions
128+
.slice(0, 2)
129+
.map(formatApiTradePosition)
130+
.filter(position => position !== undefined);
111131

112132
expect(response).toEqual({
113133
positions: expectedPositions,
@@ -164,7 +184,7 @@ describe('getRawTradePositions', () => {
164184
const response = await getRawTradePositions(fakeInput);
165185

166186
expect(response).toEqual({
167-
positions: [tradePositions[0]],
187+
positions: [formatApiTradePosition(apiTradePositions[0])],
168188
});
169189
expect(logError).toHaveBeenCalledTimes(1);
170190
expect(logError).toHaveBeenCalledWith(

apps/evm/src/clients/api/queries/getRawTradePositions/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,13 @@ export const getRawTradePositions = async ({
8888
apiTradePosition.capitalUtilization.suppliedPrincipalMantissa || 0,
8989
);
9090

91+
let dsaUtilizedBalanceMantissa = new BigNumber(
92+
apiTradePosition.capitalUtilization.capitalUtilizedMantissa || 0,
93+
);
94+
9195
if (userDsaAssetSupplyBalanceMantissa) {
9296
dsaBalanceMantissa = BigNumber.min(userDsaAssetSupplyBalanceMantissa, dsaBalanceMantissa);
97+
dsaUtilizedBalanceMantissa = BigNumber.min(dsaUtilizedBalanceMantissa, dsaBalanceMantissa);
9398
}
9499

95100
const tradePosition = formatToTradePosition({
@@ -99,6 +104,7 @@ export const getRawTradePositions = async ({
99104
longVTokenAddress: apiTradePosition.longVTokenAddress,
100105
shortVTokenAddress: apiTradePosition.shortVTokenAddress,
101106
dsaBalanceMantissa,
107+
dsaUtilizedBalanceMantissa,
102108
leverageFactor: Number(apiTradePosition.effectiveLeverageRatio ?? 0),
103109
unrealizedPnlCents: apiTradePosition.pnl
104110
? Number(apiTradePosition.pnl.unrealizedPnlUsd) * 100

apps/evm/src/components/LayeredValues/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { cn } from '@venusprotocol/ui';
22

33
export interface LayeredValuesProps {
4-
topValue: string | number;
4+
topValue: React.ReactNode;
55
bottomValue?: string | number;
66
className?: string;
77
bottomValueClassName?: string;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Slot } from '@radix-ui/react-slot';
2+
import { cn } from '@venusprotocol/ui';
3+
4+
export interface TickMarkProps extends React.HTMLAttributes<HTMLDivElement> {
5+
isActive?: boolean;
6+
asChild?: boolean;
7+
className?: string;
8+
}
9+
10+
export const TickMark: React.FC<TickMarkProps> = ({
11+
isActive = false,
12+
className,
13+
asChild,
14+
...otherProps
15+
}) => {
16+
const Comp = asChild ? Slot : 'div';
17+
18+
return (
19+
<Comp
20+
className={cn(
21+
'size-3 shrink-0 outline-hidden rounded-full border border-light-grey-disabled absolute',
22+
isActive ? 'bg-light-grey-active' : 'bg-dark-blue-hover',
23+
className,
24+
)}
25+
{...otherProps}
26+
/>
27+
);
28+
};
Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import * as SliderPrimitive from '@radix-ui/react-slider';
22
import { cn } from '@venusprotocol/ui';
3+
import { useState } from 'react';
4+
import { TickMark } from './TickMark';
5+
6+
const tickMarks = [25, 50, 75];
37

48
export interface SliderProps {
59
value: number;
@@ -21,34 +25,65 @@ export const Slider: React.FC<SliderProps> = ({
2125
disabled = false,
2226
className,
2327
rangeClassName,
24-
}) => (
25-
<SliderPrimitive.Root
26-
data-slot="slider"
27-
defaultValue={[min, max]}
28-
value={[value]}
29-
min={min}
30-
max={max}
31-
step={step}
32-
onValueChange={([newValue]) => onChange(newValue)}
33-
className={cn('relative flex w-full touch-none items-center select-none', className)}
34-
disabled={disabled}
35-
>
36-
<SliderPrimitive.Track
37-
data-slot="slider-track"
38-
className="bg-lightGrey relative grow overflow-hidden rounded-full h-2 w-full"
28+
}) => {
29+
const [isValueIndicatorVisible, setIsValueIndicatorVisible] = useState(false);
30+
31+
const showValueIndicator = () => setIsValueIndicatorVisible(true);
32+
const hideValueIndicator = () => setIsValueIndicatorVisible(false);
33+
34+
const getPercentageValue = (currValue: number) =>
35+
Math.round(((currValue - min) * 100) / (max - min));
36+
37+
const valuePercentage = getPercentageValue(value);
38+
39+
return (
40+
<SliderPrimitive.Root
41+
data-slot="slider"
42+
defaultValue={[min, max]}
43+
value={[value]}
44+
min={min}
45+
max={max}
46+
step={step}
47+
onValueChange={([newValue]) => onChange(newValue)}
48+
className={cn('relative flex w-full touch-none items-center select-none', className)}
49+
disabled={disabled}
3950
>
40-
<SliderPrimitive.Range
41-
data-slot="slider-range"
42-
className={cn('bg-blue absolute h-full', rangeClassName)}
43-
/>
44-
</SliderPrimitive.Track>
45-
46-
<SliderPrimitive.Thumb
47-
data-slot="slider-thumb"
48-
className={cn(
49-
'block size-5 shrink-0 outline-hidden rounded-full border-white border-4 bg-blue shadow-sm transition-[color,box-shadow]',
50-
!disabled && 'cursor-pointer',
51-
)}
52-
/>
53-
</SliderPrimitive.Root>
54-
);
51+
<SliderPrimitive.Track
52+
data-slot="slider-track"
53+
className="bg-dark-blue-hover relative grow overflow-hidden rounded-full h-2 w-full"
54+
>
55+
<SliderPrimitive.Range
56+
data-slot="slider-range"
57+
className={cn('bg-blue absolute h-full', rangeClassName)}
58+
/>
59+
</SliderPrimitive.Track>
60+
61+
{tickMarks.map(tickMark => (
62+
<TickMark
63+
key={tickMark}
64+
style={{
65+
left: `${tickMark}%`,
66+
marginLeft: `${4 - tickMark / 5}px`,
67+
}}
68+
isActive={getPercentageValue(tickMark) >= tickMark}
69+
/>
70+
))}
71+
72+
<SliderPrimitive.Thumb
73+
data-slot="slider-thumb"
74+
className={cn(
75+
'block relative size-5 shrink-0 outline-hidden rounded-full border-white border-4 bg-blue shadow-sm',
76+
!disabled && 'cursor-pointer',
77+
)}
78+
onMouseEnter={showValueIndicator}
79+
onMouseLeave={hideValueIndicator}
80+
>
81+
{isValueIndicatorVisible && (
82+
<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">
83+
{valuePercentage}%
84+
</div>
85+
)}
86+
</SliderPrimitive.Thumb>
87+
</SliderPrimitive.Root>
88+
);
89+
};

apps/evm/src/components/TokenListWrapper/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { TextField } from '../TextField';
1111
import { getTokenListItemTestId } from './testIdGetters';
1212

1313
export interface OptionalTokenBalance extends Omit<TokenBalance, 'balanceMantissa'> {
14+
isDeemed?: boolean;
1415
balanceMantissa?: BigNumber;
1516
}
1617

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

7071
// If b is non-negative and a is negative, b comes first
7172
return 1;
72-
}) as TokenBalance[],
73+
}) as OptionalTokenBalance[],
7374
[tokenBalances],
7475
);
7576

@@ -152,7 +153,10 @@ export const TokenListWrapper: React.FC<TokenListWrapperProps> = ({
152153
})
153154
}
154155
>
155-
<TokenIconWithSymbol token={tokenBalance.token} />
156+
<TokenIconWithSymbol
157+
token={tokenBalance.token}
158+
className={cn(tokenBalance.isDeemed && 'text-light-grey')}
159+
/>
156160

157161
{tokenBalance.balanceMantissa && (
158162
<Typography variant="small2" className="text-white">

apps/evm/src/hooks/useGetTradePositions/__tests__/__snapshots__/index.spec.ts.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ exports[`useGetTradePositions > enriches positions with wallet balances from the
110110
},
111111
"dsaBalanceCents": 11508.0606,
112112
"dsaBalanceTokens": "90",
113+
"dsaUtilizedBalanceCents": 11380.19326,
114+
"dsaUtilizedBalanceTokens": "89",
113115
"entryPriceTokens": "0.5",
114116
"leverageFactor": 2,
115117
"liquidationPriceTokens": "-0.09424715604122129993",
@@ -1268,6 +1270,8 @@ exports[`useGetTradePositions > falls back to the raw Trade position balances wh
12681270
},
12691271
"dsaBalanceCents": 11508.0606,
12701272
"dsaBalanceTokens": "90",
1273+
"dsaUtilizedBalanceCents": 11380.19326,
1274+
"dsaUtilizedBalanceTokens": "89",
12711275
"entryPriceTokens": "0.5",
12721276
"leverageFactor": 2,
12731277
"liquidationPriceTokens": "-0.09424715604122129993",
@@ -2426,6 +2430,8 @@ exports[`useGetTradePositions > passes the expected query params when the wallet
24262430
},
24272431
"dsaBalanceCents": 11508.0606,
24282432
"dsaBalanceTokens": "90",
2433+
"dsaUtilizedBalanceCents": 11380.19326,
2434+
"dsaUtilizedBalanceTokens": "89",
24292435
"entryPriceTokens": "0.5",
24302436
"leverageFactor": 2,
24312437
"liquidationPriceTokens": "-0.09424715604122129993",

apps/evm/src/hooks/useSimulateTradePositionMutations/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ export const useSimulateTradeMutations = ({
3434
});
3535
}
3636

37+
const dsaUtilizedBalanceTokens = BigNumber.min(
38+
position.dsaUtilizedBalanceTokens,
39+
dsaBalanceTokens,
40+
);
41+
3742
const simulatedTradePosition =
3843
simulatedPool &&
3944
formatToTradePosition({
@@ -43,6 +48,10 @@ export const useSimulateTradeMutations = ({
4348
value: dsaBalanceTokens,
4449
token: position.dsaAsset.vToken.underlyingToken,
4550
}),
51+
dsaUtilizedBalanceMantissa: convertTokensToMantissa({
52+
value: dsaUtilizedBalanceTokens,
53+
token: position.dsaAsset.vToken.underlyingToken,
54+
}),
4655
positionAccountAddress: position.positionAccountAddress,
4756
dsaVTokenAddress: position.dsaAsset.vToken.address,
4857
longVTokenAddress: position.longAsset.vToken.address,

0 commit comments

Comments
 (0)