diff --git a/assets/icons/fire.svg b/assets/icons/fire.svg new file mode 100644 index 0000000000..fd4c1d650a --- /dev/null +++ b/assets/icons/fire.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/networks.json b/networks.json index d4b947fe12..20e7a3109b 100644 --- a/networks.json +++ b/networks.json @@ -10894,7 +10894,11 @@ "featureObjects": [ { "type": "CosmWasmNFTsBurner", - "burnerContractAddress": "tori16tlfw7uq73d5n8j5tl0zl367c58f032j50jgxr3e7f09gez3xq5qvcrxy7" + "burnerContractAddress": "tori16tlfw7uq73d5n8j5tl0zl367c58f032j50jgxr3e7f09gez3xq5qvcrxy7", + "authorizedCollections": [ + "tori-tori1upd858fjdlme0wv4vd2v7c4majyr9dg53rl72dzfhds9zusmn9mqzjk22e", + "tori-tori1quj5act407qgszngzsh9elcelzl9pgcglq3844cwqex3cxzzeres0ckprs" + ] } ], "registryName": "teritori", @@ -11075,7 +11079,10 @@ }, { "type": "CosmWasmNFTsBurner", - "burnerContractAddress": "tori1qyl0j7a24amk8k8gcmvv07y2zjx7nkcwpk73js24euh64hkja6esd2p2xp" + "burnerContractAddress": "tori1qyl0j7a24amk8k8gcmvv07y2zjx7nkcwpk73js24euh64hkja6esd2p2xp", + "authorizedCollections": [ + "testori-tori1r8raaqul4j05qtn0t05603mgquxfl8e9p7kcf7smwzcv2hc5rrlq0vket0" + ] } ], "currencies": [ diff --git a/packages/components/connectWallet/MainConnectWalletButton.tsx b/packages/components/connectWallet/MainConnectWalletButton.tsx index d5856ef99d..d07c668241 100644 --- a/packages/components/connectWallet/MainConnectWalletButton.tsx +++ b/packages/components/connectWallet/MainConnectWalletButton.tsx @@ -16,6 +16,7 @@ export const MainConnectWalletButton: React.FC<{ setIsConnectWalletVisible(true)} /> diff --git a/packages/components/nfts/NFTView.tsx b/packages/components/nfts/NFTView.tsx index 91ec93c1e8..a1bf34227e 100644 --- a/packages/components/nfts/NFTView.tsx +++ b/packages/components/nfts/NFTView.tsx @@ -50,6 +50,7 @@ import { SecondaryButton } from "../buttons/SecondaryButton"; import { UserAvatarWithFrame } from "../images/AvatarWithFrame"; import { SpacerColumn, SpacerRow } from "../spacer"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; import { useFeedbacks } from "@/context/FeedbacksProvider"; import { TeritoriNftClient } from "@/contracts-clients/teritori-nft/TeritoriNft.client"; import { popularCollectionsQueryKey } from "@/hooks/marketplace/usePopularCollections"; @@ -59,6 +60,12 @@ import { nftBurnerUserCountQueryKey } from "@/hooks/nft-burner/useNFTBurnerUserC import { collectionStatsQueryKey } from "@/hooks/useCollectionStats"; import { nftsQueryKey } from "@/hooks/useNFTs"; import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; +import { + addSelectedToBurn, + setShowBurnCart, + selectSelectedNFTIds as selectSelectedNFTIdsForBurn, + removeSelectedFromBurn, +} from "@/store/slices/burnCartItems"; // NOTE: we put content in a memoized component to only rerender the container when the window width changes @@ -434,6 +441,23 @@ const NFTViewFooter: React.FC<{ nft: NFT; localSelected: boolean }> = memo( ({ nft, localSelected }) => { const selectedWallet = useSelectedWallet(); const isOwner = nft.ownerId === selectedWallet?.userId; + const dispatch = useAppDispatch(); + + const burnerFeature = getNetworkFeature( + nft.networkId, + NetworkFeature.CosmWasmNFTsBurner, + ); + const { data: authorizedCollections } = useNFTBurnerAuthorizedCollections( + nft.networkId, + ); + + const showRecycle = + !!burnerFeature && + (authorizedCollections || []).includes(nft.nftContractAddress); + + const selectedForBurn = useSelector(selectSelectedNFTIdsForBurn).includes( + nft.id, + ); return ( = memo( ) : ( - - Not listed - + <> + {showRecycle ? ( + { + dispatch(setShowBurnCart(true)); + if (!selectedForBurn) { + dispatch(addSelectedToBurn(nft)); + } else { + dispatch(removeSelectedFromBurn(nft.id)); + } + }} + text={selectedForBurn ? "Added to the burn 🔥" : "Burnable"} + /> + ) : ( + + Not listed + + )} + )} diff --git a/packages/hooks/useCollectionInfo.ts b/packages/hooks/useCollectionInfo.ts index 4d132c4179..2328faac42 100644 --- a/packages/hooks/useCollectionInfo.ts +++ b/packages/hooks/useCollectionInfo.ts @@ -35,6 +35,7 @@ import { isLinkBanned } from "@/utils/link-ban"; export const useCollectionInfo = ( id: string, forceInterval?: number, + enabled: boolean = true, ): { collectionInfo: CollectionInfo; notFound: boolean; diff --git a/packages/hooks/useCollectionStats.ts b/packages/hooks/useCollectionStats.ts index 7b91dc4241..f2438eee19 100644 --- a/packages/hooks/useCollectionStats.ts +++ b/packages/hooks/useCollectionStats.ts @@ -5,11 +5,14 @@ import { parseNetworkObjectId, NetworkKind } from "@/networks"; import { getMarketplaceClient } from "@/utils/backend"; export const collectionStatsQueryKey = ( - collectionId: string, + collectionId?: string, ownerId?: string, ) => { - const qk = ["collectionStats", collectionId]; - if (ownerId) { + const qk = ["collectionStats"]; + if (collectionId) { + qk.push(collectionId); + } + if (collectionId && ownerId) { qk.push(ownerId); } return qk; diff --git a/packages/networks/features.ts b/packages/networks/features.ts index 7e718ba128..939e362a69 100644 --- a/packages/networks/features.ts +++ b/packages/networks/features.ts @@ -32,6 +32,7 @@ export type CosmWasmPremiumFeed = z.infer; const zodCosmWasmNFTsBurner = z.object({ type: z.literal(NetworkFeature.CosmWasmNFTsBurner), burnerContractAddress: z.string(), + authorizedCollections: z.array(z.string()), // FIXME: use nft contract address instead of minter contract address in ids }); export type CosmWasmNFTsBurner = z.infer; diff --git a/packages/networks/teritori-testnet/index.ts b/packages/networks/teritori-testnet/index.ts index 47833a9e20..054c1a0269 100644 --- a/packages/networks/teritori-testnet/index.ts +++ b/packages/networks/teritori-testnet/index.ts @@ -19,6 +19,9 @@ const nftsBurnerFeature: CosmWasmNFTsBurner = { type: NetworkFeature.CosmWasmNFTsBurner, burnerContractAddress: "tori1qyl0j7a24amk8k8gcmvv07y2zjx7nkcwpk73js24euh64hkja6esd2p2xp", + authorizedCollections: [ + "testori-tori1r8raaqul4j05qtn0t05603mgquxfl8e9p7kcf7smwzcv2hc5rrlq0vket0", + ], }; const riotContractAddressGen0 = diff --git a/packages/networks/teritori/index.ts b/packages/networks/teritori/index.ts index 753ef34a78..8c44d49136 100644 --- a/packages/networks/teritori/index.ts +++ b/packages/networks/teritori/index.ts @@ -11,6 +11,10 @@ const burnCapitalFeature: CosmWasmNFTsBurner = { type: NetworkFeature.CosmWasmNFTsBurner, burnerContractAddress: "tori16tlfw7uq73d5n8j5tl0zl367c58f032j50jgxr3e7f09gez3xq5qvcrxy7", + authorizedCollections: [ + "tori-tori1upd858fjdlme0wv4vd2v7c4majyr9dg53rl72dzfhds9zusmn9mqzjk22e", // Sasquatch Society Farmers + "tori-tori1quj5act407qgszngzsh9elcelzl9pgcglq3844cwqex3cxzzeres0ckprs", // ToriWadz + ], }; export const teritoriNetwork: CosmosNetworkInfo = { diff --git a/packages/screens/BurnCapital/BurnCapitalScreen.tsx b/packages/screens/BurnCapital/BurnCapitalScreen.tsx index 05deaa8487..71a45fcbbc 100644 --- a/packages/screens/BurnCapital/BurnCapitalScreen.tsx +++ b/packages/screens/BurnCapital/BurnCapitalScreen.tsx @@ -1,27 +1,101 @@ +import React, { useEffect } from "react"; +import { View } from "react-native"; +import { useDispatch } from "react-redux"; + import { BrandText } from "@/components/BrandText"; import { ScreenContainer } from "@/components/ScreenContainer"; import { useNFTBurnerTotal } from "@/hooks/nft-burner/useNFTBurnerTotal"; import { useNFTBurnerUserCount } from "@/hooks/nft-burner/useNFTBurnerUserCount"; +import { useIsMobile } from "@/hooks/useIsMobile"; import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; import useSelectedWallet from "@/hooks/useSelectedWallet"; -import { teritoriNetwork } from "@/networks/teritori"; -import { ScreenFC } from "@/utils/navigation"; +import { NetworkFeature, getNetwork } from "@/networks"; +import { BurnSideCart } from "@/screens/BurnCapital/components/BurnSideCart"; +import { BurnableNFTs } from "@/screens/BurnCapital/components/BurnableNFTs"; +import { TopSectionConnectWallet } from "@/screens/BurnCapital/components/TopSectionConnectWallet"; +import { setSelectedNetworkId } from "@/store/slices/settings"; +import { ScreenFC, useAppNavigation } from "@/utils/navigation"; +import { neutral00, neutral33 } from "@/utils/style/colors"; +import { fontSemibold20 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; export const BurnCapitalScreen: ScreenFC<"BurnCapital"> = ({ route }) => { - const inputNetwork = route.params?.network || teritoriNetwork.id; + const inputNetwork = route.params?.network; const selectedWallet = useSelectedWallet(); const selectedNetworkId = useSelectedNetworkId(); + const dispatch = useDispatch(); + const navigation = useAppNavigation(); + const isMobile = useIsMobile(); + + useEffect(() => { + if (!inputNetwork) { + return; + } + const network = getNetwork(inputNetwork); + if (!network) { + return; + } + dispatch(setSelectedNetworkId(network.id)); + }, [dispatch, inputNetwork]); + const { data: count } = useNFTBurnerUserCount(selectedWallet?.userId); const { data: total } = useNFTBurnerTotal(selectedNetworkId); return ( - - Burn Capital - {typeof total === "number" && ( - Total burned: {total} - )} - {typeof count === "number" && ( - Burned by you: {count} - )} + } + fullWidth + forceNetworkFeatures={[NetworkFeature.CosmWasmNFTsBurner]} + headerChildren={ + BURN Capital 🔥 + } + responsive + onBackPress={() => navigation.navigate("Marketplace")} + > + + + + {typeof total === "number" && ( + Total burned: {total} + )} + {typeof count === "number" && ( + Burned by you: {count} + )} + + + + + ); }; diff --git a/packages/screens/BurnCapital/components/BurnSideCart.tsx b/packages/screens/BurnCapital/components/BurnSideCart.tsx new file mode 100644 index 0000000000..8b7ff3e1c9 --- /dev/null +++ b/packages/screens/BurnCapital/components/BurnSideCart.tsx @@ -0,0 +1,408 @@ +import { MsgExecuteContractEncodeObject } from "@cosmjs/cosmwasm-stargate"; +import { toUtf8 } from "@cosmjs/encoding"; +import { EncodeObject } from "@cosmjs/proto-signing"; +import { isDeliverTxFailure } from "@cosmjs/stargate"; +import { EntityId } from "@reduxjs/toolkit"; +import { useQueryClient } from "@tanstack/react-query"; +import React, { useCallback } from "react"; +import { + FlatList, + Linking, + Pressable, + StyleProp, + View, + ViewStyle, +} from "react-native"; +import { TouchableOpacity } from "react-native-gesture-handler"; +import { TrashIcon } from "react-native-heroicons/outline"; +import { useSelector } from "react-redux"; + +import closeSVG from "@/assets/icons/close.svg"; +import { BrandText } from "@/components/BrandText"; +import { OptimizedImage } from "@/components/OptimizedImage"; +import { SVG } from "@/components/SVG"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { Separator } from "@/components/separators/Separator"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { Wallet } from "@/context/WalletsProvider"; +import { ExecuteMsg } from "@/contracts-clients/teritori-nft/TeritoriNft.types"; +import { popularCollectionsQueryKey } from "@/hooks/marketplace/usePopularCollections"; +import { nftBurnerTotalQueryKey } from "@/hooks/nft-burner/useNFTBurnerTotal"; +import { nftBurnerUserCountQueryKey } from "@/hooks/nft-burner/useNFTBurnerUserCount"; +import { collectionStatsQueryKey } from "@/hooks/useCollectionStats"; +import { nftsQueryKey } from "@/hooks/useNFTs"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { + getNetworkFeature, + NetworkFeature, + parseNftId, + txExplorerLink, +} from "@/networks"; +import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; +import { + emptyBurnCart, + removeSelectedFromBurn, + selectAllSelectedNFTData, + selectSelectedNFTDataById, + selectSelectedNFTIds, + selectShowCart, + setShowBurnCart, +} from "@/store/slices/burnCartItems"; +import { RootState, useAppDispatch } from "@/store/store"; +import { + codGrayColor, + errorColor, + neutral44, + neutral77, + neutralA3, +} from "@/utils/style/colors"; +import { + fontMedium10, + fontSemibold12, + fontSemibold14, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { modalMarginPadding } from "@/utils/style/modals"; + +const Header: React.FC<{ + items: any[]; + onPressClear: () => void; + onPressHide: () => void; +}> = ({ items, onPressClear, onPressHide }) => { + return ( + + + Burn + + {items.length} + + + + + Clear + + + + + + ); +}; + +const CartItems: React.FC<{ id: EntityId }> = ({ id }) => { + const nft = useSelector((state: RootState) => + selectSelectedNFTDataById(state, id), + ); + + const dispatch = useAppDispatch(); + return nft ? ( + + + + + {nft?.name} + { + dispatch(removeSelectedFromBurn(id)); + }} + > + + + + + + ) : null; +}; + +const ItemTotal: React.FC<{ + textLeft: string; + networkId: string; + textRight: string | number; +}> = ({ textLeft, textRight, networkId }) => { + return ( + + + {textLeft} + + + + {typeof textRight === "number" ? textRight.toFixed(0) : textRight} + + + + ); +}; + +const Footer: React.FC = () => { + const wallet = useSelectedWallet(); + const dispatch = useAppDispatch(); + const selectedNFTData = useSelector(selectAllSelectedNFTData); + const { setToast, setLoadingFullScreen } = useFeedbacks(); + const queryClient = useQueryClient(); + const selectedNetworkId = useSelectedNetworkId(); + + const cosmosMultiBurn = useCallback( + async (wallet: Wallet) => { + setLoadingFullScreen(true); + try { + const sender = wallet.address; + if (!sender) { + throw Error("invalid wallet"); + } + + const burnerFeature = getNetworkFeature( + selectedNetworkId, + NetworkFeature.CosmWasmNFTsBurner, + ); + + if (!burnerFeature) { + throw new Error("invalid network"); + } + + const msgs: EncodeObject[] = []; + + selectedNFTData.map((nft) => { + const [, , tokenId] = parseNftId(nft.id); + + const payload: ExecuteMsg = { + send_nft: { + contract: burnerFeature.burnerContractAddress, + token_id: tokenId, + msg: "", + }, + }; + + const msg: MsgExecuteContractEncodeObject = { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: { + sender, + msg: toUtf8(JSON.stringify(payload)), + contract: nft.nftContractAddress, + funds: [], + }, + }; + + msgs.push(msg); + }); + + if (!msgs.length) { + return; + } + + const cosmwasmClient = + await getKeplrSigningCosmWasmClient(selectedNetworkId); + + const tx = await cosmwasmClient.signAndBroadcast(sender, msgs, "auto"); + if (isDeliverTxFailure(tx)) { + throw new Error(tx.transactionHash); + } + + setToast({ + title: `Burned ${msgs.length} NFTs!`, + message: tx.transactionHash, + mode: "normal", + duration: 10000, + onPress: () => { + Linking.openURL( + txExplorerLink(selectedNetworkId, tx.transactionHash), + ); + }, + }); + + dispatch(emptyBurnCart()); //remove items from cart + + await Promise.all([ + queryClient.invalidateQueries(nftsQueryKey()), + queryClient.invalidateQueries( + nftBurnerTotalQueryKey(selectedNetworkId), + ), + queryClient.invalidateQueries( + nftBurnerUserCountQueryKey(wallet.userId), + ), + queryClient.invalidateQueries(collectionStatsQueryKey()), + queryClient.invalidateQueries( + popularCollectionsQueryKey(selectedNetworkId), + ), + ]); + } catch (e: any) { + setToast({ + title: "Error", + message: `${e instanceof Error ? e.message : e}`, + duration: 30000, + mode: "normal", + type: "error", + }); + } finally { + setLoadingFullScreen(false); + } + }, + [ + dispatch, + queryClient, + selectedNFTData, + selectedNetworkId, + setLoadingFullScreen, + setToast, + ], + ); + + const onBuyButtonPress = async () => { + if (!wallet) { + setToast({ + title: `Wallet is not connected`, + duration: 5000, + mode: "normal", + type: "error", + }); + return; + } + await cosmosMultiBurn(wallet); + }; + + return ( + + + + + + + onBuyButtonPress()} + /> + + + ); +}; + +export const BurnSideCart: React.FC<{ style?: StyleProp }> = ({ + style, +}) => { + const dispatch = useAppDispatch(); + const selected = useSelector(selectSelectedNFTIds); + const handleEmptyCart = () => { + dispatch(emptyBurnCart()); + }; + const handleHideCart = () => { + dispatch(setShowBurnCart(false)); + }; + + return useShowCart() ? ( + +
handleEmptyCart()} + onPressHide={() => handleHideCart()} + /> + { + return ; + }} + /> + +