From 16c49035216d32c81c0bf9526a12466707b6a7ee Mon Sep 17 00:00:00 2001 From: Juanma Hidalgo Date: Thu, 26 Oct 2023 13:03:32 +0200 Subject: [PATCH] feat: cross-chain feature moved to buy page --- .../BuyNFTButtons/BuyNFTButtons.tsx | 10 +- .../components/BuyPage/BuyPage.container.tsx | 13 + webapp/src/components/BuyPage/BuyPage.tsx | 11 +- .../src/components/BuyPage/BuyPage.types.ts | 4 + .../BuyWithCryptoModal.container.ts | 61 ++ .../BuyWithCryptoModal.module.css | 147 +++ .../BuyWithCryptoModal/BuyWithCryptoModal.tsx | 961 ++++++++++++++++++ .../BuyWithCryptoModal.types.ts | 46 + .../RouteSummary/RouteSummary.module.css | 57 ++ .../RouteSummary/RouteSummary.tsx | 104 ++ .../BuyPage/BuyWithCryptoModal/index.ts | 1 + .../BuyPage/BuyWithCryptoModal/utils.ts | 15 + .../NotEnoughMana/NotEnoughMana.module.css | 12 +- .../BuyPage/NotEnoughMana/NotEnoughMana.tsx | 14 +- .../NotEnoughMana/NotEnoughMana.types.ts | 1 + .../PriceTooLow/PriceTooLow.module.css | 14 +- .../BuyPage/PriceTooLow/PriceTooLow.tsx | 16 +- .../BuyPage/PriceTooLow/PriceTooLow.types.ts | 1 + webapp/src/components/BuyPage/index.ts | 2 +- .../RouteSummary/RouteSummary.tsx | 3 +- .../Modals/BuyWithCryptoModal/utils.ts | 15 + webapp/src/lib/axelar.ts | 312 ++++++ webapp/src/lib/xchain.ts | 317 +----- webapp/src/modules/features/selectors.spec.ts | 8 +- webapp/src/modules/features/selectors.ts | 11 + webapp/src/modules/features/types.ts | 3 +- webapp/src/modules/item/actions.ts | 39 + webapp/src/modules/item/sagas.ts | 47 +- 28 files changed, 1923 insertions(+), 322 deletions(-) create mode 100644 webapp/src/components/BuyPage/BuyPage.container.tsx create mode 100644 webapp/src/components/BuyPage/BuyWithCryptoModal/BuyWithCryptoModal.container.ts create mode 100644 webapp/src/components/BuyPage/BuyWithCryptoModal/BuyWithCryptoModal.module.css create mode 100644 webapp/src/components/BuyPage/BuyWithCryptoModal/BuyWithCryptoModal.tsx create mode 100644 webapp/src/components/BuyPage/BuyWithCryptoModal/BuyWithCryptoModal.types.ts create mode 100644 webapp/src/components/BuyPage/BuyWithCryptoModal/RouteSummary/RouteSummary.module.css create mode 100644 webapp/src/components/BuyPage/BuyWithCryptoModal/RouteSummary/RouteSummary.tsx create mode 100644 webapp/src/components/BuyPage/BuyWithCryptoModal/index.ts create mode 100644 webapp/src/components/BuyPage/BuyWithCryptoModal/utils.ts create mode 100644 webapp/src/components/Modals/BuyWithCryptoModal/utils.ts create mode 100644 webapp/src/lib/axelar.ts diff --git a/webapp/src/components/AssetPage/SaleActionBox/BuyNFTButtons/BuyNFTButtons.tsx b/webapp/src/components/AssetPage/SaleActionBox/BuyNFTButtons/BuyNFTButtons.tsx index e91449125d..74abe4ef86 100644 --- a/webapp/src/components/AssetPage/SaleActionBox/BuyNFTButtons/BuyNFTButtons.tsx +++ b/webapp/src/components/AssetPage/SaleActionBox/BuyNFTButtons/BuyNFTButtons.tsx @@ -28,10 +28,6 @@ const BuyNFTButtons = ({ return ( <> - + {/* */} + ) + }, [onSwitchNetwork, providerChains, selectedChain]) + + const renderBuyWithCryptoButton = useCallback(() => { + return ( + + ) + }, [ + canBuyItem, + isBuying, + isFetchingBalance, + providerTokens, + route, + selectedChain, + selectedToken, + onBuyWithCrypto + ]) + + const onBuyNFT = useCallback(() => { + if (order && isNFT(asset)) { + const contractNames = getContractNames() + + const mana = getContractProp({ + name: contractNames.MANA, + network: asset.network + }) as DCLContract + + const marketplace = getContractProp({ + address: order?.marketplaceAddress, + network: asset.network + }) as DCLContract + + onAuthorizedAction({ + targetContractName: ContractName.MANAToken, + authorizationType: AuthorizationType.ALLOWANCE, + authorizedAddress: order.marketplaceAddress, + targetContract: mana as Contract, + authorizedContractLabel: marketplace.label || marketplace.name, + requiredAllowanceInWei: order.price, + onAuthorized: alreadyAuthorized => + onExecuteOrder(order, asset, undefined, !alreadyAuthorized) // undefined as fingerprint + }) + } + }, [asset, order, getContractProp, onAuthorizedAction, onExecuteOrder]) + + const onBuyItem = useCallback(() => { + if (order && !isNFT(asset)) { + const contractNames = getContractNames() + + const mana = getContractProp({ + name: contractNames.MANA, + network: asset.network + }) as DCLContract + + const collectionStore = getContractProp({ + name: contractNames.COLLECTION_STORE, + network: asset.network + }) as DCLContract + + onAuthorizedAction({ + targetContractName: ContractName.MANAToken, + authorizationType: AuthorizationType.ALLOWANCE, + authorizedAddress: collectionStore.address, + targetContract: mana as Contract, + authorizedContractLabel: collectionStore.label || collectionStore.name, + requiredAllowanceInWei: asset.price, + onAuthorized: () => onBuyItemProp(asset) + }) + } + }, [order, asset, getContractProp, onAuthorizedAction, onBuyItemProp]) + + const { pathname, search } = useLocation() + + const isInsufficientMANA = useMemo(() => { + return ( + !!wallet && + wallet.networks[asset.network].mana < + +ethers.utils.formatEther( + order ? order.price : !isNFT(asset) ? asset.price : '' + ) + ) + }, [asset, order, wallet]) + console.log('isInsufficientMANA: ', isInsufficientMANA) + + const renderBuyWithMANAButton = useCallback(() => { + if (!wallet) { + return + } + + // const price = order + // ? +ethers.utils.formatEther(order.price) + // : !isNFT(asset) + // ? +ethers.utils.formatEther(asset.price) + // : 0 + + // const isInsufficientMANA = wallet.networks[asset.network].mana < price + + return ( + + ) + }, [ + wallet, + asset, + isFetchingBalance, + isBuying, + isInsufficientMANA, + pathname, + search, + onBuyNFT, + onBuyItem + ]) + + const onBuyWithCard = useCallback(() => { + if (isBuyWithCardPage) { + analytics.track(events.CLICK_BUY_NFT_WITH_CARD) + return isNFT(asset) + ? onExecuteOrderWithCard(asset) + : onBuyItemWithCard(asset) + } + }, [ + analytics, + asset, + isBuyWithCardPage, + onBuyItemWithCard, + onExecuteOrderWithCard + ]) + + const canBuyDirectlyWithMANA = useMemo(() => { + return selectedToken + ? selectedToken.symbol === 'MANA' && + selectedChain === asset.chainId && + !isInsufficientMANA + : !isInsufficientMANA && !hasLowPrice + }, [ + asset.chainId, + hasLowPrice, + isInsufficientMANA, + selectedChain, + selectedToken + ]) + + const renderConfirmButton = useCallback(() => { + if (isBuyWithCardPage) { + return ( + + {/* {isWearableOrEmote(nft) ? : null} */} + + {t(`buy_with_crypto.buy_with_card`)} + + ) + } + + // if it has MANA selected in the token selector + if (showTokenSelector && !useMetaTx && selectedToken) { + // connected on the same asset network + // if (canBuyDirectlyWithMANA) { + // return renderBuyWithMANAButton() + // } + // offer to swtich network if MANA not selected + if ( + selectedChain !== wallet?.chainId + // && + // selectedToken.symbol !== 'MANA' + ) { + return switchNetworkButton + } + } + + const showBuyWithMANAButton = !isInsufficientMANA && !hasLowPrice + console.log('showBuyWithMANAButton: ', showBuyWithMANAButton) + return !showTokenSelector || useMetaTx || canBuyDirectlyWithMANA + ? showBuyWithMANAButton + ? renderBuyWithMANAButton() + : null + : renderBuyWithCryptoButton() + }, [ + isBuyWithCardPage, + showTokenSelector, + useMetaTx, + selectedToken, + isInsufficientMANA, + hasLowPrice, + canBuyDirectlyWithMANA, + renderBuyWithMANAButton, + renderBuyWithCryptoButton, + isLoading, + isLoadingAuthorization, + onBuyWithCard, + asset.chainId, + selectedChain, + wallet?.chainId, + switchNetworkButton + ]) + + const handleCancel = useCallback(() => { + if (isBuyWithCardPage) analytics.track(events.CANCEL_BUY_NFT_WITH_CARD) + }, [analytics, isBuyWithCardPage]) + + const translationPageDescriptorId = compact([ + 'mint', + isWearableOrEmote(asset) + ? isBuyWithCardPage + ? 'with_card' + : 'with_mana' + : null, + 'page' + ]).join('_') + + return ( +
+ +
+ {t('buy_with_crypto.title', { + name: asset.name, + b: (children: React.ReactChildren) => {children} + })} +
+ <> +
+ + {t('buy_with_crypto.subtitle', { + name: {getAssetName(asset)}, + amount: ( + + {formatWeiMANA( + order ? order.price : !isNFT(asset) ? asset.price : '' + )} + + ) + })} + +
+ + {showTokenSelector ? ( + !providerTokens.length || !selectedToken ? ( + + ) : ( + <> + {showTokenSelector ? ( +
+
+ {t('buy_with_crypto.choose_chain')} + ({ + text: chain.networkName, + value: +chain.chainId, + image: { + avatar: true, + src: chain.nativeCurrency.icon + } + }))} + onChange={(_, data) => { + setSelectedChain(data.value as any) + setRoute(undefined) + }} + /> +
+
+ {t('buy_with_crypto.choose_token')} + token.chainId === selectedChain.toString() + ) + .map(token => ({ + text: token.symbol, + value: token.address, + image: { avatar: true, src: token.logoURI } + }))} + onChange={onTokenDropdownChange} + /> +
+ {t('buy_with_crypto.balance')}:{' '} + {selectedTokenBalance && !isFetchingBalance ? ( + + {ethers.utils + .formatUnits( + selectedTokenBalance, + selectedToken.decimals + ) + .toString() + .slice(0, 6)} + + ) : ( +
+ )} +
+
+
+ ) : null} + {useMetaTx || + canBuyDirectlyWithMANA || + (selectedToken.symbol === 'MANA' && + selectedChain === asset.chainId) ? null : ( + <> + + {routeFailed && selectedToken ? ( + + {' '} + {t('buy_with_crypto.route_unavailable', { + token: selectedToken.symbol + })} + + ) : null} + + )} + + ) + ) : null} + + {hasLowPrice && !isBuyWithCardPage && useMetaTx ? ( + setShowTokenSelector(true)} + /> + ) : null} + {(canBuyItem === false || isInsufficientMANA) && + isWearableOrEmote(asset) ? ( + setShowTokenSelector(true) + } + description={t('buy_with_crypto.not_enough_funds', { + name: asset.name, + network: ( + + { + providerChains.find( + chain => chain.chainId === selectedChain.toString() + )?.networkName + } + + ), + token: {selectedToken?.symbol || 'MANA'}, + amount: ( + + ) + })} + /> + ) : null} + + +
+ + {renderConfirmButton()} +
+ {isWearableOrEmote(asset) && isBuyWithCardPage ? ( + + ) : null} + +
+ ) +} + +export default React.memo( + withAuthorizedAction( + BuyWithCryptoModal, + AuthorizedAction.BUY, + { + action: 'buy_with_mana_page.authorization.action', + title_action: 'buy_with_mana_page.authorization.title_action' + }, + getBuyItemStatus, + getError + ) +) diff --git a/webapp/src/components/BuyPage/BuyWithCryptoModal/BuyWithCryptoModal.types.ts b/webapp/src/components/BuyPage/BuyWithCryptoModal/BuyWithCryptoModal.types.ts new file mode 100644 index 0000000000..c4b913a3e0 --- /dev/null +++ b/webapp/src/components/BuyPage/BuyWithCryptoModal/BuyWithCryptoModal.types.ts @@ -0,0 +1,46 @@ +import { Order } from '@dcl/schemas' +import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types' +import { switchNetworkRequest } from 'decentraland-dapps/dist/modules/wallet/actions' +import { WithAuthorizedActionProps } from 'decentraland-dapps/dist/containers/withAuthorizedAction' +import { Asset } from '../../../modules/asset/types' +import { Route } from '../../../lib/xchain' +import { + buyItemRequest, + buyItemWithCardRequest +} from '../../../modules/item/actions' +import { getContract } from '../../../modules/contract/selectors' +import { Contract } from '../../../modules/vendor/services' +import { + executeOrderRequest, + executeOrderWithCardRequest +} from '../../../modules/order/actions' + +export type Props = { + asset: Asset + order: Order | null + wallet: Wallet | null + isLoading: boolean + isBuyWithCardPage: boolean + getContract: (query: Partial) => ReturnType + onSwitchNetwork: typeof switchNetworkRequest + onBuyItem: typeof buyItemRequest + onBuyItemWithCard: typeof buyItemWithCardRequest + onBuyItemThroughProvider: (route: Route) => void + onExecuteOrder: typeof executeOrderRequest + onExecuteOrderWithCard: typeof executeOrderWithCardRequest +} & WithAuthorizedActionProps + +export type OwnProps = Pick +export type MapStateProps = Pick< + Props, + 'wallet' | 'order' | 'getContract' | 'isLoading' | 'isBuyWithCardPage' +> +export type MapDispatchProps = Pick< + Props, + | 'onSwitchNetwork' + | 'onBuyItemThroughProvider' + | 'onBuyItem' + | 'onExecuteOrder' + | 'onExecuteOrderWithCard' + | 'onBuyItemWithCard' +> diff --git a/webapp/src/components/BuyPage/BuyWithCryptoModal/RouteSummary/RouteSummary.module.css b/webapp/src/components/BuyPage/BuyWithCryptoModal/RouteSummary/RouteSummary.module.css new file mode 100644 index 0000000000..8e1c90355a --- /dev/null +++ b/webapp/src/components/BuyPage/BuyWithCryptoModal/RouteSummary/RouteSummary.module.css @@ -0,0 +1,57 @@ +.summaryContainer { + display: flex; + flex-direction: column; + font-size: 14px; + color: var(--secondary-text); + margin-bottom: 12px; +} + +@keyframes wave { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.summaryRow { + display: flex; + align-items: center; + margin-bottom: 2px; + align-items: center; +} + +.skeleton { + flex-shrink: 0; + background: var(--dcl-pale-black, #43404a); + margin-right: 16px; + background: linear-gradient(90deg, #43404a 25%, #63616d 50%, #43404a 75%); + background-size: 200% 100%; + animation: wave 1.5s infinite; + border-radius: 4px; +} + +.exchangeSkeleton { + width: 167px; + height: 16px; + margin-left: 6px; +} + +.composeSkeletonConatiner { + display: flex; + flex-direction: row; + align-items: center; +} + +.shortSkeleton { + width: 60px; + height: 16px; + margin-right: 0; +} + +.longSkeleton { + width: 167px; + height: 16px; + margin-left: 8px; +} diff --git a/webapp/src/components/BuyPage/BuyWithCryptoModal/RouteSummary/RouteSummary.tsx b/webapp/src/components/BuyPage/BuyWithCryptoModal/RouteSummary/RouteSummary.tsx new file mode 100644 index 0000000000..4ce177535a --- /dev/null +++ b/webapp/src/components/BuyPage/BuyWithCryptoModal/RouteSummary/RouteSummary.tsx @@ -0,0 +1,104 @@ +import { ethers } from 'ethers' +import classNames from 'classnames' +import { Token, RouteResponse } from '@0xsquid/sdk/dist/types' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' +import styles from './RouteSummary.module.css' + +export type RouteSummaryProps = { + route: RouteResponse | undefined + selectedToken: Token +} + +const RouteSummary = ({ route, selectedToken }: RouteSummaryProps) => { + return ( + <> +
+ {t('buy_with_crypto.summary')} +
+ {t('buy_with_crypto.route.nft_cost')} + {route ? ( + <> + {ethers.utils.formatUnits( + route.route.estimate.fromAmount, + selectedToken.decimals + )}{' '} + {selectedToken.symbol} + + ) : ( +
+ )} +
+
+ {t('buy_with_crypto.route.exchange_rate')} + {route ? ( + <> + 1 {selectedToken.symbol} ={' '} + {route.route.estimate.exchangeRate?.slice(0, 7)} MANA + + ) : ( +
+ )} +
+ +
+ {t('buy_with_crypto.route.estimated_route_duration')} + {route ? ( + <>{route.route.estimate.estimatedRouteDuration}s + ) : ( +
+ )} +
+ + {route?.route.estimate.gasCosts.map((gasCost, index) => ( + + {t('buy_with_crypto.route.gas_cost_in_token', { + cost: ethers.utils.formatEther(gasCost.amount).slice(0, 6), + token: gasCost.token.symbol, + costInUSD: gasCost.amountUsd + })} + + )) || ( +
+ {[...Array(2).keys()].map((_, index) => ( +
+
+ : +
+
+ ))} +
+ )} + {route?.route.estimate.feeCosts.map(feeCost => ( + + {t('buy_with_crypto.route.fee_cost', { + feeName: feeCost.name, + cost: ethers.utils.formatEther(feeCost.amount).slice(0, 6), + token: feeCost.token.symbol, + costInUSD: feeCost.amountUsd + })} + + )) || ( +
+
+ : +
+
+ )} +
+ + ) +} + +export default RouteSummary diff --git a/webapp/src/components/BuyPage/BuyWithCryptoModal/index.ts b/webapp/src/components/BuyPage/BuyWithCryptoModal/index.ts new file mode 100644 index 0000000000..0ee16a9392 --- /dev/null +++ b/webapp/src/components/BuyPage/BuyWithCryptoModal/index.ts @@ -0,0 +1 @@ +export { default as BuyWithCryptoModal } from './BuyWithCryptoModal.container' diff --git a/webapp/src/components/BuyPage/BuyWithCryptoModal/utils.ts b/webapp/src/components/BuyPage/BuyWithCryptoModal/utils.ts new file mode 100644 index 0000000000..194a830bda --- /dev/null +++ b/webapp/src/components/BuyPage/BuyWithCryptoModal/utils.ts @@ -0,0 +1,15 @@ +import { ChainId } from '@dcl/schemas' +import { Asset } from '../../../modules/asset/types' + +export const getShouldUseMetaTx = ( + asset: Asset, + selectedChain: ChainId, + selectedTokenAddress: string, + destinyChainMANA: string +) => { + return ( + asset.chainId === ChainId.MATIC_MAINNET && + selectedChain === ChainId.ETHEREUM_MAINNET && + selectedTokenAddress.toLowerCase() === destinyChainMANA.toLowerCase() + ) +} diff --git a/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.module.css b/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.module.css index 302b5ec4b2..d5ba2ff660 100644 --- a/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.module.css +++ b/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.module.css @@ -3,10 +3,21 @@ width: 460px !important; } +.card :global(.ui.button + .ui.button) { + /* Override semantic UI's width */ + margin-left: 0; +} + .card:global(.ui.card > .content) { padding: 20px 24px 24px; } +.card + :global(.ui.button:not(.icon) + > .icon:not(.button):not(.dropdown).ethereum.icon) { + margin-right: 2px; +} + .paragraph { margin-bottom: 10px; color: var(--text); @@ -20,7 +31,6 @@ margin-top: 6px; } - @media (max-width: 768px) { .card { /* Override semantic UI's width */ diff --git a/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.tsx b/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.tsx index 6c49c33230..7f6dcdcbbe 100644 --- a/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.tsx +++ b/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.tsx @@ -6,7 +6,13 @@ import { Props } from './NotEnoughMana.types' import styles from './NotEnoughMana.module.css' const NotEnoughMana = (props: Props) => { - const { asset, description, onGetMana, onBuyWithCard } = props + const { + asset, + description, + onGetMana, + onBuyWithCard, + onBuyWithAnotherToken + } = props return ( @@ -32,6 +38,12 @@ const NotEnoughMana = (props: Props) => { {t('asset_page.actions.buy_with_card')} + {onBuyWithAnotherToken ? ( + + ) : null}
diff --git a/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.types.ts b/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.types.ts index 81336079ae..8df8204004 100644 --- a/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.types.ts +++ b/webapp/src/components/BuyPage/NotEnoughMana/NotEnoughMana.types.ts @@ -15,6 +15,7 @@ export type Props = { description: React.ReactNode onGetMana: typeof openBuyManaWithFiatModalRequest onBuyWithCard: (asset: Asset) => ReturnType + onBuyWithAnotherToken?: () => void } export type MapDispatchProps = Pick diff --git a/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.module.css b/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.module.css index 6a459bfba4..a12552cb86 100644 --- a/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.module.css +++ b/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.module.css @@ -3,6 +3,17 @@ width: 460px !important; } +.card :global(.ui.button + .ui.button) { + /* Override semantic UI's width */ + margin-left: 0; +} + +.card + :global(.ui.button:not(.icon) + > .icon:not(.button):not(.dropdown).ethereum.icon) { + margin-right: 2px; +} + .card.buyWithCard { /* Override semantic UI's width */ width: 416px !important; @@ -20,10 +31,9 @@ padding-left: 0px !important; } - @media (max-width: 768px) { .card { /* Override semantic UI's width */ width: 100% !important; } -} \ No newline at end of file +} diff --git a/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.tsx b/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.tsx index a51be81d78..76725343b6 100644 --- a/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.tsx +++ b/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.tsx @@ -1,6 +1,6 @@ import React from 'react' import classNames from 'classnames' -import { Button, Card } from 'decentraland-ui' +import { Button, Card, Icon } from 'decentraland-ui' import { T, t } from 'decentraland-dapps/dist/modules/translation/utils' import { getMinSaleValueInWei } from '../utils' import { Price } from '../Price' @@ -8,7 +8,13 @@ import { Props } from './PriceTooLow.types' import styles from './PriceTooLow.module.css' const PriceTooLow = (props: Props) => { - const { chainId, network, isBuyWithCardPage, onSwitchNetwork } = props + const { + chainId, + network, + isBuyWithCardPage, + onSwitchNetwork, + onBuyWithAnotherToken + } = props const humanNetwork = t(`networks.${network.toLowerCase()}`) const humanToken = t(`tokens.${network.toLowerCase()}`) @@ -54,6 +60,12 @@ const PriceTooLow = (props: Props) => { {t('global.learn_more')} ) : null} + {onBuyWithAnotherToken ? ( + + ) : null}
diff --git a/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.types.ts b/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.types.ts index d5937b32ba..0e685840e0 100644 --- a/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.types.ts +++ b/webapp/src/components/BuyPage/PriceTooLow/PriceTooLow.types.ts @@ -10,6 +10,7 @@ export type Props = { network: Network isBuyWithCardPage: boolean onSwitchNetwork: typeof switchNetworkRequest + onBuyWithAnotherToken?: () => void } export type MapStateProps = Pick diff --git a/webapp/src/components/BuyPage/index.ts b/webapp/src/components/BuyPage/index.ts index 068d2b2507..7af85b102f 100644 --- a/webapp/src/components/BuyPage/index.ts +++ b/webapp/src/components/BuyPage/index.ts @@ -1,2 +1,2 @@ -import BuyPage from './BuyPage' +import BuyPage from './BuyPage.container' export { BuyPage } diff --git a/webapp/src/components/Modals/BuyWithCryptoModal/RouteSummary/RouteSummary.tsx b/webapp/src/components/Modals/BuyWithCryptoModal/RouteSummary/RouteSummary.tsx index abed603d6a..0285f1d017 100644 --- a/webapp/src/components/Modals/BuyWithCryptoModal/RouteSummary/RouteSummary.tsx +++ b/webapp/src/components/Modals/BuyWithCryptoModal/RouteSummary/RouteSummary.tsx @@ -19,8 +19,7 @@ const RouteSummary = ({ route, selectedToken }: RouteSummaryProps) => { {route ? ( <> 1 {selectedToken.symbol} ={' '} - {route.route.estimate.exchangeRate?.slice(0, 7)}{' '} - {selectedToken.symbol} + {route.route.estimate.exchangeRate?.slice(0, 7)} MANA ) : (
{ + return ( + asset.chainId === ChainId.MATIC_MAINNET && + selectedChain === ChainId.MATIC_MAINNET && + selectedTokenAddress.toLowerCase() === destinyChainMANA.toLowerCase() + ) +} diff --git a/webapp/src/lib/axelar.ts b/webapp/src/lib/axelar.ts new file mode 100644 index 0000000000..d946d8c417 --- /dev/null +++ b/webapp/src/lib/axelar.ts @@ -0,0 +1,312 @@ +import { ethers } from 'ethers' +import { Squid } from '@0xsquid/sdk' +import { SquidCallType, ChainType } from '@0xsquid/sdk/dist/types' +import { ContractName, getContract } from 'decentraland-transactions' +import { Provider } from 'decentraland-dapps/dist/modules/wallet/types' +import { ERC20 } from './abis/ERC20' +import { MarketplaceV2 } from './abis/MarketplaceV2' +import { ERC721 } from './abis/ERC721' +import { CollectionStore } from './abis/CollectionStore' +import { + BuyNFTXChainData, + MintNFTXChainData, + RouteResponse, + XChainProvider +} from './xchain' + +export class AxelarProvider implements XChainProvider { + public squid: Squid + private squidMulticall = '0x4fd39C9E151e50580779bd04B1f7eCc310079fd3' // Squid calling contract + + constructor() { + this.squid = new Squid({ + baseUrl: 'https://v2.api.squidrouter.com', + integratorId: 'decentraland-sdk' + }) + this.squid.init() + } + + private async init() { + if (!this.squid.initialized) { + await this.squid.init() + } + } + + async executeRoute( + route: RouteResponse, + provider: Provider + ): Promise { + const signer = await new ethers.providers.Web3Provider(provider).getSigner() + + // tslint:disable-next-line + // @ts-ignore + const txResponse = (await this.squid.executeRoute({ + route: route.route, + signer + })) as ethers.providers.TransactionResponse + + return txResponse.wait() + } + + async buyNFT( + provider: Provider, + buyNFTXChainData: BuyNFTXChainData + ): Promise { + const route = await this.getBuyNFTRoute(buyNFTXChainData) + const tx = await this.executeRoute(route, provider) + return tx.transactionHash + } + + async getBuyNFTRoute( + buyNFTXChainData: BuyNFTXChainData + ): Promise { + await this.init() + const { + fromAddress, + fromAmount, + fromChain, + fromToken, + toChain, + toAmount, // the item price + enableExpress = true, // TODO: check if we need this + slippage = 1, // TODO: check if we need this + nft: { collectionAddress, price, tokenId } + } = buyNFTXChainData + + const ERC20ContractInterface = new ethers.utils.Interface(ERC20) + const marketplaceInterface = new ethers.utils.Interface(MarketplaceV2) + const ERC721ContractInterface = new ethers.utils.Interface(ERC721) + + const destinyChainMANA = getContract(ContractName.MANAToken, toChain) + .address + const destinyChainMarketplaceV2 = getContract( + ContractName.MarketplaceV2, + toChain + ).address + + return this.squid.getRoute({ + fromAddress, + fromAmount, + fromToken, + fromChain: fromChain.toString(), + toToken: destinyChainMANA, + toChain: toChain.toString(), + toAddress: destinyChainMarketplaceV2, + enableBoost: enableExpress, + slippageConfig: { + autoMode: 1 // 1 is "normal" slippage. Always set to 1 + }, + postHook: { + chainType: ChainType.EVM, + fundAmount: '1', + fundToken: destinyChainMANA, + calls: [ + // =================================== + // Approve MANA to be spent by Decentraland contract + // =================================== + { + // chainType: ChainType.EVM, + // callType: SquidCallType.FULL_TOKEN_BALANCE, + // target: destinyChainMANA, + // value: '0', + // callData: ERC20ContractInterface.encodeFunctionData('approve', [ + // getContract(ContractName.MarketplaceV2, toChain).address, + // toAmount + // ]), + // payload: { + // tokenAddress: destinyChainMarketplaceV2, + // inputPos: 1 + // }, + // estimatedGas: '50000' + chainType: ChainType.EVM, + callType: SquidCallType.DEFAULT, + target: destinyChainMANA, + value: '0', + callData: ERC20ContractInterface.encodeFunctionData('approve', [ + getContract(ContractName.MarketplaceV2, toChain).address, + toAmount + ]), + payload: { + tokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', + inputPos: 0 + }, + estimatedGas: '50000' + }, + // =================================== + // EXECUTE ORDER + // =================================== + { + chainType: ChainType.EVM, + callType: SquidCallType.DEFAULT, + target: destinyChainMarketplaceV2, + value: '0', + callData: marketplaceInterface.encodeFunctionData('executeOrder', [ + collectionAddress, + tokenId, + price + ]), + + payload: { + tokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // ex `0x` + inputPos: 0 + }, + estimatedGas: '300000' + }, + // =================================== + // Transfer NFT to buyer + // =================================== + { + chainType: ChainType.EVM, + callType: SquidCallType.DEFAULT, + target: collectionAddress, + value: '0', + callData: ERC721ContractInterface.encodeFunctionData( + 'safeTransferFrom(address, address, uint256)', + [this.squidMulticall, fromAddress, tokenId] + ), + payload: { + tokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', + inputPos: 1 + }, + estimatedGas: '50000' + }, + // =================================== + // Transfer remaining MANA to buyer + // =================================== + { + chainType: ChainType.EVM, + callType: SquidCallType.FULL_TOKEN_BALANCE, + target: destinyChainMANA, + value: '0', + callData: ERC20ContractInterface.encodeFunctionData('transfer', [ + fromAddress, + '0' + ]), + payload: { + tokenAddress: destinyChainMANA, + // This will replace the parameter at index 1 in the encoded Function, + // with FULL_TOKEN_BALANCE (instead of "0") + inputPos: 1 + }, + estimatedGas: '50000' + } + ] + } + }) + } + + // MINT + async mintNFT( + provider: Provider, + mintNFTXChainData: MintNFTXChainData + ): Promise { + const route = await this.getMintNFTRoute(mintNFTXChainData) + const tx = await this.executeRoute(route, provider) + return tx.transactionHash + } + + async getMintNFTRoute( + buyNFTXChainData: MintNFTXChainData + ): Promise { + await this.init() + const { + fromAddress, + fromAmount, + fromChain, + fromToken, + toChain, + toAmount, // the item price + enableExpress = true, + slippage = 1, + item: { collectionAddress, price, itemId } + } = buyNFTXChainData + + const ERC20ContractInterface = new ethers.utils.Interface(ERC20) + const collectionStoreInterface = new ethers.utils.Interface(CollectionStore) + + const destinyChainMANA = getContract(ContractName.MANAToken, toChain) + .address + const destinyChaiCollectionStoreAddress = getContract( + ContractName.CollectionStore, + toChain + ).address + + return this.squid.getRoute({ + fromAddress, + fromAmount, + fromToken, + fromChain: fromChain.toString(), + toToken: destinyChainMANA, + toChain: toChain.toString(), + toAddress: fromAddress, + enableBoost: enableExpress, // TODO: check if we need this + slippageConfig: { + autoMode: 1 // 1 is "normal" slippage. Always set to 1 + }, + postHook: { + chainType: ChainType.EVM, + fundAmount: '1', + fundToken: destinyChainMANA, + calls: [ + // =================================== + // Approve MANA to be spent by Decentraland contract + // =================================== + { + chainType: ChainType.EVM, + callType: SquidCallType.DEFAULT, + target: destinyChainMANA, + value: '0', + callData: ERC20ContractInterface.encodeFunctionData('approve', [ + getContract(ContractName.CollectionStore, toChain).address, + toAmount + ]), + payload: { + tokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', + inputPos: 0 + }, + estimatedGas: '50000' + }, + // =================================== + // BUY ITEM + // =================================== + { + chainType: ChainType.EVM, + callType: SquidCallType.DEFAULT, + target: destinyChaiCollectionStoreAddress, + value: '0', // @TODO: WHY 0? + callData: collectionStoreInterface.encodeFunctionData( + 'buy((address,uint256[],uint256[],address[])[])', + [[[collectionAddress, [itemId], [price], [fromAddress]]]] + ), + + payload: { + tokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // TODO: do we need this to be set as the native? it's working like this + inputPos: 0 + }, + estimatedGas: '300000' // TODO: where do we get this value from? + }, + // =================================== + // Transfer remaining MANA to buyer + // =================================== + { + chainType: ChainType.EVM, + callType: SquidCallType.FULL_TOKEN_BALANCE, + target: destinyChainMANA, + value: '0', + callData: ERC20ContractInterface.encodeFunctionData('transfer', [ + fromAddress, + '0' + ]), + payload: { + tokenAddress: destinyChainMANA, + // This will replace the parameter at index 1 in the encoded Function, + // with FULL_TOKEN_BALANCE (instead of "0") + inputPos: 1 + }, + estimatedGas: '50000' + } + ] + } + }) + } +} diff --git a/webapp/src/lib/xchain.ts b/webapp/src/lib/xchain.ts index b0271668ab..8176d5c7d0 100644 --- a/webapp/src/lib/xchain.ts +++ b/webapp/src/lib/xchain.ts @@ -1,17 +1,13 @@ -import { ethers } from 'ethers' -import { Squid } from '@0xsquid/sdk' import { - RouteResponse, - SquidCallType, - ChainType + ChainData as SquidChainData, + Token as SquidToken, + RouteResponse as SquidRouteResponse } from '@0xsquid/sdk/dist/types' import { ChainId } from '@dcl/schemas' -import { ContractName, getContract } from 'decentraland-transactions' import { Provider } from 'decentraland-dapps/dist/modules/wallet/types' -import { ERC20 } from './abis/ERC20' -import { MarketplaceV2 } from './abis/MarketplaceV2' -import { ERC721 } from './abis/ERC721' -import { CollectionStore } from './abis/CollectionStore' +import { AxelarProvider } from './axelar' + +export type Route = RouteResponse export type BuyNFTXChainData = { fromAddress: string @@ -19,7 +15,7 @@ export type BuyNFTXChainData = { toAmount: string fromToken: string fromChain: ChainId - toChain: ChainId // TODO: Do we need ETH? + toChain: ChainId enableExpress?: boolean slippage?: number nft: { @@ -27,9 +23,6 @@ export type BuyNFTXChainData = { tokenId: string price: string } - // toToken: string // it will be MANA - // toChain: ChainId.MATIC_MAINNET | ChainId.MATIC_MUMBAI // TODO: Do we need ETH? - // toAddress: string } export type MintNFTXChainData = Omit & { @@ -40,6 +33,15 @@ export type MintNFTXChainData = Omit & { } } +export const SUPPORTED_CHAINS = [ + ChainId.ETHEREUM_MAINNET, + ChainId.MATIC_MAINNET +] + +export type ChainData = SquidChainData +export type Token = SquidToken +export type RouteResponse = SquidRouteResponse + export interface XChainProvider { buyNFT( provider: Provider, @@ -48,289 +50,4 @@ export interface XChainProvider { mintNFT(provider: Provider, ChainCallData: MintNFTXChainData): Promise } -export class AxelarProvider implements XChainProvider { - private squid: Squid - private squidMulticall = '0x4fd39C9E151e50580779bd04B1f7eCc310079fd3' // Squid calling contract - - constructor() { - this.squid = new Squid({ - baseUrl: 'https://v2.api.squidrouter.com', - // baseUrl: 'https://api.squidrouter.com', - integratorId: 'decentraland-sdk' - }) - this.squid.init() - } - - private async init() { - if (!this.squid.initialized) { - await this.squid.init() - } - } - - async executeRoute( - route: RouteResponse, - provider: Provider - ): Promise { - const signer = await new ethers.providers.Web3Provider(provider).getSigner() - - // tslint:disable-next-line - // @ts-ignore - const txResponse = (await this.squid.executeRoute({ - route: route.route, - signer - })) as ethers.providers.TransactionResponse - - return txResponse.wait() - } - - async buyNFT( - provider: Provider, - buyNFTXChainData: BuyNFTXChainData - ): Promise { - const route = await this.getBuyNFTRoute(buyNFTXChainData) - const tx = await this.executeRoute(route, provider) - return tx.transactionHash - } - - async getBuyNFTRoute( - buyNFTXChainData: BuyNFTXChainData - ): Promise { - await this.init() - const { - fromAddress, - fromAmount, - fromChain, - fromToken, - toChain, - toAmount, // the item price - enableExpress = true, // TODO: check if we need this - slippage = 1, // TODO: check if we need this - nft: { collectionAddress, price, tokenId } - } = buyNFTXChainData - - const ERC20ContractInterface = new ethers.utils.Interface(ERC20) - const marketplaceInterface = new ethers.utils.Interface(MarketplaceV2) - const ERC721ContractInterface = new ethers.utils.Interface(ERC721) - - const destinyChainMANA = getContract(ContractName.MANAToken, toChain) - .address - const destinyChainMarketplaceV2 = getContract( - ContractName.MarketplaceV2, - toChain - ).address - - return this.squid.getRoute({ - fromAddress, - fromAmount, - fromToken, - fromChain: fromChain.toString(), - toToken: destinyChainMANA, - toChain: toChain.toString(), - toAddress: destinyChainMarketplaceV2, - enableBoost: enableExpress, - slippageConfig: { - autoMode: 1 // 1 is "normal" slippage. Always set to 1 - }, - postHook: { - chainType: ChainType.EVM, - fundAmount: '1', - fundToken: destinyChainMANA, - calls: [ - // =================================== - // Approve MANA to be spent by Decentraland contract - // =================================== - { - chainType: ChainType.EVM, - callType: SquidCallType.FULL_TOKEN_BALANCE, - target: destinyChainMANA, - value: '0', - callData: ERC20ContractInterface.encodeFunctionData('approve', [ - getContract(ContractName.MarketplaceV2, toChain).address, - toAmount - ]), - payload: { - tokenAddress: destinyChainMarketplaceV2, - inputPos: 1 - }, - estimatedGas: '50000' - }, - // =================================== - // EXECUTE ORDER - // =================================== - { - chainType: ChainType.EVM, - callType: SquidCallType.DEFAULT, - target: destinyChainMANA, - value: '0', - callData: marketplaceInterface.encodeFunctionData('executeOrder', [ - collectionAddress, - tokenId, - price - ]), - - payload: { - tokenAddress: '0x', // TODO: what's this? - inputPos: 0 - }, - estimatedGas: '300000' - }, - // =================================== - // Transfer NFT to buyer - // =================================== - { - chainType: ChainType.EVM, - callType: SquidCallType.DEFAULT, - target: collectionAddress, - value: '0', - callData: ERC721ContractInterface.encodeFunctionData( - 'safeTransferFrom(address, address, uint256)', - [this.squidMulticall, fromAddress, tokenId] - ), - payload: { - tokenAddress: '0x', - inputPos: 1 - }, - estimatedGas: '50000' - }, - // =================================== - // Transfer remaining MANA to buyer - // =================================== - { - chainType: ChainType.EVM, - callType: SquidCallType.FULL_TOKEN_BALANCE, - target: destinyChainMANA, - value: '0', - callData: ERC20ContractInterface.encodeFunctionData('transfer', [ - fromAddress, - '0' - ]), - payload: { - tokenAddress: destinyChainMANA, - // This will replace the parameter at index 1 in the encoded Function, - // with FULL_TOKEN_BALANCE (instead of "0") - inputPos: 1 - }, - estimatedGas: '50000' - } - ] - } - }) - } - - // MINT - async mintNFT( - provider: Provider, - mintNFTXChainData: MintNFTXChainData - ): Promise { - const route = await this.getMintNFTRoute(mintNFTXChainData) - const tx = await this.executeRoute(route, provider) - return tx.transactionHash - } - - async getMintNFTRoute( - buyNFTXChainData: MintNFTXChainData - ): Promise { - await this.init() - const { - fromAddress, - fromAmount, - fromChain, - fromToken, - toChain, - toAmount, // the item price - enableExpress = true, - slippage = 1, - item: { collectionAddress, price, itemId } - } = buyNFTXChainData - - const ERC20ContractInterface = new ethers.utils.Interface(ERC20) - const collectionStoreInterface = new ethers.utils.Interface(CollectionStore) - - const destinyChainMANA = getContract(ContractName.MANAToken, toChain) - .address - const destinyChaiCollectionStoreAddress = getContract( - ContractName.CollectionStore, - toChain - ).address - - return this.squid.getRoute({ - fromAddress, - fromAmount, - fromToken, - fromChain: fromChain.toString(), - toToken: destinyChainMANA, - toChain: toChain.toString(), - toAddress: destinyChaiCollectionStoreAddress, - enableBoost: enableExpress, // TODO: check if we need this - slippageConfig: { - autoMode: 1 // 1 is "normal" slippage. Always set to 1 - }, - postHook: { - chainType: ChainType.EVM, - fundAmount: '1', - fundToken: destinyChainMANA, - calls: [ - // =================================== - // Approve MANA to be spent by Decentraland contract - // =================================== - { - chainType: ChainType.EVM, - callType: SquidCallType.FULL_TOKEN_BALANCE, - target: destinyChainMANA, - value: '0', - callData: ERC20ContractInterface.encodeFunctionData('approve', [ - getContract(ContractName.CollectionStore, toChain).address, - toAmount - ]), - payload: { - tokenAddress: destinyChaiCollectionStoreAddress, - inputPos: 1 - }, - estimatedGas: '50000' - }, - // =================================== - // BUY ITEM - // =================================== - { - chainType: ChainType.EVM, - callType: SquidCallType.DEFAULT, - target: destinyChainMANA, - value: '0', // @TODO: WHY 0? - callData: collectionStoreInterface.encodeFunctionData( - 'buy((address,uint256[],uint256[],address[])[])', - [[[collectionAddress, [itemId], [price], [fromAddress]]]] - ), - - payload: { - tokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // TODO: do we need this to be set as the native? it's working like this - inputPos: 0 - }, - estimatedGas: '300000' // TODO: where do we get this value from? - }, - // =================================== - // Transfer remaining MANA to buyer - // =================================== - { - chainType: ChainType.EVM, - callType: SquidCallType.FULL_TOKEN_BALANCE, - target: destinyChainMANA, - value: '0', - callData: ERC20ContractInterface.encodeFunctionData('transfer', [ - fromAddress, - '0' - ]), - payload: { - tokenAddress: destinyChainMANA, - // This will replace the parameter at index 1 in the encoded Function, - // with FULL_TOKEN_BALANCE (instead of "0") - inputPos: 1 - }, - estimatedGas: '50000' - } - ] - } - }) - } -} - -export const axelarProvider = new AxelarProvider() +export const crossChainProvider = new AxelarProvider() diff --git a/webapp/src/modules/features/selectors.spec.ts b/webapp/src/modules/features/selectors.spec.ts index d07f7691f9..68ddf22715 100644 --- a/webapp/src/modules/features/selectors.spec.ts +++ b/webapp/src/modules/features/selectors.spec.ts @@ -24,7 +24,8 @@ import { getIsRentalPriceFilterChartEnabled, getIsSmartWearablesFTUEnabled, isLoadingFeatureFlags, - getIsNewNavbarDropdownEnabled + getIsNewNavbarDropdownEnabled, + getIsBuyCrossChainEnabled } from './selectors' import { FeatureName } from './types' @@ -215,6 +216,11 @@ const waitForInitialLoadingSelectors = [ name: 'navbar-dropdown', feature: FeatureName.NEW_NAVBAR_DROPDOWN, selector: getIsNewNavbarDropdownEnabled + }, + { + name: 'buy-crosschain', + feature: FeatureName.BUY_CROSS_CHAIN, + selector: getIsBuyCrossChainEnabled } ] diff --git a/webapp/src/modules/features/selectors.ts b/webapp/src/modules/features/selectors.ts index c61ce44e67..bdc224d60c 100644 --- a/webapp/src/modules/features/selectors.ts +++ b/webapp/src/modules/features/selectors.ts @@ -206,3 +206,14 @@ export const getIsNewNavbarDropdownEnabled = (state: RootState) => { } return false } + +export const getIsBuyCrossChainEnabled = (state: RootState) => { + if (hasLoadedInitialFlags(state)) { + return getIsFeatureEnabled( + state, + ApplicationName.MARKETPLACE, + FeatureName.BUY_CROSS_CHAIN + ) + } + return false +} diff --git a/webapp/src/modules/features/types.ts b/webapp/src/modules/features/types.ts index d1c34cdf67..9bc15ddca5 100644 --- a/webapp/src/modules/features/types.ts +++ b/webapp/src/modules/features/types.ts @@ -17,5 +17,6 @@ export enum FeatureName { SMART_WEARABLES_FTU = 'smart-wearables-ftu', EMOTES_V2 = 'emotes-2.0', EMOTES_V2_FTU = 'emotes-2.0-ftu', - NEW_NAVBAR_DROPDOWN = 'new-navbar-dropdown' + NEW_NAVBAR_DROPDOWN = 'new-navbar-dropdown', + BUY_CROSS_CHAIN = 'buy-cross-chain' } diff --git a/webapp/src/modules/item/actions.ts b/webapp/src/modules/item/actions.ts index 0ab48c7ab5..6c7f107bd3 100644 --- a/webapp/src/modules/item/actions.ts +++ b/webapp/src/modules/item/actions.ts @@ -6,6 +6,7 @@ import { } from 'decentraland-dapps/dist/modules/transaction/utils' import { action } from 'typesafe-actions' import { formatWeiMANA } from '../../lib/mana' +import { Route } from '../../lib/xchain' import { getAssetName } from '../asset/utils' import { ItemBrowseOptions } from './types' @@ -111,6 +112,44 @@ export type BuyItemRequestAction = ReturnType export type BuyItemSuccessAction = ReturnType export type BuyItemFailureAction = ReturnType +// Buy Item Cross Chain +export const BUY_ITEM_CROSS_CHAIN_REQUEST = '[Request] Buy item Cross chain' +export const BUY_ITEM_CROSS_CHAIN_SUCCESS = '[Success] Buy item Cross chain' +export const BUY_ITEM_CROSS_CHAIN_FAILURE = '[Failure] Buy item Cross chain' + +export const buyItemCrossChainRequest = (item: Item, route: Route) => + action(BUY_ITEM_CROSS_CHAIN_REQUEST, { item, route }) + +export const buyItemCrossChainSuccess = ( + chainId: ChainId, + txHash: string, + item: Item +) => + action(BUY_ITEM_CROSS_CHAIN_SUCCESS, { + item, + txHash, + ...buildTransactionWithReceiptPayload(chainId, txHash, { + itemId: item.itemId, + contractAddress: item.contractAddress, + network: item.network, + name: getAssetName(item), + price: formatWeiMANA(item.price) + }) + }) + +export const buyItemCrossChainFailure = (error: string) => + action(BUY_ITEM_CROSS_CHAIN_FAILURE, { error }) + +export type BuyItemCrossChainRequestAction = ReturnType< + typeof buyItemCrossChainRequest +> +export type BuyItemCrossChainSuccessAction = ReturnType< + typeof buyItemCrossChainSuccess +> +export type BuyItemCrossChainFailureAction = ReturnType< + typeof buyItemCrossChainFailure +> + // Buy Item With Card export const BUY_ITEM_WITH_CARD_REQUEST = '[Request] Buy Item with Card' export const BUY_ITEM_WITH_CARD_SUCCESS = '[Success] Buy Item with Card' diff --git a/webapp/src/modules/item/sagas.ts b/webapp/src/modules/item/sagas.ts index 8e938152e1..931ae6c878 100644 --- a/webapp/src/modules/item/sagas.ts +++ b/webapp/src/modules/item/sagas.ts @@ -1,7 +1,6 @@ import { matchPath } from 'react-router-dom' import { getLocation } from 'connected-react-router' import { SagaIterator } from 'redux-saga' -import { Item } from '@dcl/schemas' import { put, takeEvery } from '@redux-saga/core/effects' import { call, @@ -12,7 +11,11 @@ import { select, take } from 'redux-saga/effects' +import { ethers } from 'ethers' +import { Item } from '@dcl/schemas' +import { getConnectedProvider } from 'decentraland-dapps/dist/lib/eth' import { ContractName, getContract } from 'decentraland-transactions' +import { Provider } from 'decentraland-connect' import { AuthIdentity } from 'decentraland-crypto-fetch' import { sendTransaction } from 'decentraland-dapps/dist/modules/wallet/utils' import { t } from 'decentraland-dapps/dist/modules/translation/utils' @@ -23,6 +26,7 @@ import { import { isNFTPurchase } from 'decentraland-dapps/dist/modules/gateway/utils' import { PurchaseStatus } from 'decentraland-dapps/dist/modules/gateway/types' import { isErrorWithMessage } from '../../lib/error' +import { crossChainProvider } from '../../lib/xchain' import { config } from '../../config' import { ItemAPI } from '../vendor/decentraland/item/api' import { getWallet } from '../wallet/selectors' @@ -63,7 +67,9 @@ import { fetchCollectionItemsSuccess, fetchCollectionItemsFailure, FETCH_COLLECTION_ITEMS_REQUEST, - FETCH_ITEMS_CANCELLED_ERROR_MESSAGE + FETCH_ITEMS_CANCELLED_ERROR_MESSAGE, + BuyItemCrossChainRequestAction, + BUY_ITEM_CROSS_CHAIN_REQUEST } from './actions' import { getData as getItems } from './selectors' import { getItem } from './utils' @@ -87,6 +93,7 @@ export function* itemSaga(getIdentity: () => AuthIdentity | undefined) { ) yield takeEvery(FETCH_TRENDING_ITEMS_REQUEST, handleFetchTrendingItemsRequest) yield takeEvery(BUY_ITEM_REQUEST, handleBuyItem) + yield takeEvery(BUY_ITEM_CROSS_CHAIN_REQUEST, handleBuyItemCrossChain) yield takeEvery(BUY_ITEM_WITH_CARD_REQUEST, handleBuyItemWithCardRequest) yield takeEvery(SET_PURCHASE, handleSetItemPurchaseWithCard) yield takeEvery(FETCH_ITEM_REQUEST, handleFetchItemRequest) @@ -257,6 +264,42 @@ export function* itemSaga(getIdentity: () => AuthIdentity | undefined) { } } + function* handleBuyItemCrossChain(action: BuyItemCrossChainRequestAction) { + try { + const { item, route } = action.payload + + const wallet: ReturnType = yield select(getWallet) + + const provider: Provider | null = yield call(getConnectedProvider) + + if (!wallet) { + throw new Error('A defined wallet is required to buy an item') + } + + if (provider) { + const signer = new ethers.providers.Web3Provider(provider).getSigner() + + const txRespose: ethers.providers.TransactionResponse = yield call( + [crossChainProvider.squid, 'executeRoute'], + { route: route.route, signer } + ) + + const tx: ethers.providers.TransactionReceipt = yield call( + txRespose.wait + ) + console.log('tx: ', tx) + yield put(buyItemSuccess(item.chainId, tx.transactionHash, item)) + } + } catch (error) { + console.log('error: ', error) + yield put( + buyItemFailure( + isErrorWithMessage(error) ? error.message : t('global.unknown_error') + ) + ) + } + } + function* handleBuyItemWithCardRequest(action: BuyItemWithCardRequestAction) { try { const { item } = action.payload