Skip to content
Merged
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/light-zebras-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@venusprotocol/chains": minor
"@venusprotocol/evm": minor
---

update swap & supply feature
2 changes: 1 addition & 1 deletion apps/evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,4 @@
"last 1 safari version"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fakeAccountAddress from '__mocks__/models/address';
import { usdc, xvs } from '__mocks__/models/tokens';
import { bnb, usdc, xvs } from '__mocks__/models/tokens';
import { vXvs } from '__mocks__/models/vTokens';
import BigNumber from 'bignumber.js';
import { queryClient } from 'clients/api';
Expand All @@ -18,7 +18,7 @@ const mockPoolComptrollerAddress = '0x456' as Address;
const mockPoolName = 'Test Pool';

const mockSwap = {
direction: 'exactAmountIn' as const,
direction: 'exact-in' as const,
fromToken: xvs,
toToken: usdc,
fromTokenAmountSoldMantissa: new BigNumber(1000),
Expand Down Expand Up @@ -49,7 +49,7 @@ describe('useSwapTokensAndSupply', () => {

expect(async () =>
fn({
swap: mockSwap,
swapQuote: mockSwap,
}),
).rejects.toThrow('somethingWentWrong');
});
Expand Down Expand Up @@ -78,7 +78,7 @@ describe('useSwapTokensAndSupply', () => {
});

const { fn, onConfirmed } = (useSendTransaction as Mock).mock.calls[0][0];
const res = await fn({ swap: mockSwap });
const res = await fn({ swapQuote: mockSwap });

expect(res).toMatchInlineSnapshot(
{
Expand All @@ -87,30 +87,26 @@ describe('useSwapTokensAndSupply', () => {
`
{
"abi": Any<Object>,
"address": "0xfakeSwapRouterContractAddress",
"address": "0xfakeSwapRouterV2ContractAddress",
"args": [
"0x6d6F697e34145Bb95c54E77482d97cc261Dc237E",
1000n,
900n,
[
"0xdef",
"0xghi",
],
1747386407n,
"0xB9e0E753630434d7863528cc73CB7AC638a7c8ff",
"1000",
"900",
undefined,
],
"functionName": "swapExactTokensForTokensAndSupply",
"functionName": "swapAndSupply",
}
`,
);

onConfirmed({ input: { swap: mockSwap } });
onConfirmed({ input: { swapQuote: mockSwap } });

expect(mockCaptureAnalyticEvent).toHaveBeenCalledTimes(1);
expect(mockCaptureAnalyticEvent.mock.calls[0]).toMatchInlineSnapshot(`
[
"Tokens swapped and supplied",
{
"exchangeRate": 1,
"fromTokenAmountTokens": 1e-15,
"fromTokenSymbol": "XVS",
"poolName": "Test Pool",
Expand Down Expand Up @@ -140,7 +136,7 @@ describe('useSwapTokensAndSupply', () => {
);

const { fn } = (useSendTransaction as Mock).mock.calls[0][0];
const res = await fn({ swap: mockSwap });
const res = await fn({ swapQuote: { ...mockSwap, fromToken: bnb } });

expect(res).toMatchInlineSnapshot(
{
Expand All @@ -149,18 +145,15 @@ describe('useSwapTokensAndSupply', () => {
`
{
"abi": Any<Object>,
"address": "0xfakeSwapRouterContractAddress",
"address": "0xfakeSwapRouterV2ContractAddress",
"args": [
"0x6d6F697e34145Bb95c54E77482d97cc261Dc237E",
1000n,
900n,
[
"0xdef",
"0xghi",
],
1747386407n,
"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
"1000",
"900",
undefined,
],
"functionName": "swapExactTokensForTokensAndSupply",
"functionName": "swapAndSupply",
}
`,
);
Expand All @@ -180,7 +173,7 @@ describe('useSwapTokensAndSupply', () => {
);

const invalidMockSwap = {
direction: 'exactAmountOut' as const,
direction: 'exact-out' as const,
fromToken: {
isNative: false,
address: '0xdef' as Address,
Expand All @@ -205,6 +198,6 @@ describe('useSwapTokensAndSupply', () => {

const { fn } = (useSendTransaction as Mock).mock.calls[0][0];

expect(async () => fn({ swap: invalidMockSwap })).rejects.toThrow('incorrectSwapInput');
expect(async () => fn({ swapQuote: invalidMockSwap })).rejects.toThrow('incorrectSwapInput');
});
});
103 changes: 52 additions & 51 deletions apps/evm/src/clients/api/mutations/useSwapTokensAndSupply/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import { DEFAULT_SLIPPAGE_TOLERANCE_PERCENTAGE } from 'constants/swap';
import { useGetContractAddress } from 'hooks/useGetContractAddress';
import { type UseSendTransactionOptions, useSendTransaction } from 'hooks/useSendTransaction';
import { useAnalytics } from 'libs/analytics';
import { swapRouterAbi } from 'libs/contracts';
import { swapRouterV2Abi } from 'libs/contracts';
import { VError } from 'libs/errors';
import { useAccountAddress, useChainId } from 'libs/wallet';
import type { Swap, VToken } from 'types';
import type { ExactInSwapQuote, ExactOutSwapQuote, SwapQuote, VToken } from 'types';
import { convertMantissaToTokens } from 'utilities/convertMantissaToTokens';
import { generateTransactionDeadline } from 'utilities/generateTransactionDeadline';
import type { Address, ContractFunctionArgs } from 'viem';

export interface SwapTokensAndSupplyInput {
swapRouterContractAddress: Address;
swap: Swap;
swapQuote: SwapQuote;
vToken: VToken;
}

Expand All @@ -37,63 +36,62 @@ export const useSwapTokensAndSupply = (
const { captureAnalyticEvent } = useAnalytics();

const { address: swapRouterContractAddress } = useGetContractAddress({
name: 'SwapRouter',
poolComptrollerContractAddress: poolComptrollerAddress,
name: 'SwapRouterV2',
});

return useSendTransaction<
TrimmedSwapTokensAndSupplyInput,
typeof swapRouterAbi,
'swapExactTokensForTokensAndSupply' | 'swapExactBNBForTokensAndSupply',
typeof swapRouterV2Abi,
'swapAndSupply' | 'swapNativeAndSupply',
ContractFunctionArgs<
typeof swapRouterAbi,
typeof swapRouterV2Abi,
'nonpayable' | 'payable',
'swapExactTokensForTokensAndSupply' | 'swapExactBNBForTokensAndSupply'
'swapAndSupply' | 'swapNativeAndSupply'
>
>({
fn: ({ swap }: TrimmedSwapTokensAndSupplyInput) => {
const transactionDeadline = generateTransactionDeadline();

fn: ({ swapQuote }: TrimmedSwapTokensAndSupplyInput) => {
if (!swapRouterContractAddress) {
throw new VError({
type: 'unexpected',
code: 'somethingWentWrong',
});
}

// Sell fromTokens to supply as many toTokens as possible
if (
swap.direction === 'exactAmountIn' &&
!swap.fromToken.isNative &&
!swap.toToken.isNative
swapQuote.direction === 'exact-in' &&
!swapQuote.fromToken.tokenWrapped &&
!swapQuote.toToken.tokenWrapped
) {
return {
abi: swapRouterAbi,
abi: swapRouterV2Abi,
address: swapRouterContractAddress,
functionName: 'swapExactTokensForTokensAndSupply' as const,
functionName: 'swapAndSupply' as const,
args: [
vToken.address,
BigInt(swap.fromTokenAmountSoldMantissa.toFixed()),
BigInt(swap.minimumToTokenAmountReceivedMantissa.toFixed()),
swap.routePath as Address[],
transactionDeadline,
swapQuote.fromToken.address,
swapQuote.fromTokenAmountSoldMantissa,
swapQuote.minimumToTokenAmountReceivedMantissa,
swapQuote.callData,
],
} as const;
}

// Sell BNBs to supply as many toTokens as possible
if (swap.direction === 'exactAmountIn' && swap.fromToken.isNative && !swap.toToken.isNative) {
if (
swapQuote.direction === 'exact-in' &&
swapQuote.fromToken.tokenWrapped?.isNative &&
!swapQuote.toToken.tokenWrapped?.isNative
) {
return {
abi: swapRouterAbi,
abi: swapRouterV2Abi,
address: swapRouterContractAddress,
functionName: 'swapExactBNBForTokensAndSupply' as const,
functionName: 'swapNativeAndSupply' as const,
args: [
vToken.address,
BigInt(swap.minimumToTokenAmountReceivedMantissa.toFixed()),
swap.routePath as Address[],
transactionDeadline,
swapQuote.minimumToTokenAmountReceivedMantissa,
swapQuote.callData,
],
value: BigInt(swap.fromTokenAmountSoldMantissa.toFixed()),
value: swapQuote.fromTokenAmountSoldMantissa,
} as const;
}

Expand All @@ -105,25 +103,28 @@ export const useSwapTokensAndSupply = (
onConfirmed: async ({ input }) => {
captureAnalyticEvent('Tokens swapped and supplied', {
poolName,
fromTokenSymbol: input.swap.fromToken.symbol,
fromTokenAmountTokens: convertMantissaToTokens({
token: input.swap.fromToken,
value:
input.swap.direction === 'exactAmountIn'
? input.swap.fromTokenAmountSoldMantissa
: input.swap.expectedFromTokenAmountSoldMantissa,
}).toNumber(),
toTokenSymbol: input.swap.toToken.symbol,
toTokenAmountTokens: convertMantissaToTokens({
token: input.swap.toToken,
value:
input.swap.direction === 'exactAmountIn'
? input.swap.expectedToTokenAmountReceivedMantissa
: input.swap.toTokenAmountReceivedMantissa,
}).toNumber(),
priceImpactPercentage: input.swap.priceImpactPercentage,
fromTokenSymbol: input.swapQuote.fromToken.symbol,
fromTokenAmountTokens: (
convertMantissaToTokens({
token: input.swapQuote.fromToken,
value:
input.swapQuote.direction === 'exact-in'
? (input.swapQuote as ExactInSwapQuote).fromTokenAmountSoldMantissa
: (input.swapQuote as ExactOutSwapQuote).expectedFromTokenAmountSoldMantissa,
}) ?? '0'
).toNumber(),
toTokenSymbol: input.swapQuote.toToken.symbol,
toTokenAmountTokens: (
convertMantissaToTokens({
token: input.swapQuote.toToken,
value:
input.swapQuote.direction === 'exact-in'
? (input.swapQuote as ExactInSwapQuote).expectedToTokenAmountReceivedMantissa
: (input.swapQuote as ExactOutSwapQuote).toTokenAmountReceivedMantissa,
}) ?? '0'
).toNumber(),
priceImpactPercentage: input.swapQuote.priceImpactPercentage,
slippageTolerancePercentage: DEFAULT_SLIPPAGE_TOLERANCE_PERCENTAGE,
exchangeRate: input.swap.exchangeRate.toNumber(),
});

queryClient.invalidateQueries({
Expand All @@ -132,7 +133,7 @@ export const useSwapTokensAndSupply = (
{
chainId,
accountAddress,
tokenAddress: input.swap.fromToken.address,
tokenAddress: input.swapQuote.fromToken.address,
},
],
});
Expand All @@ -142,7 +143,7 @@ export const useSwapTokensAndSupply = (
FunctionKey.GET_TOKEN_ALLOWANCE,
{
chainId,
tokenAddress: input.swap.fromToken.address,
tokenAddress: input.swapQuote.fromToken.address,
accountAddress,
spenderAddress: poolComptrollerAddress,
},
Expand All @@ -155,7 +156,7 @@ export const useSwapTokensAndSupply = (
{
chainId,
accountAddress,
tokenAddress: input.swap.toToken.address,
tokenAddress: input.swapQuote.toToken.address,
},
],
});
Expand Down
34 changes: 24 additions & 10 deletions apps/evm/src/clients/api/queries/getSwapQuote/useGetSwapQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { type QueryObserverOptions, useQuery } from '@tanstack/react-query';

import FunctionKey from 'constants/functionKey';
import { useGetContractAddress } from 'hooks/useGetContractAddress';
import type { VError } from 'libs/errors';
import { useGetToken } from 'libs/tokens';
import { useChainId } from 'libs/wallet';
import type { SwapQuoteError } from 'types';
import { callOrThrow, generatePseudoRandomRefetchInterval } from 'utilities';
import {
type GetApproximateOutSwapQuoteInput,
Expand All @@ -12,6 +13,7 @@ import {
type GetSwapQuoteOutput,
getSwapQuote,
} from '.';
import wrapToken from './wrapToken';

export type TrimmedGetSwapQuoteInput =
| Omit<GetExactInSwapQuoteInput, 'chainId'>
Expand All @@ -20,7 +22,7 @@ export type TrimmedGetSwapQuoteInput =

type Options = QueryObserverOptions<
GetSwapQuoteOutput,
VError<'swapQuote' | 'interaction'>,
SwapQuoteError,
GetSwapQuoteOutput,
GetSwapQuoteOutput,
[FunctionKey.GET_SWAP_QUOTE, TrimmedGetSwapQuoteInput]
Expand All @@ -30,20 +32,32 @@ const refetchInterval = generatePseudoRandomRefetchInterval();

export const useGetSwapQuote = (input: TrimmedGetSwapQuoteInput, options?: Partial<Options>) => {
const { chainId } = useChainId();
const wbnb = useGetToken({
symbol: 'WBNB',
});

const wrappedFromToken = wbnb && wrapToken({ token: input.fromToken, wrappedToken: wbnb });
const wrappedToToken = wbnb && wrapToken({ token: input.toToken, wrappedToken: wbnb });

const { address: leverageManagerContractAddress } = useGetContractAddress({
name: 'LeverageManager',
const { address: swapRouterV2ContractAddress } = useGetContractAddress({
name: 'SwapRouterV2',
});

return useQuery({
queryKey: [FunctionKey.GET_SWAP_QUOTE, input],
queryFn: () =>
callOrThrow({ leverageManagerContractAddress }, params =>
getSwapQuote({
...input,
chainId,
recipientAddress: params.leverageManagerContractAddress,
}),
callOrThrow(
{
recipientAddress: swapRouterV2ContractAddress,
fromToken: wrappedFromToken,
toToken: wrappedToToken,
},
params =>
getSwapQuote({
...input,
...params,
chainId,
Comment thread
therealemjy marked this conversation as resolved.
}),
),
retry: false,
refetchInterval,
Expand Down
7 changes: 7 additions & 0 deletions apps/evm/src/clients/api/queries/getSwapQuote/wrapToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Token } from 'types';

// PancakeSwap only trades with wrapped tokens, so BNB is replaced with wBNB
const wrapToken = ({ token, wrappedToken }: { token: Token; wrappedToken: Token }) =>
token.isNative ? wrappedToken : token;

export default wrapToken;
Loading