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
......
This diff is collapsed.
This diff is collapsed.
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 { const slippage = parseInt(allowedSlippage.multiply(100).toSignificant(2))
state,
trade, const { mixedTokenTradeRouteInputs, v2TokenTradeRouteInputs, v3TokenTradeRouteInputs } =
maximumAmountIn, 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>()( ...@@ -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