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 { ...@@ -41,6 +41,7 @@ export enum ActivityType {
Send = 'SEND', Send = 'SEND',
Stake = 'STAKE', Stake = 'STAKE',
Swap = 'SWAP', Swap = 'SWAP',
Swapx = 'SWAPX',
Staking = 'Staking', Staking = 'Staking',
Unknown = 'UNKNOWN', Unknown = 'UNKNOWN',
Unstake = 'UNSTAKE', Unstake = 'UNSTAKE',
...@@ -587,6 +588,7 @@ export type Portfolio = { ...@@ -587,6 +588,7 @@ export type Portfolio = {
export type PortfolioAssetActivitiesArgs = { export type PortfolioAssetActivitiesArgs = {
includeOffChain?: InputMaybe<Scalars['Boolean']>;
page?: InputMaybe<Scalars['Int']>; page?: InputMaybe<Scalars['Int']>;
pageSize?: InputMaybe<Scalars['Int']>; pageSize?: InputMaybe<Scalars['Int']>;
}; };
...@@ -1048,7 +1050,7 @@ export type NftRouteQueryVariables = Exact<{ ...@@ -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` export const RecentlySearchedAssetsDocument = gql`
...@@ -1989,6 +1991,7 @@ export const NftRouteDocument = gql` ...@@ -1989,6 +1991,7 @@ export const NftRouteDocument = gql`
nftTrades: $nftTrades nftTrades: $nftTrades
tokenTrades: $tokenTrades tokenTrades: $tokenTrades
) { ) {
id
calldata calldata
route { route {
amount amount
...@@ -1996,10 +1999,12 @@ export const NftRouteDocument = gql` ...@@ -1996,10 +1999,12 @@ export const NftRouteDocument = gql`
id id
marketplace marketplace
price { price {
id
currency currency
value value
} }
quotePrice { quotePrice {
id
currency currency
value value
} }
...@@ -2007,6 +2012,7 @@ export const NftRouteDocument = gql` ...@@ -2007,6 +2012,7 @@ export const NftRouteDocument = gql`
tokenType tokenType
} }
sendAmount { sendAmount {
id
currency currency
value value
} }
......
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { NftTradeInput, TokenTradeInput, useNftRouteQuery } from '../__generated__/types-and-hooks'
gql` gql`
query NftRoute( query NftRoute(
$chain: Chain = ETHEREUM $chain: Chain = ETHEREUM
...@@ -10,6 +8,7 @@ gql` ...@@ -10,6 +8,7 @@ gql`
$tokenTrades: [TokenTradeInput!] $tokenTrades: [TokenTradeInput!]
) { ) {
nftRoute(chain: $chain, senderAddress: $senderAddress, nftTrades: $nftTrades, tokenTrades: $tokenTrades) { nftRoute(chain: $chain, senderAddress: $senderAddress, nftTrades: $nftTrades, tokenTrades: $tokenTrades) {
id
calldata calldata
route { route {
amount amount
...@@ -17,10 +16,12 @@ gql` ...@@ -17,10 +16,12 @@ gql`
id id
marketplace marketplace
price { price {
id
currency currency
value value
} }
quotePrice { quotePrice {
id
currency currency
value value
} }
...@@ -28,6 +29,7 @@ gql` ...@@ -28,6 +29,7 @@ gql`
tokenType tokenType
} }
sendAmount { sendAmount {
id
currency currency
value value
} }
...@@ -35,13 +37,3 @@ gql` ...@@ -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 { ...@@ -27,7 +27,7 @@ interface AllowanceRequired {
approveAndPermit: () => Promise<void> approveAndPermit: () => Promise<void>
} }
type Allowance = export type Allowance =
| { state: AllowanceState.LOADING } | { state: AllowanceState.LOADING }
| { | {
state: AllowanceState.ALLOWED state: AllowanceState.ALLOWED
......
...@@ -4,7 +4,7 @@ import { NFTEventName } from '@uniswap/analytics-events' ...@@ -4,7 +4,7 @@ import { NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting' import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2' 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 { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
import { BagFooter } from 'nft/components/bag/BagFooter' import { BagFooter } from 'nft/components/bag/BagFooter'
import ListingModal from 'nft/components/bag/profile/ListingModal' import ListingModal from 'nft/components/bag/profile/ListingModal'
...@@ -21,14 +21,17 @@ import { ...@@ -21,14 +21,17 @@ import {
useSendTransaction, useSendTransaction,
useTransactionResponse, useTransactionResponse,
} from 'nft/hooks' } from 'nft/hooks'
import { useTokenInput } from 'nft/hooks/useTokenInput'
import { fetchRoute } from 'nft/queries' import { fetchRoute } from 'nft/queries'
import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types' import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types'
import { import {
buildNftTradeInputFromBagItems,
buildSellObject, buildSellObject,
formatAssetEventProperties, formatAssetEventProperties,
recalculateBagUsingPooledAssets, recalculateBagUsingPooledAssets,
sortUpdatedAssets, sortUpdatedAssets,
} from 'nft/utils' } from 'nft/utils'
import { buildRouteResponse } from 'nft/utils/nftRoute'
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute' import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
...@@ -50,7 +53,7 @@ interface SeparatorProps { ...@@ -50,7 +53,7 @@ interface SeparatorProps {
show?: boolean show?: boolean
} }
const BagContainer = styled.div<{ raiseZIndex: boolean }>` const BagContainer = styled.div<{ raiseZIndex: boolean; isProfilePage: boolean }>`
position: fixed; position: fixed;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -62,7 +65,8 @@ const BagContainer = styled.div<{ raiseZIndex: boolean }>` ...@@ -62,7 +65,8 @@ const BagContainer = styled.div<{ raiseZIndex: boolean }>`
border: 1px solid ${({ theme }) => theme.backgroundOutline}; border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 16px; border-radius: 16px;
box-shadow: ${({ theme }) => theme.shallowShadow}; 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`}) { @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
right: 0px; right: 0px;
...@@ -143,6 +147,7 @@ const Bag = () => { ...@@ -143,6 +147,7 @@ const Bag = () => {
const setTransactionState = useSendTransaction((state) => state.setState) const setTransactionState = useSendTransaction((state) => state.setState)
const transactionStateRef = useRef(transactionState) const transactionStateRef = useRef(transactionState)
const [setTransactionResponse] = useTransactionResponse((state) => [state.setTransactionResponse]) const [setTransactionResponse] = useTransactionResponse((state) => [state.setTransactionResponse])
const tokenTradeInput = useTokenInput((state) => state.tokenTradeInput)
const queryClient = useQueryClient() const queryClient = useQueryClient()
...@@ -197,7 +202,8 @@ const Bag = () => { ...@@ -197,7 +202,8 @@ const Bag = () => {
setBagExpanded({ bagExpanded: false, manualClose: true }) setBagExpanded({ bagExpanded: false, manualClose: true })
}, [setBagExpanded]) }, [setBagExpanded])
useNftRoute(usingGqlRouting ? account ?? '' : '', []) const [fetchGqlRoute] = useNftRouteLazyQuery()
const fetchAssets = async () => { const fetchAssets = async () => {
const itemsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset) const itemsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
const ethSellObject = buildSellObject( const ethSellObject = buildSellObject(
...@@ -210,7 +216,72 @@ const Bag = () => { ...@@ -210,7 +216,72 @@ const Bag = () => {
!bagIsLocked && setLocked(true) !bagIsLocked && setLocked(true)
setBagStatus(BagStatus.FETCHING_ROUTE) setBagStatus(BagStatus.FETCHING_ROUTE)
try { try {
const data = await queryClient.fetchQuery(['assetsRoute', ethSellObject, itemsToBuy, 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)
},
})
} else {
const routeData = await queryClient.fetchQuery(['assetsRoute', ethSellObject, itemsToBuy, account], () =>
fetchRoute({ fetchRoute({
toSell: [ethSellObject], toSell: [ethSellObject],
toBuy: itemsToBuy, toBuy: itemsToBuy,
...@@ -218,9 +289,11 @@ const Bag = () => { ...@@ -218,9 +289,11 @@ const Bag = () => {
}) })
) )
const updatedAssets = combineBuyItemsWithTxRoute(itemsToBuy, data.route) const updatedAssets = combineBuyItemsWithTxRoute(itemsToBuy, routeData.route)
const fetchedPriceChangedAssets = updatedAssets.filter((asset) => asset.updatedPriceInfo).sort(sortUpdatedAssets) const fetchedPriceChangedAssets = updatedAssets
.filter((asset) => asset.updatedPriceInfo)
.sort(sortUpdatedAssets)
const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable) const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
const fetchedUnchangedAssets = updatedAssets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable) const fetchedUnchangedAssets = updatedAssets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
const hasReviewedAssets = fetchedUnchangedAssets.length > 0 const hasReviewedAssets = fetchedUnchangedAssets.length > 0
...@@ -238,13 +311,16 @@ const Bag = () => { ...@@ -238,13 +311,16 @@ const Bag = () => {
asset: changedAsset, asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE, status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})), })),
...fetchedUnchangedAssets.map((unchangedAsset) => ({ asset: unchangedAsset, status: BagItemStatus.REVIEWED })), ...fetchedUnchangedAssets.map((unchangedAsset) => ({
asset: unchangedAsset,
status: BagItemStatus.REVIEWED,
})),
]) ])
setLocked(false) setLocked(false)
if (hasAssets) { if (hasAssets) {
if (!shouldReview) { if (!shouldReview) {
purchaseAssets(data) purchaseAssets(routeData)
setBagStatus(BagStatus.CONFIRMING_IN_WALLET) setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW) } else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else { else {
...@@ -253,6 +329,7 @@ const Bag = () => { ...@@ -253,6 +329,7 @@ const Bag = () => {
} else { } else {
setBagStatus(BagStatus.ADDING_TO_BAG) setBagStatus(BagStatus.ADDING_TO_BAG)
} }
}
} catch (error) { } catch (error) {
setBagStatus(BagStatus.ADDING_TO_BAG) setBagStatus(BagStatus.ADDING_TO_BAG)
} }
...@@ -313,7 +390,7 @@ const Bag = () => { ...@@ -313,7 +390,7 @@ const Bag = () => {
return ( return (
<Portal> <Portal>
<BagContainer data-testid="nft-bag" raiseZIndex={isMobile || isModalOpen}> <BagContainer data-testid="nft-bag" raiseZIndex={isMobile || isModalOpen} isProfilePage={isProfilePage}>
{!(isProfilePage && profilePageState === ProfilePageStateType.LISTING) ? ( {!(isProfilePage && profilePageState === ProfilePageStateType.LISTING) ? (
<> <>
<BagHeader <BagHeader
......
...@@ -19,15 +19,17 @@ import { usePayWithAnyTokenEnabled } from 'featureFlags/flags/payWithAnyToken' ...@@ -19,15 +19,17 @@ import { usePayWithAnyTokenEnabled } from 'featureFlags/flags/payWithAnyToken'
import { useCurrency } from 'hooks/Tokens' import { useCurrency } from 'hooks/Tokens'
import { AllowanceState } from 'hooks/usePermit2Allowance' import { AllowanceState } from 'hooks/usePermit2Allowance'
import { useStablecoinValue } from 'hooks/useStablecoinPrice' import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useBag } from 'nft/hooks/useBag' import { useBag } from 'nft/hooks/useBag'
import useDerivedPayWithAnyTokenSwapInfo from 'nft/hooks/useDerivedPayWithAnyTokenSwapInfo'
import usePayWithAnyTokenSwap from 'nft/hooks/usePayWithAnyTokenSwap' import usePayWithAnyTokenSwap from 'nft/hooks/usePayWithAnyTokenSwap'
import usePermit2Approval from 'nft/hooks/usePermit2Approval' import usePermit2Approval from 'nft/hooks/usePermit2Approval'
import { useTokenInput } from 'nft/hooks/useTokenInput' import { useTokenInput } from 'nft/hooks/useTokenInput'
import { useWalletBalance } from 'nft/hooks/useWalletBalance' import { useWalletBalance } from 'nft/hooks/useWalletBalance'
import { BagStatus } from 'nft/types' import { BagStatus } from 'nft/types'
import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils' 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 { AlertTriangle, ChevronDown } from 'react-feather'
import { useToggleWalletModal } from 'state/application/hooks' import { useToggleWalletModal } from 'state/application/hooks'
import { InterfaceTrade, TradeState } from 'state/routing/types' import { InterfaceTrade, TradeState } from 'state/routing/types'
...@@ -36,6 +38,7 @@ import { ThemedText } from 'theme' ...@@ -36,6 +38,7 @@ import { ThemedText } from 'theme'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact' import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { warningSeverity } from 'utils/prices' import { warningSeverity } from 'utils/prices'
import { switchChain } from 'utils/switchChain' import { switchChain } from 'utils/switchChain'
import shallow from 'zustand/shallow'
const LOW_SEVERITY_THRESHOLD = 1 const LOW_SEVERITY_THRESHOLD = 1
const MEDIUM_SEVERITY_THRESHOLD = 3 const MEDIUM_SEVERITY_THRESHOLD = 3
...@@ -63,10 +66,12 @@ const FooterHeader = styled(Column)<{ usingPayWithAnyToken?: boolean }>` ...@@ -63,10 +66,12 @@ const FooterHeader = styled(Column)<{ usingPayWithAnyToken?: boolean }>`
const CurrencyRow = styled(Row)` const CurrencyRow = styled(Row)`
justify-content: space-between; justify-content: space-between;
align-items: start; align-items: start;
gap: 8px;
` `
const TotalColumn = styled(Column)` const TotalColumn = styled(Column)`
text-align: end; text-align: end;
overflow-x: hidden;
` `
const WarningIcon = styled(AlertTriangle)` const WarningIcon = styled(AlertTriangle)`
...@@ -96,10 +101,10 @@ const CurrencyInput = styled(Row)` ...@@ -96,10 +101,10 @@ const CurrencyInput = styled(Row)`
cursor: pointer; cursor: pointer;
` `
const PayButton = styled.button<{ $backgroundColor: string }>` const PayButton = styled.button<{ $backgroundColor: string; $color: string }>`
display: flex; display: flex;
background: ${({ $backgroundColor }) => $backgroundColor}; background: ${({ $backgroundColor }) => $backgroundColor};
color: ${({ theme }) => theme.accentTextLightPrimary}; color: ${({ $color }) => $color};
font-weight: 600; font-weight: 600;
line-height: 24px; line-height: 24px;
font-size: 16px; font-size: 16px;
...@@ -134,15 +139,35 @@ const PriceImpactRow = styled(Row)` ...@@ -134,15 +139,35 @@ const PriceImpactRow = styled(Row)`
gap: 8px; 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 { interface ActionButtonProps {
disabled?: boolean disabled?: boolean
onClick: () => void onClick: () => void
backgroundColor: string backgroundColor: string
textColor: string
} }
const ActionButton = ({ disabled, children, onClick, backgroundColor }: PropsWithChildren<ActionButtonProps>) => { const ActionButton = ({
disabled,
children,
onClick,
backgroundColor,
textColor,
}: PropsWithChildren<ActionButtonProps>) => {
return ( return (
<PayButton disabled={disabled} onClick={onClick} $backgroundColor={backgroundColor}> <PayButton disabled={disabled} onClick={onClick} $backgroundColor={backgroundColor} $color={textColor}>
{children} {children}
</PayButton> </PayButton>
) )
...@@ -175,7 +200,6 @@ const Helper = ({ children, color }: PropsWithChildren<HelperTextProps>) => { ...@@ -175,7 +200,6 @@ const Helper = ({ children, color }: PropsWithChildren<HelperTextProps>) => {
) )
} }
// TODO: ask design about no route found
const InputCurrencyValue = ({ const InputCurrencyValue = ({
usingPayWithAnyToken, usingPayWithAnyToken,
totalEthPrice, totalEthPrice,
...@@ -198,22 +222,18 @@ const InputCurrencyValue = ({ ...@@ -198,22 +222,18 @@ const InputCurrencyValue = ({
) )
} }
if (tradeState === TradeState.VALID || tradeState === TradeState.SYNCING) { if (tradeState === TradeState.LOADING) {
return ( return (
<ThemedText.BodyPrimary <ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500">
lineHeight="20px" <Trans>Fetching price...</Trans>
fontWeight="500"
color={tradeState === TradeState.VALID ? 'textPrimary' : 'textTertiary'}
>
{ethNumberStandardFormatter(trade?.inputAmount.toExact())}
</ThemedText.BodyPrimary> </ThemedText.BodyPrimary>
) )
} }
return ( return (
<ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500"> <ValueText color={tradeState === TradeState.SYNCING ? 'textTertiary' : 'textPrimary'}>
<Trans>Fetching price...</Trans> {ethNumberStandardFormatter(trade?.inputAmount.toExact())}
</ThemedText.BodyPrimary> </ValueText>
) )
} }
...@@ -221,12 +241,20 @@ const FiatValue = ({ ...@@ -221,12 +241,20 @@ const FiatValue = ({
usdcValue, usdcValue,
priceImpact, priceImpact,
priceImpactColor, priceImpactColor,
tradeState,
usingPayWithAnyToken,
}: { }: {
usdcValue: CurrencyAmount<Token> | null usdcValue: CurrencyAmount<Token> | null
priceImpact: Percent | undefined priceImpact: Percent | undefined
priceImpactColor: string | undefined priceImpactColor: string | undefined
tradeState: TradeState
usingPayWithAnyToken: boolean
}) => { }) => {
if (!usdcValue) { if (!usdcValue) {
if (usingPayWithAnyToken && (tradeState === TradeState.INVALID || tradeState === TradeState.NO_ROUTE_FOUND)) {
return null
}
return <FiatLoadingBubble /> return <FiatLoadingBubble />
} }
...@@ -274,31 +302,45 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti ...@@ -274,31 +302,45 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
const inputCurrency = useTokenInput((state) => state.inputCurrency) const inputCurrency = useTokenInput((state) => state.inputCurrency)
const setInputCurrency = useTokenInput((state) => state.setInputCurrency) const setInputCurrency = useTokenInput((state) => state.setInputCurrency)
const defaultCurrency = useCurrency('ETH') const defaultCurrency = useCurrency('ETH')
const inputCurrencyBalance = useTokenBalance(
account ?? undefined,
!!inputCurrency && inputCurrency.isToken ? inputCurrency : undefined
)
const setBagExpanded = useBag((state) => state.setBagExpanded) const {
const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false) isLocked: bagIsLocked,
setBagExpanded,
setBagStatus,
} = useBag(
({ isLocked, setBagExpanded, setBagStatus }) => ({
isLocked,
setBagExpanded,
setBagStatus,
}),
shallow
)
const { balance: balanceInEth } = useWalletBalance() const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false)
const sufficientBalance = useMemo(() => {
if (!connected || chainId !== SupportedChainId.MAINNET) {
return undefined
}
return parseEther(balanceInEth).gte(totalEthPrice)
}, [connected, chainId, balanceInEth, totalEthPrice])
const isPending = PENDING_BAG_STATUSES.includes(bagStatus) const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
const activeCurrency = inputCurrency ?? defaultCurrency const activeCurrency = inputCurrency ?? defaultCurrency
const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken && chainId === SupportedChainId.MAINNET
const parsedOutputAmount = useMemo(() => { const parsedOutputAmount = useMemo(() => {
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined) return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
}, [defaultCurrency, totalEthPrice]) }, [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( const { allowance, isAllowancePending, isApprovalLoading, updateAllowance } = usePermit2Approval(
trade?.inputAmount.currency.isToken ? (trade?.inputAmount as CurrencyAmount<Token>) : undefined, trade?.inputAmount.currency.isToken ? (trade?.inputAmount as CurrencyAmount<Token>) : undefined,
maximumAmountIn, maximumAmountIn,
shouldUsePayWithAnyToken shouldUsePayWithAnyToken
) )
usePayWithAnyTokenSwap(trade, allowance, allowedSlippage)
const fiatValueTradeInput = useStablecoinValue(trade?.inputAmount) const fiatValueTradeInput = useStablecoinValue(trade?.inputAmount)
const fiatValueTradeOutput = useStablecoinValue(parsedOutputAmount) const fiatValueTradeOutput = useStablecoinValue(parsedOutputAmount)
...@@ -324,7 +366,31 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti ...@@ -324,7 +366,31 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
return { priceImpactWarning: true, priceImpactColor: theme.accentCritical } return { priceImpactWarning: true, priceImpactColor: theme.accentCritical }
}, [stablecoinPriceImpact, theme.accentCritical, theme.accentWarning]) }, [stablecoinPriceImpact, theme.accentCritical, theme.accentWarning])
const { buttonText, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor } = useMemo(() => { 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
}
return !inputCurrencyBalance.lessThan(inputAmount)
}
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 handleClick = fetchAssets
let buttonText = <Trans>Something went wrong</Trans> let buttonText = <Trans>Something went wrong</Trans>
let disabled = true let disabled = true
...@@ -332,6 +398,7 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti ...@@ -332,6 +398,7 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
let helperText = undefined let helperText = undefined
let helperTextColor = theme.textSecondary let helperTextColor = theme.textSecondary
let buttonColor = theme.accentAction let buttonColor = theme.accentAction
let buttonTextColor = theme.accentTextLightPrimary
if (connected && chainId !== SupportedChainId.MAINNET) { if (connected && chainId !== SupportedChainId.MAINNET) {
handleClick = () => switchChain(connector, SupportedChainId.MAINNET) handleClick = () => switchChain(connector, SupportedChainId.MAINNET)
...@@ -354,6 +421,17 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti ...@@ -354,6 +421,17 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
} else if (usingPayWithAnyToken && tradeState !== TradeState.VALID) { } else if (usingPayWithAnyToken && tradeState !== TradeState.VALID) {
disabled = true disabled = true
buttonText = <Trans>Fetching Route</Trans> 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) { } else if (allowance.state === AllowanceState.REQUIRED || allowance.state === AllowanceState.LOADING) {
handleClick = () => updateAllowance() handleClick = () => updateAllowance()
disabled = isAllowancePending || isApprovalLoading || allowance.state === AllowanceState.LOADING disabled = isAllowancePending || isApprovalLoading || allowance.state === AllowanceState.LOADING
...@@ -383,13 +461,28 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti ...@@ -383,13 +461,28 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
} else if (sufficientBalance === true) { } else if (sufficientBalance === true) {
disabled = false disabled = false
buttonText = <Trans>Pay</Trans> buttonText = <Trans>Pay</Trans>
helperText = usingPayWithAnyToken ? (
<Trans>Refunds for unavailable items will be given in ETH</Trans>
) : undefined
} }
return { buttonText, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor } return {
buttonText,
buttonTextColor,
disabled,
warningText,
helperText,
helperTextColor,
handleClick,
buttonColor,
}
}, [ }, [
fetchAssets, fetchAssets,
theme.textSecondary, theme.textSecondary,
theme.accentAction, theme.accentAction,
theme.accentTextLightPrimary,
theme.backgroundInteractive,
theme.textPrimary,
connected, connected,
chainId, chainId,
sufficientBalance, sufficientBalance,
...@@ -422,7 +515,7 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti ...@@ -422,7 +515,7 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
<ThemedText.SubHeaderSmall> <ThemedText.SubHeaderSmall>
<Trans>Pay with</Trans> <Trans>Pay with</Trans>
</ThemedText.SubHeaderSmall> </ThemedText.SubHeaderSmall>
<CurrencyInput onClick={() => setTokenSelectorOpen(true)}> <CurrencyInput onClick={() => (bagIsLocked ? undefined : setTokenSelectorOpen(true))}>
<CurrencyLogo currency={activeCurrency} size="24px" /> <CurrencyLogo currency={activeCurrency} size="24px" />
<ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px"> <ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px">
{activeCurrency?.symbol} {activeCurrency?.symbol}
...@@ -443,7 +536,13 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti ...@@ -443,7 +536,13 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
/> />
</TotalColumn> </TotalColumn>
</CurrencyRow> </CurrencyRow>
<FiatValue usdcValue={usdcValue} priceImpact={stablecoinPriceImpact} priceImpactColor={priceImpactColor} /> <FiatValue
usdcValue={usdcValue}
priceImpact={stablecoinPriceImpact}
priceImpactColor={priceImpactColor}
tradeState={tradeState}
usingPayWithAnyToken={usingPayWithAnyToken}
/>
</FooterHeader> </FooterHeader>
)} )}
{!shouldUsePayWithAnyToken && ( {!shouldUsePayWithAnyToken && (
...@@ -459,7 +558,13 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti ...@@ -459,7 +558,13 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
</ThemedText.HeadlineSmall> </ThemedText.HeadlineSmall>
</div> </div>
</Row> </Row>
<FiatValue usdcValue={usdcValue} priceImpact={stablecoinPriceImpact} priceImpactColor={priceImpactColor} /> <FiatValue
usdcValue={usdcValue}
priceImpact={stablecoinPriceImpact}
priceImpactColor={priceImpactColor}
tradeState={tradeState}
usingPayWithAnyToken={usingPayWithAnyToken}
/>
</FooterHeader> </FooterHeader>
)} )}
<TraceEvent <TraceEvent
...@@ -471,7 +576,12 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti ...@@ -471,7 +576,12 @@ export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperti
> >
<Warning>{warningText}</Warning> <Warning>{warningText}</Warning>
<Helper color={helperTextColor}>{helperText}</Helper> <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" />} {isPending && <Loader size="20px" stroke="white" />}
{buttonText} {buttonText}
</ActionButton> </ActionButton>
......
import { style } from '@vanilla-extract/css' import { style } from '@vanilla-extract/css'
import { Z_INDEX } from 'theme/zIndex'
import { sprinkles } from '../../css/sprinkles.css' import { sprinkles } from '../../css/sprinkles.css'
...@@ -11,10 +12,10 @@ export const overlay = style([ ...@@ -11,10 +12,10 @@ export const overlay = style([
position: 'fixed', position: 'fixed',
display: 'block', display: 'block',
background: 'black', background: 'black',
zIndex: 'modalBackdrop',
}), }),
{ {
opacity: 0.72, opacity: 0.72,
overflow: 'hidden', 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 { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance' import { PermitInput, TokenTradeRoutesInput, TokenTradeType } from 'graphql/data/__generated__/types-and-hooks'
import { useBestTrade } from 'hooks/useBestTrade' import { Allowance } from 'hooks/usePermit2Allowance'
import { useMemo } from 'react' import { buildAllTradeRouteInputs } from 'nft/utils/tokenRoutes'
import { InterfaceTrade, TradeState } from 'state/routing/types' import { useEffect } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { useTokenInput } from './useTokenInput'
export default function usePayWithAnyTokenSwap( export default function usePayWithAnyTokenSwap(
inputCurrency?: Currency, trade?: InterfaceTrade<Currency, Currency, TradeType> | undefined,
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token> allowance?: Allowance,
): { allowedSlippage?: Percent
state: TradeState ) {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined const setTokenTradeInput = useTokenInput((state) => state.setTokenTradeInput)
maximumAmountIn: CurrencyAmount<Token> | undefined const hasRoutes = !!trade && trade.routes
} { const hasInputAmount = !!trade && !!trade.inputAmount && trade.inputAmount.currency.isToken
const { state, trade } = useBestTrade(TradeType.EXACT_OUTPUT, parsedOutputAmount, inputCurrency ?? undefined) const hasAllowance = !!allowedSlippage && !!allowance
const allowedSlippage = useAutoSlippageTolerance(trade)
const maximumAmountIn = useMemo(() => { useEffect(() => {
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage) if (!hasRoutes || !hasInputAmount || !hasAllowance) {
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined setTokenTradeInput(undefined)
}, [allowedSlippage, trade]) return
return useMemo(() => {
return {
state,
trade,
maximumAmountIn,
} }
}, [maximumAmountIn, state, trade])
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,
}
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>()( ...@@ -38,7 +38,7 @@ export const useSendTransaction = create<TxState>()(
try { try {
const txNoGasLimit = { const txNoGasLimit = {
to: transactionData.to, to: transactionData.to,
value: BigNumber.from(transactionData.valueToSend), value: transactionData.valueToSend ? BigNumber.from(transactionData.valueToSend) : undefined,
data: transactionData.data, data: transactionData.data,
} }
......
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import { TokenTradeInput } from 'graphql/data/__generated__/types-and-hooks'
import create from 'zustand' import create from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
...@@ -6,14 +7,18 @@ interface TokenInputState { ...@@ -6,14 +7,18 @@ interface TokenInputState {
inputCurrency: Currency | undefined inputCurrency: Currency | undefined
setInputCurrency: (currency: Currency | undefined) => void setInputCurrency: (currency: Currency | undefined) => void
clearInputCurrency: () => void clearInputCurrency: () => void
tokenTradeInput: TokenTradeInput | undefined
setTokenTradeInput: (tokenTradeInput: TokenTradeInput | undefined) => void
} }
export const useTokenInput = create<TokenInputState>()( export const useTokenInput = create<TokenInputState>()(
devtools( devtools(
(set) => ({ (set) => ({
inputCurrency: undefined, inputCurrency: undefined,
tokenTradeInput: undefined,
setInputCurrency: (currency) => set(() => ({ inputCurrency: currency })), setInputCurrency: (currency) => set(() => ({ inputCurrency: currency })),
clearInputCurrency: () => set(() => ({ inputCurrency: undefined })), clearInputCurrency: () => set(() => ({ inputCurrency: undefined })),
setTokenTradeInput: (tokenTradeInput) => set(() => ({ tokenTradeInput })),
}), }),
{ name: 'useTokenInput' } { name: 'useTokenInput' }
) )
......
...@@ -30,7 +30,7 @@ export type SellItem = { ...@@ -30,7 +30,7 @@ export type SellItem = {
export type BuyItem = { export type BuyItem = {
id?: string id?: string
symbol?: string symbol?: string
name: string name?: string
decimals: number decimals: number
address: string address: string
priceInfo: PriceInfo priceInfo: PriceInfo
...@@ -52,7 +52,7 @@ export type RoutingItem = { ...@@ -52,7 +52,7 @@ export type RoutingItem = {
} }
export interface RouteResponse { export interface RouteResponse {
valueToSend: string valueToSend?: string
route: RoutingItem[] route: RoutingItem[]
data: any data: any
to: 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) => { export const buildSellObject = (amount: string) => {
return { return {
address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
...@@ -14,3 +17,38 @@ export const buildSellObject = (amount: string) => { ...@@ -14,3 +17,38 @@ export const buildSellObject = (amount: string) => {
tokenType: 'ERC20', 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