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 ;
+ }}
+ />
+
+
+
+ ) : null;
+};
+
+const useShowCart = () => {
+ const selected = useSelector(selectSelectedNFTIds);
+ return useSelector(selectShowCart) && selected.length > 0;
+};
diff --git a/packages/screens/BurnCapital/components/BurnableNFTs.tsx b/packages/screens/BurnCapital/components/BurnableNFTs.tsx
new file mode 100644
index 0000000000..db1b5f47fe
--- /dev/null
+++ b/packages/screens/BurnCapital/components/BurnableNFTs.tsx
@@ -0,0 +1,98 @@
+import React from "react";
+import { View, ViewStyle, StyleProp } from "react-native";
+
+import { Sort, SortDirection } from "@/api/marketplace/v1/marketplace";
+import { EmptyList } from "@/components/EmptyList";
+import { NetworkIcon } from "@/components/NetworkIcon";
+import { Section } from "@/components/Section";
+import { NFTView } from "@/components/nfts/NFTView";
+import { useCollectionInfo } from "@/hooks/useCollectionInfo";
+import { useNFTs } from "@/hooks/useNFTs";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import {
+ NetworkFeature,
+ getNetworkFeature,
+ parseCollectionId,
+} from "@/networks";
+import { layout } from "@/utils/style/layout";
+
+const gridHalfGutter = 12;
+
+export const BurnableNFTs: React.FC<{
+ ownerId: string;
+ style?: StyleProp;
+}> = ({ ownerId, style }) => {
+ const selectedNetworkId = useSelectedNetworkId();
+ const burnFeature = getNetworkFeature(
+ selectedNetworkId,
+ NetworkFeature.CosmWasmNFTsBurner,
+ );
+
+ return (
+
+ {burnFeature?.authorizedCollections.length ? (
+ burnFeature.authorizedCollections.map((collectionId) => (
+
+ ))
+ ) : (
+
+ )}
+
+ );
+};
+
+const OwnedNFTsSection: React.FC<{
+ ownerId: string;
+ collectionId: string;
+}> = ({ ownerId, collectionId }) => {
+ const [network] = parseCollectionId(collectionId);
+
+ const { nfts } = useNFTs({
+ offset: 0,
+ limit: 100, // FIXME: pagination
+ ownerId,
+ collectionId,
+ sort: Sort.SORT_PRICE,
+ sortDirection: SortDirection.SORT_DIRECTION_ASCENDING,
+ attributes: [],
+ isListed: false,
+ priceRange: undefined,
+ });
+
+ const { collectionInfo } = useCollectionInfo(
+ collectionId,
+ undefined,
+ !!nfts.length,
+ );
+
+ if (nfts.length === 0 || !collectionInfo?.name) {
+ return null;
+ }
+
+ return (
+
+ }
+ >
+
+ {nfts.map((nft) => (
+
+ ))}
+
+
+ );
+};
diff --git a/packages/screens/BurnCapital/components/TopSectionConnectWallet.tsx b/packages/screens/BurnCapital/components/TopSectionConnectWallet.tsx
new file mode 100644
index 0000000000..2b4bdf7d15
--- /dev/null
+++ b/packages/screens/BurnCapital/components/TopSectionConnectWallet.tsx
@@ -0,0 +1,147 @@
+import React from "react";
+import { View } from "react-native";
+
+import dappCardSVG from "@/assets/cards/dapp-card.svg";
+import iconSVG from "@/assets/icons/fire.svg";
+import { BrandText } from "@/components/BrandText";
+import { SVG } from "@/components/SVG";
+import { MainConnectWalletButton } from "@/components/connectWallet/MainConnectWalletButton";
+import { UserAvatarWithFrame } from "@/components/images/AvatarWithFrame";
+import { shortUserAddressFromID } from "@/components/nfts/NFTView";
+import { useNSUserInfo } from "@/hooks/useNSUserInfo";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { fontBold16 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+const gridHalfGutter = 12;
+
+export const TopSectionConnectWallet: React.FC