Skip to content

Commit e18dd33

Browse files
committed
feat: add Repay with collateral UI
1 parent eaf9b9f commit e18dd33

File tree

60 files changed

+2448
-222
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2448
-222
lines changed

.changeset/quiet-dots-call.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@venusprotocol/evm": minor
3+
---
4+
5+
add Repay with collateral UI
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ExactInSwapQuote } from 'types';
2+
import { lisUsd, usdc } from './tokens';
3+
4+
export const exactInSwapQuote: ExactInSwapQuote = {
5+
fromToken: usdc,
6+
toToken: lisUsd,
7+
direction: 'exact-in',
8+
priceImpactPercentage: 0.1,
9+
fromTokenAmountSoldMantissa: 100000000n,
10+
expectedToTokenAmountReceivedMantissa: 100000000n,
11+
minimumToTokenAmountReceivedMantissa: 100000000n,
12+
callData: '0x',
13+
};

apps/evm/src/clients/api/__mocks__/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,14 @@ export const useOpenLeveragedPosition = vi.fn(
695695
}),
696696
);
697697

698+
export const useRepayWithCollateral = vi.fn(
699+
(_variables: never, options?: MutationObserverOptions) =>
700+
useMutation({
701+
mutationFn: vi.fn(),
702+
...options,
703+
}),
704+
);
705+
698706
export const withdrawXvs = vi.fn();
699707
export const useWithdrawXvs = (options?: MutationObserverOptions) =>
700708
useMutation({

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export * from './mutations/useWithdraw';
3232
export * from './mutations/useImportSupplyPosition';
3333
export * from './mutations/useSetEModeGroup';
3434
export * from './mutations/useOpenLeveragedPosition';
35+
export * from './mutations/useRepayWithCollateral';
3536

3637
// Queries
3738
export * from './queries/getVaiTreasuryPercentage';

apps/evm/src/clients/api/mutations/useImportSupplyPosition/__tests__/__snapshots__/index.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ exports[`useImportSupplyPosition > returns a Fusion quote on success 6`] = `
8787
"sponsorship": true,
8888
"trigger": {
8989
"amount": 20000000000000000000n,
90-
"approvalAmount": 24024000000000000000n,
90+
"approvalAmount": 24000002400000000000n,
9191
"chainId": 97,
9292
"gasLimit": 500000n,
9393
"tokenAddress": "0xfakeAXVSAddress",

apps/evm/src/clients/api/mutations/useOpenLeveragedPosition/__tests__/index.spec.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,12 @@
1-
import { lisUsd, usdc } from '__mocks__/models/tokens';
1+
import { exactInSwapQuote as fakeSwapQuote } from '__mocks__/models/swap';
22
import { vLisUSD, vUsdc } from '__mocks__/models/vTokens';
33
import { queryClient } from 'clients/api';
44
import { useGetContractAddress } from 'hooks/useGetContractAddress';
55
import { useSendTransaction } from 'hooks/useSendTransaction';
66
import { renderHook } from 'testUtils/render';
7-
import type { ExactInSwapQuote } from 'types';
87
import type { Mock } from 'vitest';
98
import { useOpenLeveragedPosition } from '..';
109

11-
const fakeSwapQuote: ExactInSwapQuote = {
12-
fromToken: usdc,
13-
toToken: lisUsd,
14-
direction: 'exact-in',
15-
priceImpactPercentage: 0.1,
16-
fromTokenAmountSoldMantissa: 100000000n,
17-
expectedToTokenAmountReceivedMantissa: 100000000n,
18-
minimumToTokenAmountReceivedMantissa: 100000000n,
19-
callData: '0x',
20-
};
21-
2210
vi.mock('libs/contracts');
2311

2412
describe('useOpenLeveragedPosition', () => {

apps/evm/src/clients/api/mutations/useRepay/__tests__/index.spec.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ describe('useRepay', () => {
9292
expect(await fn(repayFullLoanInput)).toMatchInlineSnapshot(
9393
{
9494
abi: expect.any(Array),
95-
},
96-
`
95+
}, `
9796
{
9897
"abi": Any<Array>,
9998
"address": "0xfakeMaximillionContractAddress",
@@ -102,10 +101,9 @@ describe('useRepay', () => {
102101
"0x2E7222e51c0f6e98610A1543Aa3836E092CDe62c",
103102
],
104103
"functionName": "repayBehalfExplicit",
105-
"value": 10010000000000000n,
104+
"value": 10000001000000000n,
106105
}
107-
`,
108-
);
106+
`);
109107

110108
onConfirmed({ input: fakeInput });
111109

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`useRepayWithCollateral > calls useSendTransaction with correct parameters 'with single asset' 1`] = `
4+
{
5+
"abi": Any<Object>,
6+
"address": "0xfakeLeverageManagerContractAddress",
7+
"args": [
8+
"0xD5C4C2e2facBEB59D0216D0595d63FcDc6F9A1a7",
9+
100000000n,
10+
],
11+
"functionName": "exitSingleAssetLeverage",
12+
}
13+
`;
14+
15+
exports[`useRepayWithCollateral > calls useSendTransaction with correct parameters 'with single asset' 2`] = `
16+
[
17+
[
18+
{
19+
"queryKey": [
20+
"GET_POOLS",
21+
],
22+
},
23+
],
24+
]
25+
`;
26+
27+
exports[`useRepayWithCollateral > calls useSendTransaction with correct parameters 'with swapQuote' 1`] = `
28+
{
29+
"abi": Any<Object>,
30+
"address": "0xfakeLeverageManagerContractAddress",
31+
"args": [
32+
"0xD5C4C2e2facBEB59D0216D0595d63FcDc6F9A1a7",
33+
100000000n,
34+
"0x170d3b2da05cc2124334240fB34ad1359e34C562",
35+
100000000n,
36+
100000000n,
37+
"0x",
38+
],
39+
"functionName": "exitLeverage",
40+
}
41+
`;
42+
43+
exports[`useRepayWithCollateral > calls useSendTransaction with correct parameters 'with swapQuote' 2`] = `
44+
[
45+
[
46+
{
47+
"queryKey": [
48+
"GET_POOLS",
49+
],
50+
},
51+
],
52+
]
53+
`;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import fakeAccountAddress from '__mocks__/models/address';
2+
import { exactInSwapQuote as fakeSwapQuote } from '__mocks__/models/swap';
3+
import { vLisUSD, vUsdc } from '__mocks__/models/vTokens';
4+
import { queryClient } from 'clients/api';
5+
import { useGetContractAddress } from 'hooks/useGetContractAddress';
6+
import { useSendTransaction } from 'hooks/useSendTransaction';
7+
import { renderHook } from 'testUtils/render';
8+
import type { Mock } from 'vitest';
9+
import { useRepayWithCollateral } from '..';
10+
11+
vi.mock('libs/contracts');
12+
13+
describe('useRepayWithCollateral', () => {
14+
it('should throw error if LeverageManager contract address is not available', async () => {
15+
(useGetContractAddress as Mock).mockReturnValue({ address: undefined });
16+
17+
renderHook(() => useRepayWithCollateral());
18+
19+
const { fn } = (useSendTransaction as Mock).mock.calls[0][0];
20+
21+
expect(async () => fn()).rejects.toThrow('somethingWentWrong');
22+
});
23+
24+
it.each([
25+
{
26+
label: 'with swapQuote',
27+
input: {
28+
collateralVToken: vUsdc,
29+
repaidVToken: vLisUSD,
30+
swapQuote: fakeSwapQuote,
31+
},
32+
},
33+
{
34+
label: 'with single asset',
35+
input: {
36+
vToken: vUsdc,
37+
amountMantissa: 100000000n,
38+
},
39+
},
40+
])('calls useSendTransaction with correct parameters $label', async ({ input }) => {
41+
renderHook(() => useRepayWithCollateral(), {
42+
accountAddress: fakeAccountAddress,
43+
});
44+
45+
expect(useSendTransaction).toHaveBeenCalledWith({
46+
fn: expect.any(Function),
47+
onConfirmed: expect.any(Function),
48+
options: undefined,
49+
});
50+
51+
const { fn } = (useSendTransaction as Mock).mock.calls[0][0];
52+
53+
expect(await fn(input)).toMatchSnapshot({
54+
abi: expect.any(Object),
55+
});
56+
57+
const { onConfirmed } = (useSendTransaction as Mock).mock.calls[0][0];
58+
await onConfirmed();
59+
60+
expect((queryClient.invalidateQueries as Mock).mock.calls).toMatchSnapshot();
61+
});
62+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { Account, Address, Chain, Hex, WriteContractParameters } from 'viem';
2+
3+
import { queryClient } from 'clients/api';
4+
import FunctionKey from 'constants/functionKey';
5+
import { useGetContractAddress } from 'hooks/useGetContractAddress';
6+
import { type UseSendTransactionOptions, useSendTransaction } from 'hooks/useSendTransaction';
7+
import { leverageManagerAbi } from 'libs/contracts';
8+
import { VError } from 'libs/errors';
9+
import type { SwapQuote, VToken } from 'types';
10+
11+
type RepayWithCollateralWithSwapInput = {
12+
swapQuote: SwapQuote;
13+
collateralVToken: VToken;
14+
repaidVToken: VToken;
15+
};
16+
17+
type RepayWithCollateralWithSingleAssetInput = {
18+
vToken: VToken;
19+
amountMantissa: bigint;
20+
};
21+
22+
type RepayWithCollateralInput =
23+
| RepayWithCollateralWithSwapInput
24+
| RepayWithCollateralWithSingleAssetInput;
25+
26+
type Options = UseSendTransactionOptions<RepayWithCollateralInput>;
27+
28+
export const useRepayWithCollateral = (options?: Partial<Options>) => {
29+
const { address: leverageManagerContractAddress } = useGetContractAddress({
30+
name: 'LeverageManager',
31+
});
32+
33+
return useSendTransaction({
34+
fn: (input: RepayWithCollateralInput) => {
35+
if (!leverageManagerContractAddress) {
36+
throw new VError({ type: 'unexpected', code: 'somethingWentWrong' });
37+
}
38+
39+
if ('swapQuote' in input) {
40+
const collateralAmountMantissa =
41+
input.swapQuote.direction === 'exact-in' ||
42+
input.swapQuote.direction === 'approximate-out'
43+
? input.swapQuote.fromTokenAmountSoldMantissa
44+
: input.swapQuote.maximumFromTokenAmountSoldMantissa;
45+
46+
const repaidAmountMantissa =
47+
input.swapQuote.direction === 'exact-in' ||
48+
input.swapQuote.direction === 'approximate-out'
49+
? input.swapQuote.minimumToTokenAmountReceivedMantissa
50+
: input.swapQuote.toTokenAmountReceivedMantissa;
51+
52+
return {
53+
abi: leverageManagerAbi,
54+
address: leverageManagerContractAddress,
55+
functionName: 'exitLeverage',
56+
args: [
57+
input.collateralVToken.address,
58+
collateralAmountMantissa,
59+
input.repaidVToken.address,
60+
repaidAmountMantissa, // borrowedAmountToFlashLoan
61+
repaidAmountMantissa, // minAmountOutAfterSwap: this needs to correspond to
62+
// borrowedAmountToFlashLoan + swap fee (which is currently 0%, hence why it is
63+
// currently equal to borrowedAmountToFlashLoan)
64+
input.swapQuote.callData,
65+
],
66+
} as WriteContractParameters<
67+
typeof leverageManagerAbi,
68+
'exitLeverage',
69+
readonly [Address, bigint, Address, bigint, bigint, Hex],
70+
Chain,
71+
Account
72+
>;
73+
}
74+
75+
return {
76+
abi: leverageManagerAbi,
77+
address: leverageManagerContractAddress,
78+
functionName: 'exitSingleAssetLeverage',
79+
args: [input.vToken.address, input.amountMantissa],
80+
} as WriteContractParameters<
81+
typeof leverageManagerAbi,
82+
'exitSingleAssetLeverage',
83+
readonly [Address, bigint],
84+
Chain,
85+
Account
86+
>;
87+
},
88+
onConfirmed: () => {
89+
// TODO: send analytic event
90+
91+
queryClient.invalidateQueries({
92+
queryKey: [FunctionKey.GET_POOLS],
93+
});
94+
},
95+
options,
96+
});
97+
};

0 commit comments

Comments
 (0)