Commit 27e20d72 authored by Jack Short's avatar Jack Short Committed by GitHub

feat: pay with any token routing (#5959)

* feat: implementing graphql endpoint

* changing from hook to function call

* initial gql routing works

* feat: initial pwatRouting setup

* sending correct amount

* removing console

* it is working

* sufficient balance

* 0 if no inputCurrency

* removing value to send if erc20

* removing console

* permit2 optional flag

* removing not necessary stuff

* mobile fixes

* overlay needs to be here

* changing swap amount to pool reserves

* refactoring routing logic

* no route found button state

* better price loading for insufficient liquidity

* refactoring graphql routing code

* overflow

* initial comments

* resetting bag status on input currency change

* locking

* done

* remove helper text for eth
parent 95eafbab
......@@ -41,6 +41,7 @@ export enum ActivityType {
Send = 'SEND',
Stake = 'STAKE',
Swap = 'SWAP',
Swapx = 'SWAPX',
Staking = 'Staking',
Unknown = 'UNKNOWN',
Unstake = 'UNSTAKE',
......@@ -587,6 +588,7 @@ export type Portfolio = {
export type PortfolioAssetActivitiesArgs = {
includeOffChain?: InputMaybe<Scalars['Boolean']>;
page?: InputMaybe<Scalars['Int']>;
pageSize?: InputMaybe<Scalars['Int']>;
};
......@@ -1048,7 +1050,7 @@ export type NftRouteQueryVariables = Exact<{
}>;
export type NftRouteQuery = { __typename?: 'Query', nftRoute?: { __typename?: 'NftRouteResponse', calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', currency: Currency, value: string } } };
export type NftRouteQuery = { __typename?: 'Query', nftRoute?: { __typename?: 'NftRouteResponse', id: string, calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string } } };
export const RecentlySearchedAssetsDocument = gql`
......@@ -1989,6 +1991,7 @@ export const NftRouteDocument = gql`
nftTrades: $nftTrades
tokenTrades: $tokenTrades
) {
id
calldata
route {
amount
......@@ -1996,10 +1999,12 @@ export const NftRouteDocument = gql`
id
marketplace
price {
id
currency
value
}
quotePrice {
id
currency
value
}
......@@ -2007,6 +2012,7 @@ export const NftRouteDocument = gql`
tokenType
}
sendAmount {
id
currency
value
}
......
import gql from 'graphql-tag'
import { NftTradeInput, TokenTradeInput, useNftRouteQuery } from '../__generated__/types-and-hooks'
gql`
query NftRoute(
$chain: Chain = ETHEREUM
......@@ -10,6 +8,7 @@ gql`
$tokenTrades: [TokenTradeInput!]
) {
nftRoute(chain: $chain, senderAddress: $senderAddress, nftTrades: $nftTrades, tokenTrades: $tokenTrades) {
id
calldata
route {
amount
......@@ -17,10 +16,12 @@ gql`
id
marketplace
price {
id
currency
value
}
quotePrice {
id
currency
value
}
......@@ -28,6 +29,7 @@ gql`
tokenType
}
sendAmount {
id
currency
value
}
......@@ -35,13 +37,3 @@ gql`
}
}
`
export function useNftRoute(senderAddress: string, nftTrades: NftTradeInput[], tokenTrades?: TokenTradeInput[]) {
return useNftRouteQuery({
variables: {
senderAddress,
nftTrades,
tokenTrades,
},
})
}
......@@ -27,7 +27,7 @@ interface AllowanceRequired {
approveAndPermit: () => Promise<void>
}
type Allowance =
export type Allowance =
| { state: AllowanceState.LOADING }
| {
state: AllowanceState.ALLOWED
......
......@@ -4,7 +4,7 @@ import { NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { useNftRoute } from 'graphql/data/nft/Routing'
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
import { BagFooter } from 'nft/components/bag/BagFooter'
import ListingModal from 'nft/components/bag/profile/ListingModal'
......@@ -21,14 +21,17 @@ import {
useSendTransaction,
useTransactionResponse,
} from 'nft/hooks'
import { useTokenInput } from 'nft/hooks/useTokenInput'
import { fetchRoute } from 'nft/queries'
import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types'
import {
buildNftTradeInputFromBagItems,
buildSellObject,
formatAssetEventProperties,
recalculateBagUsingPooledAssets,
sortUpdatedAssets,
} from 'nft/utils'
import { buildRouteResponse } from 'nft/utils/nftRoute'
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQueryClient } from 'react-query'
......@@ -50,7 +53,7 @@ interface SeparatorProps {
show?: boolean
}
const BagContainer = styled.div<{ raiseZIndex: boolean }>`
const BagContainer = styled.div<{ raiseZIndex: boolean; isProfilePage: boolean }>`
position: fixed;
display: flex;
flex-direction: column;
......@@ -62,7 +65,8 @@ const BagContainer = styled.div<{ raiseZIndex: boolean }>`
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 16px;
box-shadow: ${({ theme }) => theme.shallowShadow};
z-index: ${({ raiseZIndex }) => (raiseZIndex ? Z_INDEX.modalOverTooltip : 3)};
z-index: ${({ raiseZIndex, isProfilePage }) =>
raiseZIndex ? (isProfilePage ? Z_INDEX.modalOverTooltip : Z_INDEX.modalBackdrop) : 3};
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
right: 0px;
......@@ -143,6 +147,7 @@ const Bag = () => {
const setTransactionState = useSendTransaction((state) => state.setState)
const transactionStateRef = useRef(transactionState)
const [setTransactionResponse] = useTransactionResponse((state) => [state.setTransactionResponse])
const tokenTradeInput = useTokenInput((state) => state.tokenTradeInput)
const queryClient = useQueryClient()
......@@ -197,7 +202,8 @@ const Bag = () => {
setBagExpanded({ bagExpanded: false, manualClose: true })
}, [setBagExpanded])
useNftRoute(usingGqlRouting ? account ?? '' : '', [])
const [fetchGqlRoute] = useNftRouteLazyQuery()
const fetchAssets = async () => {
const itemsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
const ethSellObject = buildSellObject(
......@@ -210,48 +216,119 @@ const Bag = () => {
!bagIsLocked && setLocked(true)
setBagStatus(BagStatus.FETCHING_ROUTE)
try {
const data = await queryClient.fetchQuery(['assetsRoute', ethSellObject, itemsToBuy, account], () =>
fetchRoute({
toSell: [ethSellObject],
toBuy: itemsToBuy,
senderAddress: account ?? '',
if (usingGqlRouting) {
fetchGqlRoute({
variables: {
senderAddress: usingGqlRouting && account ? account : '',
nftTrades: usingGqlRouting ? buildNftTradeInputFromBagItems(itemsInBag) : [],
tokenTrades: tokenTradeInput ? tokenTradeInput : undefined,
},
onCompleted: (data) => {
if (!data.nftRoute || !data.nftRoute.route) {
setBagStatus(BagStatus.ADDING_TO_BAG)
setLocked(false)
return
}
const { route, routeResponse } = buildRouteResponse(data.nftRoute, !!tokenTradeInput)
const updatedAssets = combineBuyItemsWithTxRoute(itemsToBuy, route)
const fetchedPriceChangedAssets = updatedAssets
.filter((asset) => asset.updatedPriceInfo)
.sort(sortUpdatedAssets)
const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
const fetchedUnchangedAssets = updatedAssets.filter(
(asset) => !asset.updatedPriceInfo && !asset.isUnavailable
)
const hasReviewedAssets = fetchedUnchangedAssets.length > 0
const hasAssetsInReview = fetchedPriceChangedAssets.length > 0
const hasUnavailableAssets = fetchedUnavailableAssets.length > 0
const hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
const shouldReview = hasAssetsInReview || hasUnavailableAssets
setItemsInBag([
...fetchedUnavailableAssets.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...fetchedPriceChangedAssets.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...fetchedUnchangedAssets.map((unchangedAsset) => ({
asset: unchangedAsset,
status: BagItemStatus.REVIEWED,
})),
])
let shouldLock = false
if (hasAssets) {
if (!shouldReview) {
purchaseAssets(routeResponse)
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
shouldLock = true
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else {
setBagStatus(BagStatus.IN_REVIEW)
}
} else {
setBagStatus(BagStatus.ADDING_TO_BAG)
}
setLocked(shouldLock)
},
})
)
const updatedAssets = combineBuyItemsWithTxRoute(itemsToBuy, data.route)
const fetchedPriceChangedAssets = updatedAssets.filter((asset) => asset.updatedPriceInfo).sort(sortUpdatedAssets)
const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
const fetchedUnchangedAssets = updatedAssets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
const hasReviewedAssets = fetchedUnchangedAssets.length > 0
const hasAssetsInReview = fetchedPriceChangedAssets.length > 0
const hasUnavailableAssets = fetchedUnavailableAssets.length > 0
const hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
const shouldReview = hasAssetsInReview || hasUnavailableAssets
setItemsInBag([
...fetchedUnavailableAssets.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...fetchedPriceChangedAssets.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...fetchedUnchangedAssets.map((unchangedAsset) => ({ asset: unchangedAsset, status: BagItemStatus.REVIEWED })),
])
setLocked(false)
if (hasAssets) {
if (!shouldReview) {
purchaseAssets(data)
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else {
setBagStatus(BagStatus.IN_REVIEW)
}
} else {
setBagStatus(BagStatus.ADDING_TO_BAG)
const routeData = await queryClient.fetchQuery(['assetsRoute', ethSellObject, itemsToBuy, account], () =>
fetchRoute({
toSell: [ethSellObject],
toBuy: itemsToBuy,
senderAddress: account ?? '',
})
)
const updatedAssets = combineBuyItemsWithTxRoute(itemsToBuy, routeData.route)
const fetchedPriceChangedAssets = updatedAssets
.filter((asset) => asset.updatedPriceInfo)
.sort(sortUpdatedAssets)
const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
const fetchedUnchangedAssets = updatedAssets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
const hasReviewedAssets = fetchedUnchangedAssets.length > 0
const hasAssetsInReview = fetchedPriceChangedAssets.length > 0
const hasUnavailableAssets = fetchedUnavailableAssets.length > 0
const hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
const shouldReview = hasAssetsInReview || hasUnavailableAssets
setItemsInBag([
...fetchedUnavailableAssets.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...fetchedPriceChangedAssets.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...fetchedUnchangedAssets.map((unchangedAsset) => ({
asset: unchangedAsset,
status: BagItemStatus.REVIEWED,
})),
])
setLocked(false)
if (hasAssets) {
if (!shouldReview) {
purchaseAssets(routeData)
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else {
setBagStatus(BagStatus.IN_REVIEW)
}
} else {
setBagStatus(BagStatus.ADDING_TO_BAG)
}
}
} catch (error) {
setBagStatus(BagStatus.ADDING_TO_BAG)
......@@ -313,7 +390,7 @@ const Bag = () => {
return (
<Portal>
<BagContainer data-testid="nft-bag" raiseZIndex={isMobile || isModalOpen}>
<BagContainer data-testid="nft-bag" raiseZIndex={isMobile || isModalOpen} isProfilePage={isProfilePage}>
{!(isProfilePage && profilePageState === ProfilePageStateType.LISTING) ? (
<>
<BagHeader
......
......@@ -19,15 +19,17 @@ import { usePayWithAnyTokenEnabled } from 'featureFlags/flags/payWithAnyToken'
import { useCurrency } from 'hooks/Tokens'
import { AllowanceState } from 'hooks/usePermit2Allowance'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useBag } from 'nft/hooks/useBag'
import useDerivedPayWithAnyTokenSwapInfo from 'nft/hooks/useDerivedPayWithAnyTokenSwapInfo'
import usePayWithAnyTokenSwap from 'nft/hooks/usePayWithAnyTokenSwap'
import usePermit2Approval from 'nft/hooks/usePermit2Approval'
import { useTokenInput } from 'nft/hooks/useTokenInput'
import { useWalletBalance } from 'nft/hooks/useWalletBalance'
import { BagStatus } from 'nft/types'
import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils'
import { PropsWithChildren, useMemo, useState } from 'react'
import { PropsWithChildren, useEffect, useMemo, useState } from 'react'
import { AlertTriangle, ChevronDown } from 'react-feather'
import { useToggleWalletModal } from 'state/application/hooks'
import { InterfaceTrade, TradeState } from 'state/routing/types'
......@@ -36,6 +38,7 @@ import { ThemedText } from 'theme'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { warningSeverity } from 'utils/prices'
import { switchChain } from 'utils/switchChain'
import shallow from 'zustand/shallow'
const LOW_SEVERITY_THRESHOLD = 1
const MEDIUM_SEVERITY_THRESHOLD = 3
......@@ -63,10 +66,12 @@ const FooterHeader = styled(Column)<{ usingPayWithAnyToken?: boolean }>`
const CurrencyRow = styled(Row)`
justify-content: space-between;
align-items: start;
gap: 8px;
`
const TotalColumn = styled(Column)`
text-align: end;
overflow-x: hidden;
`
const WarningIcon = styled(AlertTriangle)`
......@@ -96,10 +101,10 @@ const CurrencyInput = styled(Row)`
cursor: pointer;
`
const PayButton = styled.button<{ $backgroundColor: string }>`
const PayButton = styled.button<{ $backgroundColor: string; $color: string }>`
display: flex;
background: ${({ $backgroundColor }) => $backgroundColor};
color: ${({ theme }) => theme.accentTextLightPrimary};
color: ${({ $color }) => $color};
font-weight: 600;
line-height: 24px;
font-size: 16px;
......@@ -134,15 +139,35 @@ const PriceImpactRow = styled(Row)`
gap: 8px;
`
const ValueText = styled(ThemedText.BodyPrimary)`
line-height: 20px;
font-weight: 500;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
scrollbar-width: none;
::-webkit-scrollbar {
display: none;
}
`
interface ActionButtonProps {
disabled?: boolean
onClick: () => void
backgroundColor: string
textColor: string
}
const ActionButton = ({ disabled, children, onClick, backgroundColor }: PropsWithChildren<ActionButtonProps>) => {
const ActionButton = ({
disabled,
children,
onClick,
backgroundColor,
textColor,
}: PropsWithChildren<ActionButtonProps>) => {
return (
<PayButton disabled={disabled} onClick={onClick} $backgroundColor={backgroundColor}>
<PayButton disabled={disabled} onClick={onClick} $backgroundColor={backgroundColor} $color={textColor}>
{children}
</PayButton>
)
......@@ -175,7 +200,6 @@ const Helper = ({ children, color }: PropsWithChildren<HelperTextProps>) => {
)
}
// TODO: ask design about no route found
const InputCurrencyValue = ({
usingPayWithAnyToken,
totalEthPrice,
......@@ -198,22 +222,18 @@ const InputCurrencyValue = ({
)
}
if (tradeState === TradeState.VALID || tradeState === TradeState.SYNCING) {
if (tradeState === TradeState.LOADING) {
return (
<ThemedText.BodyPrimary
lineHeight="20px"
fontWeight="500"
color={tradeState === TradeState.VALID ? 'textPrimary' : 'textTertiary'}
>
{ethNumberStandardFormatter(trade?.inputAmount.toExact())}
<ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500">
<Trans>Fetching price...</Trans>
</ThemedText.BodyPrimary>
)
}
return (
<ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500">
<Trans>Fetching price...</Trans>
</ThemedText.BodyPrimary>
<ValueText color={tradeState === TradeState.SYNCING ? 'textTertiary' : 'textPrimary'}>
{ethNumberStandardFormatter(trade?.inputAmount.toExact())}
</ValueText>
)
}
......@@ -221,12 +241,20 @@ const FiatValue = ({
usdcValue,
priceImpact,
priceImpactColor,
tradeState,
usingPayWithAnyToken,
}: {
usdcValue: CurrencyAmount<Token> | null
priceImpact: Percent | undefined
priceImpactColor: string | undefined
tradeState: TradeState
usingPayWithAnyToken: boolean
}) => {
if (!usdcValue) {
if (usingPayWithAnyToken && (tradeState === TradeState.INVALID || tradeState === TradeState.NO_ROUTE_FOUND)) {
return null
}
return <FiatLoadingBubble />
}
......@@ -274,31 +302,45 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
const inputCurrency = useTokenInput((state) => state.inputCurrency)
const setInputCurrency = useTokenInput((state) => state.setInputCurrency)
const defaultCurrency = useCurrency('ETH')
const inputCurrencyBalance = useTokenBalance(
account ?? undefined,
!!inputCurrency && inputCurrency.isToken ? inputCurrency : undefined
)
const setBagExpanded = useBag((state) => state.setBagExpanded)
const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false)
const {
isLocked: bagIsLocked,
setBagExpanded,
setBagStatus,
} = useBag(
({ isLocked, setBagExpanded, setBagStatus }) => ({
isLocked,
setBagExpanded,
setBagStatus,
}),
shallow
)
const { balance: balanceInEth } = useWalletBalance()
const sufficientBalance = useMemo(() => {
if (!connected || chainId !== SupportedChainId.MAINNET) {
return undefined
}
return parseEther(balanceInEth).gte(totalEthPrice)
}, [connected, chainId, balanceInEth, totalEthPrice])
const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false)
const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
const activeCurrency = inputCurrency ?? defaultCurrency
const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken
const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken && chainId === SupportedChainId.MAINNET
const parsedOutputAmount = useMemo(() => {
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
}, [defaultCurrency, totalEthPrice])
const { state: tradeState, trade, maximumAmountIn } = usePayWithAnyTokenSwap(inputCurrency, parsedOutputAmount)
const {
state: tradeState,
trade,
maximumAmountIn,
allowedSlippage,
} = useDerivedPayWithAnyTokenSwapInfo(usingPayWithAnyToken ? inputCurrency : undefined, parsedOutputAmount)
const { allowance, isAllowancePending, isApprovalLoading, updateAllowance } = usePermit2Approval(
trade?.inputAmount.currency.isToken ? (trade?.inputAmount as CurrencyAmount<Token>) : undefined,
maximumAmountIn,
shouldUsePayWithAnyToken
)
usePayWithAnyTokenSwap(trade, allowance, allowedSlippage)
const fiatValueTradeInput = useStablecoinValue(trade?.inputAmount)
const fiatValueTradeOutput = useStablecoinValue(parsedOutputAmount)
......@@ -324,88 +366,139 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
return { priceImpactWarning: true, priceImpactColor: theme.accentCritical }
}, [stablecoinPriceImpact, theme.accentCritical, theme.accentWarning])
const { buttonText, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor } = useMemo(() => {
let handleClick = fetchAssets
let buttonText = <Trans>Something went wrong</Trans>
let disabled = true
let warningText = undefined
let helperText = undefined
let helperTextColor = theme.textSecondary
let buttonColor = theme.accentAction
if (connected && chainId !== SupportedChainId.MAINNET) {
handleClick = () => switchChain(connector, SupportedChainId.MAINNET)
buttonText = <Trans>Switch networks</Trans>
disabled = false
warningText = <Trans>Wrong network</Trans>
} else if (sufficientBalance === false) {
buttonText = <Trans>Pay</Trans>
disabled = true
warningText = <Trans>Insufficient funds</Trans>
} else if (bagStatus === BagStatus.WARNING) {
warningText = <Trans>Something went wrong. Please try again.</Trans>
} else if (!connected) {
handleClick = () => {
toggleWalletModal()
setBagExpanded({ bagExpanded: false })
}
disabled = false
buttonText = <Trans>Connect wallet</Trans>
} else if (usingPayWithAnyToken && tradeState !== TradeState.VALID) {
disabled = true
buttonText = <Trans>Fetching Route</Trans>
} else if (allowance.state === AllowanceState.REQUIRED || allowance.state === AllowanceState.LOADING) {
handleClick = () => updateAllowance()
disabled = isAllowancePending || isApprovalLoading || allowance.state === AllowanceState.LOADING
if (allowance.state === AllowanceState.LOADING) {
buttonText = <Trans>Loading Allowance</Trans>
} else if (isAllowancePending) {
buttonText = <Trans>Approve in your wallet</Trans>
} else if (isApprovalLoading) {
buttonText = <Trans>Approval pending</Trans>
} else {
helperText = <Trans>An approval is needed to use this token. </Trans>
buttonText = <Trans>Approve</Trans>
const { balance: balanceInEth } = useWalletBalance()
const sufficientBalance = useMemo(() => {
if (!connected || chainId !== SupportedChainId.MAINNET) {
return undefined
}
if (inputCurrency) {
const inputAmount = trade?.inputAmount
if (!inputCurrencyBalance || !inputAmount) {
return undefined
}
} else if (bagStatus === BagStatus.FETCHING_FINAL_ROUTE || bagStatus === BagStatus.CONFIRMING_IN_WALLET) {
disabled = true
buttonText = <Trans>Proceed in wallet</Trans>
} else if (bagStatus === BagStatus.PROCESSING_TRANSACTION) {
disabled = true
buttonText = <Trans>Transaction pending</Trans>
} else if (priceImpactWarning && priceImpactColor) {
disabled = false
buttonColor = priceImpactColor
helperText = <Trans>Price impact warning</Trans>
helperTextColor = priceImpactColor
buttonText = <Trans>Pay Anyway</Trans>
} else if (sufficientBalance === true) {
disabled = false
buttonText = <Trans>Pay</Trans>
return !inputCurrencyBalance.lessThan(inputAmount)
}
return { buttonText, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor }
}, [
fetchAssets,
theme.textSecondary,
theme.accentAction,
connected,
chainId,
sufficientBalance,
bagStatus,
usingPayWithAnyToken,
tradeState,
allowance.state,
priceImpactWarning,
priceImpactColor,
connector,
toggleWalletModal,
setBagExpanded,
isAllowancePending,
isApprovalLoading,
updateAllowance,
])
return parseEther(balanceInEth).gte(totalEthPrice)
}, [connected, chainId, inputCurrency, balanceInEth, totalEthPrice, trade?.inputAmount, inputCurrencyBalance])
useEffect(() => {
setBagStatus(BagStatus.ADDING_TO_BAG)
}, [inputCurrency, setBagStatus])
const { buttonText, buttonTextColor, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor } =
useMemo(() => {
let handleClick = fetchAssets
let buttonText = <Trans>Something went wrong</Trans>
let disabled = true
let warningText = undefined
let helperText = undefined
let helperTextColor = theme.textSecondary
let buttonColor = theme.accentAction
let buttonTextColor = theme.accentTextLightPrimary
if (connected && chainId !== SupportedChainId.MAINNET) {
handleClick = () => switchChain(connector, SupportedChainId.MAINNET)
buttonText = <Trans>Switch networks</Trans>
disabled = false
warningText = <Trans>Wrong network</Trans>
} else if (sufficientBalance === false) {
buttonText = <Trans>Pay</Trans>
disabled = true
warningText = <Trans>Insufficient funds</Trans>
} else if (bagStatus === BagStatus.WARNING) {
warningText = <Trans>Something went wrong. Please try again.</Trans>
} else if (!connected) {
handleClick = () => {
toggleWalletModal()
setBagExpanded({ bagExpanded: false })
}
disabled = false
buttonText = <Trans>Connect wallet</Trans>
} else if (usingPayWithAnyToken && tradeState !== TradeState.VALID) {
disabled = true
buttonText = <Trans>Fetching Route</Trans>
if (tradeState === TradeState.INVALID) {
buttonText = <Trans>Pay</Trans>
}
if (tradeState === TradeState.NO_ROUTE_FOUND) {
buttonText = <Trans>Insufficient liquidity</Trans>
buttonColor = theme.backgroundInteractive
buttonTextColor = theme.textPrimary
helperText = <Trans>Insufficient pool liquidity to complete transaction</Trans>
}
} else if (allowance.state === AllowanceState.REQUIRED || allowance.state === AllowanceState.LOADING) {
handleClick = () => updateAllowance()
disabled = isAllowancePending || isApprovalLoading || allowance.state === AllowanceState.LOADING
if (allowance.state === AllowanceState.LOADING) {
buttonText = <Trans>Loading Allowance</Trans>
} else if (isAllowancePending) {
buttonText = <Trans>Approve in your wallet</Trans>
} else if (isApprovalLoading) {
buttonText = <Trans>Approval pending</Trans>
} else {
helperText = <Trans>An approval is needed to use this token. </Trans>
buttonText = <Trans>Approve</Trans>
}
} else if (bagStatus === BagStatus.FETCHING_FINAL_ROUTE || bagStatus === BagStatus.CONFIRMING_IN_WALLET) {
disabled = true
buttonText = <Trans>Proceed in wallet</Trans>
} else if (bagStatus === BagStatus.PROCESSING_TRANSACTION) {
disabled = true
buttonText = <Trans>Transaction pending</Trans>
} else if (priceImpactWarning && priceImpactColor) {
disabled = false
buttonColor = priceImpactColor
helperText = <Trans>Price impact warning</Trans>
helperTextColor = priceImpactColor
buttonText = <Trans>Pay Anyway</Trans>
} else if (sufficientBalance === true) {
disabled = false
buttonText = <Trans>Pay</Trans>
helperText = usingPayWithAnyToken ? (
<Trans>Refunds for unavailable items will be given in ETH</Trans>
) : undefined
}
return {
buttonText,
buttonTextColor,
disabled,
warningText,
helperText,
helperTextColor,
handleClick,
buttonColor,
}
}, [
fetchAssets,
theme.textSecondary,
theme.accentAction,
theme.accentTextLightPrimary,
theme.backgroundInteractive,
theme.textPrimary,
connected,
chainId,
sufficientBalance,
bagStatus,
usingPayWithAnyToken,
tradeState,
allowance.state,
priceImpactWarning,
priceImpactColor,
connector,
toggleWalletModal,
setBagExpanded,
isAllowancePending,
isApprovalLoading,
updateAllowance,
])
const traceEventProperties = {
usd_value: usdcValue?.toExact(),
......@@ -422,7 +515,7 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
<ThemedText.SubHeaderSmall>
<Trans>Pay with</Trans>
</ThemedText.SubHeaderSmall>
<CurrencyInput onClick={() => setTokenSelectorOpen(true)}>
<CurrencyInput onClick={() => (bagIsLocked ? undefined : setTokenSelectorOpen(true))}>
<CurrencyLogo currency={activeCurrency} size="24px" />
<ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px">
{activeCurrency?.symbol}
......@@ -443,7 +536,13 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
/>
</TotalColumn>
</CurrencyRow>
<FiatValue usdcValue={usdcValue} priceImpact={stablecoinPriceImpact} priceImpactColor={priceImpactColor} />
<FiatValue
usdcValue={usdcValue}
priceImpact={stablecoinPriceImpact}
priceImpactColor={priceImpactColor}
tradeState={tradeState}
usingPayWithAnyToken={usingPayWithAnyToken}
/>
</FooterHeader>
)}
{!shouldUsePayWithAnyToken && (
......@@ -459,7 +558,13 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
</ThemedText.HeadlineSmall>
</div>
</Row>
<FiatValue usdcValue={usdcValue} priceImpact={stablecoinPriceImpact} priceImpactColor={priceImpactColor} />
<FiatValue
usdcValue={usdcValue}
priceImpact={stablecoinPriceImpact}
priceImpactColor={priceImpactColor}
tradeState={tradeState}
usingPayWithAnyToken={usingPayWithAnyToken}
/>
</FooterHeader>
)}
<TraceEvent
......@@ -471,7 +576,12 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
>
<Warning>{warningText}</Warning>
<Helper color={helperTextColor}>{helperText}</Helper>
<ActionButton onClick={handleClick} disabled={disabled} backgroundColor={buttonColor}>
<ActionButton
onClick={handleClick}
disabled={disabled}
backgroundColor={buttonColor}
textColor={buttonTextColor}
>
{isPending && <Loader size="20px" stroke="white" />}
{buttonText}
</ActionButton>
......
import { style } from '@vanilla-extract/css'
import { Z_INDEX } from 'theme/zIndex'
import { sprinkles } from '../../css/sprinkles.css'
......@@ -11,10 +12,10 @@ export const overlay = style([
position: 'fixed',
display: 'block',
background: 'black',
zIndex: 'modalBackdrop',
}),
{
opacity: 0.72,
overflow: 'hidden',
zIndex: Z_INDEX.modalBackdrop - 1,
},
])
import { Currency, CurrencyAmount, NativeCurrency, Percent, Token, TradeType } from '@uniswap/sdk-core'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useBestTrade } from 'hooks/useBestTrade'
import { useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
export default function useDerivedPayWithAnyTokenSwapInfo(
inputCurrency?: Currency,
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token>
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
maximumAmountIn: CurrencyAmount<Token> | undefined
allowedSlippage: Percent
} {
const { state, trade } = useBestTrade(TradeType.EXACT_OUTPUT, parsedOutputAmount, inputCurrency ?? undefined)
const allowedSlippage = useAutoSlippageTolerance(trade)
const maximumAmountIn = useMemo(() => {
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
}, [allowedSlippage, trade])
return useMemo(() => {
return {
state,
trade,
maximumAmountIn,
allowedSlippage,
}
}, [allowedSlippage, maximumAmountIn, state, trade])
}
import { Currency, CurrencyAmount, NativeCurrency, Token, TradeType } from '@uniswap/sdk-core'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useBestTrade } from 'hooks/useBestTrade'
import { useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { PermitInput, TokenTradeRoutesInput, TokenTradeType } from 'graphql/data/__generated__/types-and-hooks'
import { Allowance } from 'hooks/usePermit2Allowance'
import { buildAllTradeRouteInputs } from 'nft/utils/tokenRoutes'
import { useEffect } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { useTokenInput } from './useTokenInput'
export default function usePayWithAnyTokenSwap(
inputCurrency?: Currency,
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token>
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
maximumAmountIn: CurrencyAmount<Token> | undefined
} {
const { state, trade } = useBestTrade(TradeType.EXACT_OUTPUT, parsedOutputAmount, inputCurrency ?? undefined)
const allowedSlippage = useAutoSlippageTolerance(trade)
const maximumAmountIn = useMemo(() => {
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
}, [allowedSlippage, trade])
return useMemo(() => {
return {
state,
trade,
maximumAmountIn,
trade?: InterfaceTrade<Currency, Currency, TradeType> | undefined,
allowance?: Allowance,
allowedSlippage?: Percent
) {
const setTokenTradeInput = useTokenInput((state) => state.setTokenTradeInput)
const hasRoutes = !!trade && trade.routes
const hasInputAmount = !!trade && !!trade.inputAmount && trade.inputAmount.currency.isToken
const hasAllowance = !!allowedSlippage && !!allowance
useEffect(() => {
if (!hasRoutes || !hasInputAmount || !hasAllowance) {
setTokenTradeInput(undefined)
return
}
const slippage = parseInt(allowedSlippage.multiply(100).toSignificant(2))
const { mixedTokenTradeRouteInputs, v2TokenTradeRouteInputs, v3TokenTradeRouteInputs } =
buildAllTradeRouteInputs(trade)
const routes: TokenTradeRoutesInput = {
mixedRoutes: mixedTokenTradeRouteInputs,
tradeType: TokenTradeType.ExactOutput,
v2Routes: v2TokenTradeRouteInputs,
v3Routes: v3TokenTradeRouteInputs,
}
}, [maximumAmountIn, state, trade])
const permitInput: PermitInput | undefined =
'permitSignature' in allowance && allowance.permitSignature
? {
details: {
amount: allowance.permitSignature.details.amount.toString(),
expiration: allowance.permitSignature.details.expiration.toString(),
nonce: allowance.permitSignature.details.nonce.toString(),
token: allowance.permitSignature.details.token,
},
sigDeadline: allowance.permitSignature.sigDeadline.toString(),
signature: allowance.permitSignature.signature,
spender: allowance.permitSignature.spender,
}
: undefined
setTokenTradeInput({
permit: permitInput,
routes,
slippageToleranceBasisPoints: slippage,
tokenAmount: {
amount: trade.inputAmount.quotient.toString(),
token: {
address: trade.inputAmount.currency.address,
chainId: trade.inputAmount.currency.chainId,
decimals: trade.inputAmount.currency.decimals,
isNative: trade.inputAmount.currency.isNative,
},
},
})
}, [allowance, allowedSlippage, hasAllowance, hasInputAmount, hasRoutes, setTokenTradeInput, trade])
}
......@@ -38,7 +38,7 @@ export const useSendTransaction = create<TxState>()(
try {
const txNoGasLimit = {
to: transactionData.to,
value: BigNumber.from(transactionData.valueToSend),
value: transactionData.valueToSend ? BigNumber.from(transactionData.valueToSend) : undefined,
data: transactionData.data,
}
......
import { Currency } from '@uniswap/sdk-core'
import { TokenTradeInput } from 'graphql/data/__generated__/types-and-hooks'
import create from 'zustand'
import { devtools } from 'zustand/middleware'
......@@ -6,14 +7,18 @@ interface TokenInputState {
inputCurrency: Currency | undefined
setInputCurrency: (currency: Currency | undefined) => void
clearInputCurrency: () => void
tokenTradeInput: TokenTradeInput | undefined
setTokenTradeInput: (tokenTradeInput: TokenTradeInput | undefined) => void
}
export const useTokenInput = create<TokenInputState>()(
devtools(
(set) => ({
inputCurrency: undefined,
tokenTradeInput: undefined,
setInputCurrency: (currency) => set(() => ({ inputCurrency: currency })),
clearInputCurrency: () => set(() => ({ inputCurrency: undefined })),
setTokenTradeInput: (tokenTradeInput) => set(() => ({ tokenTradeInput })),
}),
{ name: 'useTokenInput' }
)
......
......@@ -30,7 +30,7 @@ export type SellItem = {
export type BuyItem = {
id?: string
symbol?: string
name: string
name?: string
decimals: number
address: string
priceInfo: PriceInfo
......@@ -52,7 +52,7 @@ export type RoutingItem = {
}
export interface RouteResponse {
valueToSend: string
valueToSend?: string
route: RoutingItem[]
data: any
to: any
......
import { NftMarketplace, NftTradeInput, TokenAmountInput } from 'graphql/data/__generated__/types-and-hooks'
import { BagItem, BagItemStatus, UpdatedGenieAsset } from 'nft/types'
export const buildSellObject = (amount: string) => {
return {
address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
......@@ -14,3 +17,38 @@ export const buildSellObject = (amount: string) => {
tokenType: 'ERC20',
}
}
export const buildNftTradeInputFromBagItems = (itemsInBag: BagItem[]): NftTradeInput[] => {
const assetsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
return buildNftTradeInput(assetsToBuy)
}
const buildNftTradeInput = (assets: UpdatedGenieAsset[]): NftTradeInput[] => {
return assets.flatMap((asset) => {
const { id, address, marketplace, priceInfo, tokenId, tokenType } = asset
if (!id || !marketplace || !tokenType) return []
const ethAmountInput: TokenAmountInput = {
amount: priceInfo.ETHPrice,
token: {
address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
chainId: 1,
decimals: 18,
isNative: true,
},
}
return [
{
amount: 1,
contractAddress: address,
id,
marketplace: marketplace.toUpperCase() as NftMarketplace,
quotePrice: ethAmountInput,
tokenId,
tokenType,
},
]
})
}
import { NftRouteResponse, NftTrade } from 'graphql/data/__generated__/types-and-hooks'
import { Markets, RouteResponse, RoutingActions, RoutingItem, TokenType } from 'nft/types'
function buildRoutingItem(routingItem: NftTrade): RoutingItem {
return {
action: RoutingActions.Buy,
marketplace: routingItem.marketplace.toLowerCase(),
amountIn: routingItem.price.value,
assetIn: {
ETHPrice: routingItem.price.value,
baseAsset: routingItem.price.currency,
basePrice: routingItem.price.value,
baseDecimals: '18',
},
amountOut: routingItem.amount.toString(),
assetOut: {
id: routingItem.id,
decimals: 18,
address: routingItem.contractAddress,
priceInfo: {
ETHPrice: routingItem.price.value,
baseAsset: routingItem.price.currency,
basePrice: routingItem.price.value,
baseDecimals: '18',
},
tokenType: routingItem.tokenType as unknown as TokenType,
tokenId: routingItem.tokenId,
amount: routingItem.amount.toString(),
marketplace: routingItem.marketplace.toLowerCase() as Markets,
orderSource: 'api',
},
}
}
function buildRoutingItems(routingItems: NftTrade[]): RoutingItem[] {
return routingItems.map(buildRoutingItem)
}
export function buildRouteResponse(
routeResponse: NftRouteResponse,
useErc20Token: boolean
): { route: RoutingItem[]; routeResponse: RouteResponse } {
const route = routeResponse.route ? buildRoutingItems(routeResponse.route) : []
return {
route,
routeResponse: {
route,
valueToSend: useErc20Token ? undefined : routeResponse.sendAmount.value,
data: routeResponse.calldata,
to: routeResponse.toAddress,
},
}
}
import { IRoute, Protocol } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { Pool } from '@uniswap/v3-sdk'
import { TokenAmountInput, TokenTradeRouteInput, TradePoolInput } from 'graphql/data/__generated__/types-and-hooks'
import { InterfaceTrade } from 'state/routing/types'
interface SwapAmounts {
inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<Currency>
}
interface TradeTokenInputAmounts {
inputAmount: TokenAmountInput
outputAmount: TokenAmountInput
}
interface Swap {
route: IRoute<Currency, Currency, Pair | Pool>
inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<Currency>
}
function buildTradeRouteInputAmounts(swapAmounts: SwapAmounts): TradeTokenInputAmounts {
return {
inputAmount: {
amount: swapAmounts.inputAmount.quotient.toString(),
token: {
address: swapAmounts.inputAmount.currency.isToken
? swapAmounts.inputAmount.currency.address
: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
chainId: swapAmounts.inputAmount.currency.chainId,
decimals: swapAmounts.inputAmount.currency.decimals,
isNative: swapAmounts.inputAmount.currency.isNative,
},
},
outputAmount: {
amount: swapAmounts.outputAmount.quotient.toString(),
token: {
address: swapAmounts.outputAmount.currency.isToken
? swapAmounts.outputAmount.currency.address
: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
chainId: swapAmounts.outputAmount.currency.chainId,
decimals: swapAmounts.outputAmount.currency.decimals,
isNative: swapAmounts.outputAmount.currency.isNative,
},
},
}
}
function buildPool(pool: Pair | Pool): TradePoolInput {
const isPool = 'fee' in pool
return {
pair: !isPool
? {
tokenAmountA: {
amount: pool.reserve0.quotient.toString(),
token: {
address: pool.token0.address,
chainId: pool.token0.chainId,
decimals: pool.token0.decimals,
isNative: pool.token0.isNative,
},
},
tokenAmountB: {
amount: pool.reserve1.quotient.toString(),
token: {
address: pool.token1.address,
chainId: pool.token1.chainId,
decimals: pool.token1.decimals,
isNative: pool.token1.isNative,
},
},
}
: undefined,
pool: isPool
? {
fee: pool.fee,
liquidity: pool.liquidity.toString(),
sqrtRatioX96: pool.sqrtRatioX96.toString(),
tickCurrent: pool.tickCurrent.toString(),
tokenA: {
address: pool.token0.address,
chainId: pool.token0.chainId,
decimals: pool.token0.decimals,
isNative: pool.token0.isNative,
},
tokenB: {
address: pool.token1.address,
chainId: pool.token1.chainId,
decimals: pool.token1.decimals,
isNative: pool.token1.isNative,
},
}
: undefined,
}
}
function buildPools(pools: (Pair | Pool)[]): TradePoolInput[] {
return pools.map((pool) => buildPool(pool))
}
function buildTradeRouteInput(swap: Swap): TokenTradeRouteInput {
return {
...buildTradeRouteInputAmounts({ inputAmount: swap.inputAmount, outputAmount: swap.outputAmount }),
pools: buildPools(swap.route.pools),
}
}
export function buildAllTradeRouteInputs(trade: InterfaceTrade<Currency, Currency, TradeType>): {
mixedTokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
v2TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
v3TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
} {
const mixedTokenTradeRouteInputs: TokenTradeRouteInput[] = []
const v2TokenTradeRouteInputs: TokenTradeRouteInput[] = []
const v3TokenTradeRouteInputs: TokenTradeRouteInput[] = []
const swaps = trade.swaps
for (const swap of swaps) {
if (swap.route.protocol === Protocol.MIXED) {
mixedTokenTradeRouteInputs.push(buildTradeRouteInput(swap))
} else if (swap.route.protocol === Protocol.V2) {
v2TokenTradeRouteInputs.push(buildTradeRouteInput(swap))
} else {
v3TokenTradeRouteInputs.push(buildTradeRouteInput(swap))
}
}
return {
mixedTokenTradeRouteInputs: mixedTokenTradeRouteInputs.length > 0 ? mixedTokenTradeRouteInputs : undefined,
v2TokenTradeRouteInputs: v2TokenTradeRouteInputs.length > 0 ? v2TokenTradeRouteInputs : undefined,
v3TokenTradeRouteInputs: v3TokenTradeRouteInputs.length > 0 ? v3TokenTradeRouteInputs : undefined,
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment