From e298a37e04243370235a47e3102f2a40890b5ef1 Mon Sep 17 00:00:00 2001 From: Apotheosis <0xapotheosis@gmail.com> Date: Tue, 3 Sep 2024 19:45:25 +1000 Subject: [PATCH 1/4] feat: portal slippage optimization approval swaps (#7672) --- .../getPortalsTradeQuote.ts | 24 +++++++-- .../utils/fetchPortalsTradeOrder.ts | 53 +++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts index 240b14fd6fb..1fb4efd77cf 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts @@ -21,7 +21,7 @@ import { import { getRate, makeSwapErrorRight } from '../../../utils' import { getTreasuryAddressFromChainId, isNativeEvmAsset } from '../../utils/helpers/helpers' import { chainIdToPortalsNetwork } from '../constants' -import { fetchPortalsTradeOrder } from '../utils/fetchPortalsTradeOrder' +import { fetchPortalsTradeEstimate, fetchPortalsTradeOrder } from '../utils/fetchPortalsTradeOrder' import { getDummyQuoteParams, isSupportedChainId } from '../utils/helpers' export async function getPortalsTradeQuote( @@ -120,10 +120,24 @@ export async function getPortalsTradeQuote( validate: true, swapperConfig, }).catch(async e => { - // If validation fails, fire two more quotes: - // 1. a quote with validation enabled, but using a well-funded address to get a rough gasLimit estimate - // 2. another quote with validation disabled, to get an actual quote + // If validation fails, fire 3 more quotes: + // 1. a quote estimate (does not require approval) to get the optimal slippage tolerance + // 2. a quote with validation enabled, but using a well-funded address to get a rough gasLimit estimate + // 3. another quote with validation disabled, to get an actual quote (using the user slippage, or the optimal from the estimate) console.info('failed to get Portals quote with validation enabled', e) + + // Use the quote estimate endpoint to get the optimal slippage tolerance + const quoteEstimateResponse = await fetchPortalsTradeEstimate({ + sender: sendAddress, + inputToken, + outputToken, + inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, + swapperConfig, + }).catch(e => { + console.info('failed to get Portals quote estimate', e) + return undefined + }) + const dummyQuoteParams = getDummyQuoteParams(sellAsset.chainId) const dummySellAssetAddress = fromAssetId(dummyQuoteParams.sellAssetId).assetReference @@ -132,6 +146,7 @@ export async function getPortalsTradeQuote( const dummyInputToken = `${portalsNetwork}:${dummySellAssetAddress}` const dummyOutputToken = `${portalsNetwork}:${dummyBuyAssetAddress}` + // Use a dummy request to the portal endpoint to get a rough gasLimit estimate const dummyOrderResponse = await fetchPortalsTradeOrder({ sender: dummyQuoteParams.accountAddress, inputToken: dummyInputToken, @@ -158,6 +173,7 @@ export async function getPortalsTradeQuote( inputAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, slippageTolerancePercentage: userSlippageTolerancePercentageDecimalOrDefault ?? + quoteEstimateResponse?.context.slippageTolerancePercentage ?? bnOrZero(getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Portals)) .times(100) .toNumber(), diff --git a/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts b/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts index e879193e0b7..63430aef232 100644 --- a/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts +++ b/packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts @@ -17,6 +17,8 @@ type PortalsTradeOrderParams = { swapperConfig: SwapperConfig } +type PortalsTradeOrderEstimateParams = Omit + type PortalsTradeOrderResponse = { context: { orderId: string @@ -50,6 +52,25 @@ type PortalsTradeOrderResponse = { gasLimit: string } } + +type PortalsTradeOrderEstimateResponse = { + outputAmount: string + minOutputAmount: string + outputToken: string + outputTokenDecimals: number + context: { + slippageTolerancePercentage: number + inputAmount: string + inputAmountUsd: number + inputToken: string + outputToken: string + outputAmount: string + outputAmountUsd: number + minOutputAmountUsd: number + sender?: string + } +} + export const fetchPortalsTradeOrder = async ({ sender, inputToken, @@ -90,3 +111,35 @@ export const fetchPortalsTradeOrder = async ({ throw error } } + +export const fetchPortalsTradeEstimate = async ({ + sender, + inputToken, + inputAmount, + outputToken, + slippageTolerancePercentage, + swapperConfig, +}: PortalsTradeOrderEstimateParams): Promise => { + const url = `${swapperConfig.REACT_APP_PORTALS_BASE_URL}/v2/portal/estimate` + + const params = new URLSearchParams({ + sender, + inputToken, + inputAmount, + outputToken, + }) + + if (slippageTolerancePercentage !== undefined) { + params.append('slippageTolerancePercentage', slippageTolerancePercentage.toFixed(2)) // Portals API expects a string with at most 2 decimal places + } + + try { + const response = await axios.get(url, { params }) + return response.data + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(`Failed to fetch Portals trade estimate: ${error.message}`) + } + throw error + } +} From 7354e35ea7556812371645b3734ca1bcdd68a006 Mon Sep 17 00:00:00 2001 From: 0xean <0xean.eth@gmail.com> Date: Tue, 3 Sep 2024 14:55:53 -0400 Subject: [PATCH 2/4] chore: update hash for epoch 1 pending (#7680) --- src/pages/RFOX/constants.ts | 2 +- src/pages/RFOX/hooks/useLifetimeRewardDistributionsQuery.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/RFOX/constants.ts b/src/pages/RFOX/constants.ts index 72e35ffcfaf..33cafac3333 100644 --- a/src/pages/RFOX/constants.ts +++ b/src/pages/RFOX/constants.ts @@ -11,7 +11,7 @@ export const withdrawEvent = getAbiItem({ abi: foxStakingV1Abi, name: 'Withdraw' export const IPFS_GATEWAY = 'https://gateway.shapeshift.com/ipfs' -export const CURRENT_EPOCH_IPFS_HASH = 'QmTr3pFd14d5RaYao7LrtFkrR5ABoa4KCQqffx9G73SA1T' +export const CURRENT_EPOCH_IPFS_HASH = 'QmPYuHffJfCQuexWJYv4CpukhRAyw3YM8MJbCZ34ZGda3n' const client = viemClientByNetworkId[arbitrum.id] diff --git a/src/pages/RFOX/hooks/useLifetimeRewardDistributionsQuery.ts b/src/pages/RFOX/hooks/useLifetimeRewardDistributionsQuery.ts index 7bc1fe52b93..085150f8b8e 100644 --- a/src/pages/RFOX/hooks/useLifetimeRewardDistributionsQuery.ts +++ b/src/pages/RFOX/hooks/useLifetimeRewardDistributionsQuery.ts @@ -28,6 +28,7 @@ export const useLifetimeRewardDistributionsQuery = ({ if (!stakingAssetAccountAddresses) return [] return data .filter(epoch => epoch.number >= 0) + .sort((a, b) => b.number - a.number) .flatMap(epoch => stakingAssetAccountAddresses.map(stakingAssetAccountAddress => { const stakingAddress = getAddress(stakingAssetAccountAddress) From a98e6a91e82442c2212562f884ee738fd817b47d Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Tue, 3 Sep 2024 21:29:26 +0200 Subject: [PATCH 3/4] fix: make most of the addresses to be kopiable (#7663) --- .../AccountDropdown/AccountSegement.tsx | 1 + src/components/InlineCopyButton.tsx | 12 ++++-- .../Modals/Nfts/components/NftOverview.tsx | 43 ++++++++++--------- src/components/Modals/Send/views/Confirm.tsx | 28 ++++++++---- .../components/StepperStep.tsx | 6 ++- .../TransactionDetails/Address.tsx | 41 ++++++++++-------- .../Claim/components/Confirm.tsx | 13 ++++-- .../CosmosManager/Claim/components/Status.tsx | 13 ++++-- .../Withdraw/components/Status.tsx | 5 ++- .../FoxFarmingManager/Claim/ClaimConfirm.tsx | 17 +++++--- .../FoxFarmingManager/Claim/ClaimStatus.tsx | 17 +++++--- .../Overview/Claim/ClaimConfirm.tsx | 17 +++++--- .../Overview/Claim/ClaimStatus.tsx | 17 +++++--- .../components/Account.tsx | 17 +++++--- .../components/header/AddressLinks.tsx | 9 ++-- 15 files changed, 162 insertions(+), 94 deletions(-) diff --git a/src/components/AccountDropdown/AccountSegement.tsx b/src/components/AccountDropdown/AccountSegement.tsx index d65deb35d36..1298840cbb3 100644 --- a/src/components/AccountDropdown/AccountSegement.tsx +++ b/src/components/AccountDropdown/AccountSegement.tsx @@ -14,6 +14,7 @@ export const AccountSegment: FC = ({ title, subtitle }) => ( py={2} color='text.subtle' fontSize='sm' + alignItems='center' justifyContent='space-between' > {title} diff --git a/src/components/InlineCopyButton.tsx b/src/components/InlineCopyButton.tsx index d3938e1cafc..784eb5d1ce8 100644 --- a/src/components/InlineCopyButton.tsx +++ b/src/components/InlineCopyButton.tsx @@ -1,6 +1,6 @@ import { CheckIcon, CopyIcon } from '@chakra-ui/icons' import { Flex, IconButton } from '@chakra-ui/react' -import type { PropsWithChildren } from 'react' +import type { MouseEvent, PropsWithChildren } from 'react' import React, { useCallback } from 'react' import { useTranslate } from 'react-polyglot' import { useCopyToClipboard } from 'hooks/useCopyToClipboard' @@ -23,9 +23,13 @@ export const InlineCopyButton: React.FC = ({ const translate = useTranslate() const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) - const handleCopyClick = useCallback(() => { - copyToClipboard(value) - }, [copyToClipboard, value]) + const handleCopyClick = useCallback( + (e: MouseEvent) => { + e.stopPropagation() + copyToClipboard(value) + }, + [copyToClipboard, value], + ) // Hide the copy button if it is disabled if (isDisabled) return <>{children} diff --git a/src/components/Modals/Nfts/components/NftOverview.tsx b/src/components/Modals/Nfts/components/NftOverview.tsx index 7a246fb561b..d18f7c96834 100644 --- a/src/components/Modals/Nfts/components/NftOverview.tsx +++ b/src/components/Modals/Nfts/components/NftOverview.tsx @@ -5,6 +5,7 @@ import { CopyButton } from 'plugins/walletConnectToDapps/components/modals/CopyB import { useCallback } from 'react' import { useTranslate } from 'react-polyglot' import { AssetIcon } from 'components/AssetIcon' +import { InlineCopyButton } from 'components/InlineCopyButton' import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' import { Row } from 'components/Row/Row' import { SanitizedHtml } from 'components/SanitizedHtml/SanitizedHtml' @@ -66,26 +67,28 @@ export const NftOverview: React.FC = ({ nftItem }) => { {translate('nft.address')} - + + + )} diff --git a/src/components/Modals/Send/views/Confirm.tsx b/src/components/Modals/Send/views/Confirm.tsx index c35a525636f..f149c044b47 100644 --- a/src/components/Modals/Send/views/Confirm.tsx +++ b/src/components/Modals/Send/views/Confirm.tsx @@ -8,6 +8,7 @@ import { Stack, useColorModeValue, } from '@chakra-ui/react' +import { fromAccountId } from '@shapeshiftoss/caip' import { fromAssetId } from '@shapeshiftoss/caip/dist/assetId/assetId' import { CHAIN_NAMESPACE } from '@shapeshiftoss/caip/dist/constants' import type { FeeDataKey } from '@shapeshiftoss/chain-adapters' @@ -19,6 +20,7 @@ import { useTranslate } from 'react-polyglot' import { useHistory } from 'react-router-dom' import { AccountDropdown } from 'components/AccountDropdown/AccountDropdown' import { Amount } from 'components/Amount/Amount' +import { InlineCopyButton } from 'components/InlineCopyButton' import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' import { DialogBackButton } from 'components/Modal/components/DialogBackButton' import { DialogBody } from 'components/Modal/components/DialogBody' @@ -30,6 +32,7 @@ import { SlideTransition } from 'components/SlideTransition' import { RawText, Text } from 'components/Text' import type { TextPropTypes } from 'components/Text/Text' import { bnOrZero } from 'lib/bignumber/bignumber' +import { isUtxoAccountId } from 'lib/utils/utxo' import { selectAssetById, selectFeeAssetById } from 'state/slices/selectors' import { useAppSelector } from 'state/store' @@ -142,20 +145,29 @@ export const Confirm = () => { - + + + - {vanityAddress ? vanityAddress : } + + + {vanityAddress ? vanityAddress : } + + {allowCustomSendNonce && ( diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StepperStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StepperStep.tsx index 59b5c723705..c1569cb9146 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StepperStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/StepperStep.tsx @@ -12,6 +12,7 @@ import { Tag, useStyleConfig, } from '@chakra-ui/react' +import { InlineCopyButton } from 'components/InlineCopyButton' import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' import { selectActiveQuote } from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector } from 'state/store' @@ -39,10 +40,13 @@ const LastStepTag = () => { return ( - + + + ) } + export const StepperStep = ({ title, stepIndicator, diff --git a/src/components/TransactionHistoryRows/TransactionDetails/Address.tsx b/src/components/TransactionHistoryRows/TransactionDetails/Address.tsx index 75990c91e12..76501fc247b 100644 --- a/src/components/TransactionHistoryRows/TransactionDetails/Address.tsx +++ b/src/components/TransactionHistoryRows/TransactionDetails/Address.tsx @@ -1,5 +1,6 @@ import { Button, Link } from '@chakra-ui/react' import { useCallback } from 'react' +import { InlineCopyButton } from 'components/InlineCopyButton' import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' const buttonHover = { bg: 'transparent' } @@ -18,24 +19,28 @@ export const Address = ({ [], ) return explorerAddressLink ? ( - + + + ) : ( - + + + ) } diff --git a/src/features/defi/providers/cosmos/components/CosmosManager/Claim/components/Confirm.tsx b/src/features/defi/providers/cosmos/components/CosmosManager/Claim/components/Confirm.tsx index 662c17d0653..9c71a472b13 100644 --- a/src/features/defi/providers/cosmos/components/CosmosManager/Claim/components/Confirm.tsx +++ b/src/features/defi/providers/cosmos/components/CosmosManager/Claim/components/Confirm.tsx @@ -14,6 +14,7 @@ import { useTranslate } from 'react-polyglot' import { Amount } from 'components/Amount/Amount' import { AssetIcon } from 'components/AssetIcon' import type { StepComponentProps } from 'components/DeFi/components/Steps' +import { InlineCopyButton } from 'components/InlineCopyButton' import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' import { Row } from 'components/Row/Row' import { Text } from 'components/Text' @@ -193,9 +194,15 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { - - {userAddress && } - + + + {userAddress && } + + diff --git a/src/features/defi/providers/cosmos/components/CosmosManager/Claim/components/Status.tsx b/src/features/defi/providers/cosmos/components/CosmosManager/Claim/components/Status.tsx index a33c0c42b87..1905192a0ab 100644 --- a/src/features/defi/providers/cosmos/components/CosmosManager/Claim/components/Status.tsx +++ b/src/features/defi/providers/cosmos/components/CosmosManager/Claim/components/Status.tsx @@ -12,6 +12,7 @@ import { Amount } from 'components/Amount/Amount' import { AssetIcon } from 'components/AssetIcon' import { CircularProgress } from 'components/CircularProgress/CircularProgress' import { IconCircle } from 'components/IconCircle' +import { InlineCopyButton } from 'components/InlineCopyButton' import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' import { Row } from 'components/Row/Row' import { RawText } from 'components/Text' @@ -151,9 +152,15 @@ export const Status: React.FC = ({ accountId }) => { {translate('defi.modals.claim.claimToAddress')} - - {userAddress && } - + + + {userAddress && } + +