+
From wallet
-
- {shortAddressByChain[sourceChain]}
+
+ {shortAddressByChain[sourceChain] ?? "Not connected"}
To wallet
-
- {shortAddressByChain[targetChain]}
+
+ {shortAddressByChain[targetChain] ?? "Not connected"}
diff --git a/apps/web/src/app/(routes)/bridge/_components/TransferStarknetNftsAction.tsx b/apps/web/src/app/(routes)/bridge/_components/TransferStarknetNftsAction.tsx
new file mode 100644
index 00000000..01ca4bc2
--- /dev/null
+++ b/apps/web/src/app/(routes)/bridge/_components/TransferStarknetNftsAction.tsx
@@ -0,0 +1,38 @@
+import clsx from "clsx";
+import { Button, Typography } from "design-system";
+
+import useNftSelection from "../_hooks/useNftSelection";
+import useTransferStarknetNfts from "../_hooks/useTransferStarknetNfts";
+
+export default function TransferStarknetNftsAction() {
+ const { totalSelectedNfts } = useNftSelection();
+ const { depositTokens, isSigning } = useTransferStarknetNfts();
+
+ const disabled = totalSelectedNfts === 0 || isSigning;
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useEthereumCollectionApproval.tsx b/apps/web/src/app/(routes)/bridge/_hooks/useEthereumCollectionApproval.tsx
new file mode 100644
index 00000000..5f4b5b22
--- /dev/null
+++ b/apps/web/src/app/(routes)/bridge/_hooks/useEthereumCollectionApproval.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { useEffect } from "react";
+import { erc721Abi } from "viem";
+import {
+ useBlockNumber,
+ useAccount as useEthereumAccount,
+ useReadContract,
+ useWaitForTransactionReceipt,
+ useWriteContract,
+} from "wagmi";
+
+import useNftSelection from "./useNftSelection";
+
+export default function useEthereumCollectionApproval() {
+ const { selectedCollectionAddress, totalSelectedNfts } = useNftSelection();
+
+ const { address: ethereumAddress } = useEthereumAccount();
+
+ const { data: isApprovedForAll, refetch } = useReadContract({
+ abi: erc721Abi,
+ address: selectedCollectionAddress as `0x${string}`,
+ args: [
+ ethereumAddress ?? "0xa",
+ process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`,
+ ],
+ functionName: "isApprovedForAll",
+ query: {
+ enabled: totalSelectedNfts > 0,
+ },
+ });
+
+ const {
+ data: approveHash,
+ isLoading: isSigning,
+ writeContract: writeContractApprove,
+ } = useWriteContract();
+
+ function approveForAll() {
+ writeContractApprove({
+ abi: erc721Abi,
+ address: selectedCollectionAddress as `0x${string}`,
+ args: [process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`, true],
+ functionName: "setApprovalForAll",
+ });
+ }
+
+ const { isLoading: isApproveLoading } = useWaitForTransactionReceipt({
+ hash: approveHash,
+ });
+
+ const { data: blockNumber } = useBlockNumber({ watch: true });
+
+ useEffect(() => {
+ void refetch();
+ }, [blockNumber, refetch]);
+
+ return {
+ approveForAll: () => approveForAll(),
+ isApproveLoading: isApproveLoading && approveHash !== undefined,
+ isApprovedForAll,
+ isSigning,
+ };
+}
diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useEthereumNftDeposit.tsx b/apps/web/src/app/(routes)/bridge/_hooks/useEthereumNftDeposit.tsx
new file mode 100644
index 00000000..f3df9bec
--- /dev/null
+++ b/apps/web/src/app/(routes)/bridge/_hooks/useEthereumNftDeposit.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { useAccount as useStarknetAccount } from "@starknet-react/core";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+import { parseGwei } from "viem";
+import { useWriteContract } from "wagmi";
+
+import useNftSelection from "./useNftSelection";
+
+export default function useEthereumNftDeposit() {
+ const { deselectAllNfts, selectedCollectionAddress, selectedTokenIds } =
+ useNftSelection();
+
+ const { address: starknetAddress } = useStarknetAccount();
+
+ const {
+ data: depositTransactionHash,
+ isLoading,
+ writeContract: writeContractDeposit,
+ } = useWriteContract();
+
+ const router = useRouter();
+
+ function depositTokens() {
+ writeContractDeposit({
+ abi: [
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "salt",
+ type: "uint256",
+ },
+ {
+ internalType: "address",
+ name: "collectionL1",
+ type: "address",
+ },
+ {
+ internalType: "snaddress",
+ name: "ownerL2",
+ type: "uint256",
+ },
+ {
+ internalType: "uint256[]",
+ name: "ids",
+ type: "uint256[]",
+ },
+ {
+ internalType: "bool",
+ name: "useAutoBurn",
+ type: "bool",
+ },
+ ],
+ name: "depositTokens",
+ outputs: [],
+ stateMutability: "payable",
+ type: "function",
+ },
+ ],
+ address: process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`,
+ args: [
+ // TODO @YohanTz: Get the proper request hash from ?
+ Date.now(),
+ selectedCollectionAddress as `0x${string}`,
+ starknetAddress,
+ selectedTokenIds,
+ false,
+ ],
+ functionName: "depositTokens",
+ // TODO @YohanTz: Get needed gas from ?
+ value: parseGwei("40000"),
+ });
+ }
+
+ useEffect(() => {
+ if (depositTransactionHash !== undefined) {
+ void router.push(`lounge/${depositTransactionHash}?from=ethereum`);
+ }
+ }, [depositTransactionHash, deselectAllNfts, router]);
+
+ return {
+ depositTokens: () => depositTokens(),
+ depositTransactionHash,
+ isSigning: isLoading && depositTransactionHash === undefined,
+ };
+}
diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useNftSelection.ts b/apps/web/src/app/(routes)/bridge/_hooks/useNftSelection.ts
index f6a25be0..e879ddb5 100644
--- a/apps/web/src/app/(routes)/bridge/_hooks/useNftSelection.ts
+++ b/apps/web/src/app/(routes)/bridge/_hooks/useNftSelection.ts
@@ -1,190 +1,168 @@
-import { useEffect, useState } from "react";
+import { useCallback, useMemo } from "react";
import { useLocalStorage } from "usehooks-ts";
import useAccountFromChain from "~/app/_hooks/useAccountFromChain";
import useCurrentChain from "~/app/_hooks/useCurrentChain";
-import { api } from "~/utils/api";
+import { type Nft } from "~/server/api/types";
+
+export const MAX_SELECTED_ITEMS = 100;
export default function useNftSelection() {
const { sourceChain } = useCurrentChain();
- const { address } = useAccountFromChain(sourceChain);
-
- const [selectedNftIdsByAddress, setSelectedNftIdsByAddress] = useLocalStorage<
- Record<`0x${string}`, Array
>
- >("selectedNftIdsByAddress", {});
-
- const [
- lastSelectedCollectionNameByAddress,
- setLastSelectedCollectionNameByAddress,
- ] = useLocalStorage>(
- "lastSelectedCollectionNameByAddress",
- {}
+ const { address: userAddress } = useAccountFromChain(sourceChain);
+
+ const [selectedTokensByUserAddress, setSelectedTokensByUserAddress] =
+ useLocalStorage<
+ Record<
+ `0x${string}`,
+ { collectionAddress: string; tokenIds: Array } | null
+ >
+ >("selectedTokensByUserAddress", {});
+
+ const { selectedCollectionAddress, selectedTokenIds } = useMemo(
+ () => ({
+ selectedCollectionAddress: userAddress
+ ? selectedTokensByUserAddress[userAddress]?.collectionAddress
+ : undefined,
+ selectedTokenIds: userAddress
+ ? selectedTokensByUserAddress[userAddress]?.tokenIds ?? []
+ : [],
+ }),
+ [selectedTokensByUserAddress, userAddress]
);
- // Change to use contract address
- const [selectedCollectionName, setSelectedCollectionName] = useState<
- null | string
- >(null);
-
- const { data: l1Nfts } = api.nfts.getL1NftsByCollection.useQuery(
- {
- address: address ?? "",
- },
- {
- enabled: address !== undefined && sourceChain === "Ethereum",
- }
+ const totalSelectedNfts = useMemo(
+ () => selectedTokenIds.length,
+ [selectedTokenIds]
);
- const { data: l2Nfts } = api.nfts.getL2NftsByCollection.useQuery(
- {
- address: address ?? "",
+
+ const isNftSelected = useCallback(
+ (tokenId: string, collectionAddress: string) => {
+ return (
+ selectedTokenIds.includes(tokenId) &&
+ collectionAddress === selectedCollectionAddress
+ );
},
- {
- enabled: address !== undefined && sourceChain === "Starknet",
- }
+ [selectedCollectionAddress, selectedTokenIds]
);
- const nfts = sourceChain === "Ethereum" ? l1Nfts : l2Nfts;
-
- const selectedCollection = selectedCollectionName
- ? nfts?.byCollection[selectedCollectionName] ?? []
- : [];
-
- /**
- * array.filter() is used because we need to clean the nft ids that are still in the local storage
- * but that are not in the user wallet anymore
- */
- const selectedNftIds = address
- ? selectedNftIdsByAddress[address]?.filter(
- (nftId) => nfts?.raw.find((rawNft) => rawNft.id === nftId) !== undefined
- ) ?? []
- : [];
-
- const numberOfSelectedNfts = selectedNftIds.length;
-
- const lastSelectedCollectionName =
- address && selectedNftIds.length > 0
- ? lastSelectedCollectionNameByAddress[address]
- : undefined;
-
- // TODO @YohanTz: Directly search in the collection
- const selectedNfts = selectedNftIds
- .map((selectedNftId) => nfts?.raw.find((nft) => nft.id === selectedNftId))
- .filter((nft) => nft !== undefined);
-
- const allCollectionSelected =
- selectedCollection.length === selectedNftIds.length;
-
- // @YohanTz: Refacto to remove the need of useEffect
- useEffect(() => {
- setSelectedCollectionName(null);
- }, [sourceChain]);
-
- function deselectNft(nftId: string) {
- if (address === undefined || !selectedNftIds.includes(nftId)) {
- return null;
- }
-
- if (selectedNftIds.length === 1) {
- setLastSelectedCollectionNameByAddress({
- ...lastSelectedCollectionNameByAddress,
- [address]: null,
- });
+ function selectNft(tokenId: string, collectionAddress: string) {
+ if (
+ isNftSelected(tokenId, collectionAddress) ||
+ userAddress === undefined
+ ) {
+ return;
}
- setSelectedNftIdsByAddress({
- ...selectedNftIdsByAddress,
- [address]: selectedNftIds.filter(
- (selectedNftId) => selectedNftId !== nftId
- ),
- });
- }
-
- function selectNft(nftId: string) {
- if (address === undefined) {
- return null;
+ if (
+ totalSelectedNfts === MAX_SELECTED_ITEMS &&
+ collectionAddress === selectedCollectionAddress
+ ) {
+ // TODO @YohanTz: Trigger toast here
+ return;
}
if (
- selectedCollectionName !== lastSelectedCollectionNameByAddress[address]
+ collectionAddress !==
+ selectedTokensByUserAddress[userAddress]?.collectionAddress
) {
- setSelectedNftIdsByAddress({
- ...selectedNftIdsByAddress,
- [address]: [nftId],
- });
- setLastSelectedCollectionNameByAddress({
- ...lastSelectedCollectionNameByAddress,
- [address]: selectedCollectionName,
- });
+ setSelectedTokensByUserAddress((previousValue) => ({
+ ...previousValue,
+ [userAddress]: { collectionAddress, tokenIds: [tokenId] },
+ }));
return;
}
- setSelectedNftIdsByAddress({
- ...selectedNftIdsByAddress,
- [address]: [...selectedNftIds, nftId],
- });
+ setSelectedTokensByUserAddress((previousValue) => ({
+ ...previousValue,
+ [userAddress]: {
+ collectionAddress,
+ tokenIds: [...selectedTokenIds, tokenId],
+ },
+ }));
}
- function toggleNftSelection(nftId: string) {
- if (address === undefined) {
- return null;
- }
+ const deselectNft = useCallback(
+ (tokenId: string, collectionAddress: string) => {
+ if (
+ !isNftSelected(tokenId, collectionAddress) ||
+ userAddress === undefined
+ ) {
+ return;
+ }
+
+ if (selectedTokenIds.length === 1) {
+ setSelectedTokensByUserAddress((previousValue) => ({
+ ...previousValue,
+ [userAddress]: undefined,
+ }));
+ return;
+ }
+
+ setSelectedTokensByUserAddress((previousValue) => ({
+ ...previousValue,
+ [userAddress]: {
+ collectionAddress,
+ tokenIds: selectedTokenIds.filter(
+ (selectedTokenId) => selectedTokenId !== tokenId
+ ),
+ },
+ }));
+ },
+ [
+ isNftSelected,
+ selectedTokenIds,
+ setSelectedTokensByUserAddress,
+ userAddress,
+ ]
+ );
- if (selectedNftIds.includes(nftId)) {
- deselectNft(nftId);
+ function selectBatchNfts(nfts: Array) {
+ if (nfts.length === 0 || userAddress === undefined) {
return;
}
- selectNft(nftId);
+ setSelectedTokensByUserAddress((previousValue) => ({
+ ...previousValue,
+ [userAddress]: {
+ collectionAddress: nfts[0]?.contractAddress,
+ tokenIds: nfts.map((nft) => nft.tokenId).slice(0, MAX_SELECTED_ITEMS),
+ },
+ }));
}
- function toggleSelectAll() {
- if (address === undefined || nfts === undefined) {
+ const deselectAllNfts = useCallback(() => {
+ if (userAddress === undefined) {
return;
}
- if (allCollectionSelected) {
- setSelectedNftIdsByAddress({
- ...selectedNftIdsByAddress,
- [address]: [],
- });
- setLastSelectedCollectionNameByAddress({
- ...lastSelectedCollectionNameByAddress,
- [address]: null,
- });
+ setSelectedTokensByUserAddress((previousValue) => ({
+ ...previousValue,
+ [userAddress]: undefined,
+ }));
+ }, [setSelectedTokensByUserAddress, userAddress]);
+
+ function toggleNftSelection(tokenId: string, collectionAddress: string) {
+ if (userAddress === undefined) {
return;
}
- setSelectedNftIdsByAddress({
- ...selectedNftIdsByAddress,
- [address]: selectedCollection.map((nft) => nft.id),
- });
- setLastSelectedCollectionNameByAddress({
- ...lastSelectedCollectionNameByAddress,
- [address]: selectedCollectionName,
- });
- }
- function selectCollection(collectionName: null | string) {
- setSelectedCollectionName(collectionName);
- }
+ if (isNftSelected(tokenId, collectionAddress)) {
+ deselectNft(tokenId, collectionAddress);
+ return;
+ }
- function isSelected(nftId: string) {
- return selectedNftIds.includes(nftId);
+ selectNft(tokenId, collectionAddress);
}
return {
- allCollectionSelected,
+ deselectAllNfts,
deselectNft,
- isSelected,
- lastSelectedCollectionName,
- nfts,
- numberOfSelectedNfts,
- selectCollection,
- selectNft,
- selectedCollection,
- selectedCollectionName,
- selectedNftIds,
- selectedNfts,
+ isNftSelected,
+ selectBatchNfts,
+ selectedCollectionAddress,
+ selectedTokenIds,
toggleNftSelection,
- toggleSelectAll,
+ totalSelectedNfts,
};
}
diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useTransferEthereumNfts.ts b/apps/web/src/app/(routes)/bridge/_hooks/useTransferEthereumNfts.ts
deleted file mode 100644
index c983acf9..00000000
--- a/apps/web/src/app/(routes)/bridge/_hooks/useTransferEthereumNfts.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { useAccount as useStarknetAccount } from "@starknet-react/core";
-import { parseGwei } from "viem";
-import {
- erc721ABI,
- useContractRead,
- useContractWrite,
- useAccount as useEthereumAccount,
- useWaitForTransaction,
-} from "wagmi";
-
-import useNftSelection from "./useNftSelection";
-
-export default function useTransferEthereumNfts() {
- const { numberOfSelectedNfts, selectedNfts } = useNftSelection();
-
- const { address: ethereumAddress } = useEthereumAccount();
- const { address: starknetAddress } = useStarknetAccount();
-
- const { data: isApprovedForAll } = useContractRead({
- abi: erc721ABI,
- address: selectedNfts[0]?.collectionContractAddress as `0x${string}`,
- args: [
- ethereumAddress ?? "0xtest",
- process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`,
- ],
- enabled: numberOfSelectedNfts > 0,
- functionName: "isApprovedForAll",
- watch: true,
- });
-
- const { data: approveData, write: approveForAll } = useContractWrite({
- abi: erc721ABI,
- address: selectedNfts[0]?.collectionContractAddress as `0x${string}`,
- args: [process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`, true],
- functionName: "setApprovalForAll",
- });
-
- const { isLoading: isApproveLoading } = useWaitForTransaction({
- hash: approveData?.hash,
- });
-
- const { data: depositData, write: depositTokens } = useContractWrite({
- abi: [
- {
- inputs: [
- {
- internalType: "uint256",
- name: "salt",
- type: "uint256",
- },
- {
- internalType: "address",
- name: "collectionL1",
- type: "address",
- },
- {
- internalType: "snaddress",
- name: "ownerL2",
- type: "uint256",
- },
- {
- internalType: "uint256[]",
- name: "ids",
- type: "uint256[]",
- },
- {
- internalType: "bool",
- name: "useAutoBurn",
- type: "bool",
- },
- ],
- name: "depositTokens",
- outputs: [],
- stateMutability: "payable",
- type: "function",
- },
- ],
- address: process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`,
- args: [
- // TODO @YohanTz: Get the proper request hash from ?
- Date.now(),
- selectedNfts[0]?.collectionContractAddress as `0x${string}`,
- starknetAddress,
- selectedNfts.map((selectedNft) => selectedNft?.tokenId),
- false,
- ],
- functionName: "depositTokens",
- // TODO @YohanTz: Get needed gas from ?
- value: parseGwei("40000"),
- });
-
- // Use isSuccess from useWaitForTransaction once fixed... or once we do not rely on a public provider ?
- const { isLoading: isDepositLoading, isSuccess: isDepositSuccess } =
- useWaitForTransaction({
- hash: depositData?.hash,
- });
-
- return {
- approveForAll: () => approveForAll(),
- depositTokens: () => depositTokens(),
- isApproveLoading,
- isApprovedForAll,
- isDepositLoading,
- isDepositSuccess,
- };
-}
diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useTransferNfts.ts b/apps/web/src/app/(routes)/bridge/_hooks/useTransferNfts.ts
deleted file mode 100644
index ad68d251..00000000
--- a/apps/web/src/app/(routes)/bridge/_hooks/useTransferNfts.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { type Chain } from "~/app/_types";
-
-import useTransferEthereumNfts from "./useTransferEthereumNfts";
-import useTransferStarknetNfts from "./useTransferStarknetNfts";
-
-export default function useTransferNftsFromChain(chain: Chain) {
- const transferEthereumNfts = useTransferEthereumNfts();
- const transferStarknetNfts = useTransferStarknetNfts();
-
- return chain === "Ethereum" ? transferEthereumNfts : transferStarknetNfts;
-}
diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useTransferStarknetNfts.ts b/apps/web/src/app/(routes)/bridge/_hooks/useTransferStarknetNfts.ts
index 5e22104c..fe74e24f 100644
--- a/apps/web/src/app/(routes)/bridge/_hooks/useTransferStarknetNfts.ts
+++ b/apps/web/src/app/(routes)/bridge/_hooks/useTransferStarknetNfts.ts
@@ -1,10 +1,10 @@
import {
useContract,
useContractRead,
- useContractWrite,
useAccount as useStarknetAccount,
- useWaitForTransaction,
} from "@starknet-react/core";
+import { useRouter } from "next/navigation";
+import { useCallback, useMemo, useState } from "react";
import { CallData } from "starknet";
import { useAccount as useEthereumAccount } from "wagmi";
@@ -13,12 +13,13 @@ import useNftSelection from "./useNftSelection";
const L2_BRIDGE_ADDRESS = process.env.NEXT_PUBLIC_L2_BRIDGE_ADDRESS || "";
export default function useTransferStarknetNfts() {
- const { selectedNfts } = useNftSelection();
+ const [isSigning, setIsSigning] = useState(false);
+ const { selectedCollectionAddress, selectedTokenIds } = useNftSelection();
const { address: ethereumAddress } = useEthereumAccount();
- const { address: starknetAddress } = useStarknetAccount();
+ const { account: starknetAccount, address: starknetAddress } =
+ useStarknetAccount();
- // TODO @YohanTz: Cast type
const { data: isApprovedForAll } = useContractRead({
abi: [
{
@@ -42,7 +43,7 @@ export default function useTransferStarknetNfts() {
type: "function",
},
],
- address: selectedNfts[0]?.collectionContractAddress ?? "",
+ address: selectedCollectionAddress ?? "",
args: [starknetAddress ?? "0xtest", L2_BRIDGE_ADDRESS],
functionName: "is_approved_for_all",
watch: true,
@@ -86,61 +87,69 @@ export default function useTransferStarknetNfts() {
address: L2_BRIDGE_ADDRESS,
});
- const { data: approveData, write: approveForAll } = useContractWrite({
- calls: [
- {
- calldata: [L2_BRIDGE_ADDRESS, 1],
- contractAddress: selectedNfts[0]?.collectionContractAddress ?? "",
- entrypoint: "set_approval_for_all",
- },
- ],
- });
+ const getDepositCalldata = useCallback(() => {
+ if (
+ bridgeContract?.abi !== undefined &&
+ ethereumAddress !== undefined &&
+ selectedCollectionAddress !== undefined
+ ) {
+ const depositCallData = new CallData(bridgeContract?.abi);
+ return depositCallData.compile("deposit_tokens", {
+ collection_l2: selectedCollectionAddress,
+ owner_l1: ethereumAddress,
+ salt: Date.now(),
+ token_ids: selectedTokenIds,
+ use_deposit_burn_auto: false,
+ use_withdraw_auto: false,
+ });
+ }
+ }, [
+ bridgeContract?.abi,
+ ethereumAddress,
+ selectedCollectionAddress,
+ selectedTokenIds,
+ ]);
- const { isLoading: isApproveLoading } = useWaitForTransaction({
- hash: approveData?.transaction_hash,
- });
+ const depositCalls = useMemo(() => {
+ const approveCall = {
+ calldata: [L2_BRIDGE_ADDRESS, 1],
+ contractAddress: selectedCollectionAddress ?? "",
+ entrypoint: "set_approval_for_all",
+ };
- // TODO @YohanTz: Refacto
- let depositCallData = undefined;
- if (
- bridgeContract?.abi !== undefined &&
- ethereumAddress !== undefined &&
- selectedNfts[0] !== undefined
- ) {
- depositCallData = new CallData(bridgeContract?.abi);
- depositCallData = depositCallData.compile("deposit_tokens", {
- collection_l2: selectedNfts[0]?.collectionContractAddress ?? "",
- owner_l1: ethereumAddress,
- salt: Date.now(),
- token_ids: selectedNfts.map((selectedNft) => selectedNft?.tokenId),
- use_deposit_burn_auto: false,
- use_withdraw_auto: true,
- });
- }
+ const depositCall = {
+ calldata: getDepositCalldata(),
+ contractAddress: L2_BRIDGE_ADDRESS,
+ entrypoint: "deposit_tokens",
+ };
- const { data: depositData, write: depositTokens } = useContractWrite({
- calls: [
- {
- calldata: depositCallData,
- contractAddress: L2_BRIDGE_ADDRESS,
- entrypoint: "deposit_tokens",
- },
- ],
- });
+ if (!isApprovedForAll) {
+ return [approveCall, depositCall];
+ }
+
+ return [depositCall];
+ }, [getDepositCalldata, isApprovedForAll, selectedCollectionAddress]);
+
+ async function depositTokens() {
+ setIsSigning(true);
+ try {
+ // await writeAsync();
+ const depositData = await starknetAccount?.execute(depositCalls);
+ if (depositData !== undefined) {
+ router.push(`lounge/${depositData.transaction_hash}`);
+ setIsSigning(false);
+ }
+ } catch (error) {
+ console.log(error);
+ setIsSigning(false);
+ }
+ }
- const { isLoading: isDepositLoading, isSuccess: isDepositSuccess } =
- useWaitForTransaction({
- hash: depositData?.transaction_hash,
- });
+ const router = useRouter();
return {
- approveForAll: () => approveForAll(),
depositTokens: () => depositTokens(),
- isApproveLoading:
- isApproveLoading && approveData?.transaction_hash !== undefined,
- isApprovedForAll,
- isDepositLoading:
- isDepositLoading && depositData?.transaction_hash !== undefined,
- isDepositSuccess,
+ // isSigning: isSigning && depositData?.transaction_hash !== undefined,
+ isSigning,
};
}
diff --git a/apps/web/src/app/(routes)/bridge/layout.tsx b/apps/web/src/app/(routes)/bridge/layout.tsx
new file mode 100644
index 00000000..fb3f30c4
--- /dev/null
+++ b/apps/web/src/app/(routes)/bridge/layout.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { Typography } from "design-system";
+
+import MainPageContainer from "../../_components/MainPageContainer";
+import TargetChainSwitch from "./_components/TargetChainSwitch";
+import TransferNftsSummary from "./_components/TransferNftsSummary";
+
+// TODO @YohanTz: Refactor when the UX is finalized
+export default function Page({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {/* Where do you want to move
+
+ your digital goods? */}
+ Where do you want to move
+ your Everai?
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/bridge/page.tsx b/apps/web/src/app/(routes)/bridge/page.tsx
index c1449201..12690789 100644
--- a/apps/web/src/app/(routes)/bridge/page.tsx
+++ b/apps/web/src/app/(routes)/bridge/page.tsx
@@ -1,29 +1,7 @@
"use client";
-import { Typography } from "design-system";
+import Collections from "./_components/Collections";
-import MainPageContainer from "../../_components/MainPageContainer";
-import NftTransferSummary from "./_components/NftTransferSummary";
-import TargetChainSwitch from "./_components/TargetChainSwitch";
-import TokenList from "./_components/TokenList";
-
-// TODO @YohanTz: Refactor when the UX is finalized
export default function Page() {
- return (
-
-
-
- Where do you want to move
-
- your digital goods?
-
-
-
-
-
-
-
-
-
- );
+ return ;
}
diff --git a/apps/web/src/app/(routes)/faq/_components/Banner.tsx b/apps/web/src/app/(routes)/faq/_components/Banner.tsx
new file mode 100644
index 00000000..6ffdf5ac
--- /dev/null
+++ b/apps/web/src/app/(routes)/faq/_components/Banner.tsx
@@ -0,0 +1,28 @@
+import clsx from "clsx";
+import { Typography } from "design-system";
+import Image from "next/image";
+
+interface BannerProps {
+ className?: string;
+}
+export default function Banner({ className }: BannerProps) {
+ return (
+
+
+
+ Frequently Asked
+
+ Questions
+
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/faq/_components/FaqEntries.tsx b/apps/web/src/app/(routes)/faq/_components/FaqEntries.tsx
new file mode 100644
index 00000000..752a07a6
--- /dev/null
+++ b/apps/web/src/app/(routes)/faq/_components/FaqEntries.tsx
@@ -0,0 +1,120 @@
+import clsx from "clsx";
+
+import FaqEntry from "./FaqEntry";
+
+interface FaqEntriesProps {
+ className?: string;
+}
+
+export default function FaqEntries({ className }: FaqEntriesProps) {
+ return (
+
+
+ The ArkProject Bridge, developed by Screenshot Labs, allows users to
+ bridge NFTs (ERC-721) between Ethereum (L1) and Starknet (L2).
+
+
+ {`The ArkProject Bridge currently supports the bridging of the Everai NFT
+ collection exclusively. Holders of Everai NFTs can seamlessly transfer
+ their assets from Ethereum (L1) to Starknet (L2) and vice versa,
+ pioneering the integration of NFTs into the next generation of
+ blockchain technology. Don't own an Everai yet? `}
+ Buy one{" "}
+
+ here
+ {" "}
+ and join the bridging fun!
+
+
+ In order to bridge NFTs from Ethereum (L1) to Starknet (L2) via the
+ ArkProject Bridge, you will need to set up a Starknet wallet (eg.{" "}
+
+ Argent
+ {" "}
+ or{" "}
+
+ Braavos
+
+ {`) to which you will send the NFTs. Then, you will need to
+ connect both your Ethereum (L1) wallet and Starknet (L2) wallet to
+ ArkProject bridge, and define the NFTs you'd like to send.`}
+
+
+ The ArkProject NFT Bridge enables two-way transfers. You can bridge your
+ Everai NFTs from Ethereum (L1) to Starknet (L2) and also from Starknet
+ (L2) back to Ethereum (L1). This flexibility allows NFT holders to
+ leverage the benefits of both L1 and L2 technologies whenever they see
+ fit.
+
+
+ Yes, bridging transactions require the payment of gas fees. When
+ transferring NFTs from Ethereum (L1) to Starknet (L2) or vice versa, you
+ will be responsible for the gas fees associated with these transactions
+ on the respective networks. These fees contribute to the processing and
+ security of your transactions on the blockchain.
+
+
+ {`In the unlikely event of a transfer error during the bridging process,
+ you will have the option to click the "Return To Ethereum (L1)" button.
+ This safety feature is designed to ensure that your assets are not lost
+ and can be securely retrieved, providing peace of mind during the
+ transfer process.`}
+
+
+ Should you have any questions or require assistance, our support team is
+ ready to help you. You can reach us at{" "}
+
+ support@arkproject.dev
+
+ . Our team is committed to providing timely and helpful responses to
+ ensure a smooth and enjoyable experience with the ArkProject NFT Bridge.
+
+
+ Transfers typically complete within a few minutes to a few hours,
+ depending on network congestion. We strive to ensure that your bridging
+ experience is as efficient as possible.
+
+
+ When you transfer NFTs from Ethereum to Starknet, it all happens in one
+ single transaction. However, if you want to take the bridge from
+ Starknet back to Ethereum, you need two separate transactions. Users
+ often forget the second step, as it is relevant only once your NFTs are
+ actually moved to L1, sometimes hours after you initiated your
+ transaction.
+
+
+ First, you initiate the transfer on L2 with your Starknet wallet, then
+ you need to wait until the block containing the transaction has been
+ proved and verified by the Starknet verifier smart contract on Ethereum
+ L1. This can take a few hours.
+
+
+ Then you will need to connect again with your Ethereum wallet to
+ ArkProject Bridge and issue a withdraw transaction, withdrawing the NFTs
+ from the Ethereum side of the bridge.
+
+
+ {`Until you do this, the NFTs will remain in the L2 side of the bridge and
+ won't go through to your L1 wallet.`}
+
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/faq/_components/FaqEntry.tsx b/apps/web/src/app/(routes)/faq/_components/FaqEntry.tsx
new file mode 100644
index 00000000..f372de78
--- /dev/null
+++ b/apps/web/src/app/(routes)/faq/_components/FaqEntry.tsx
@@ -0,0 +1,41 @@
+"use client";
+
+import * as Collapsible from "@radix-ui/react-collapsible";
+import { MinusIcon, PlusIcon, Typography } from "design-system";
+import { type PropsWithChildren, useState } from "react";
+
+interface FaqEntryProps {
+ title: string;
+}
+
+export default function FaqEntry({
+ children,
+ title,
+}: PropsWithChildren) {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+
+
+
{title}
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/faq/page.tsx b/apps/web/src/app/(routes)/faq/page.tsx
new file mode 100644
index 00000000..e7b35b1d
--- /dev/null
+++ b/apps/web/src/app/(routes)/faq/page.tsx
@@ -0,0 +1,19 @@
+import Footer from "~/app/_components/Footer";
+import MainPageContainer from "~/app/_components/MainPageContainer";
+
+import Banner from "./_components/Banner";
+import FaqEntries from "./_components/FaqEntries";
+
+export default function FaqPage() {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/(routes)/layout.tsx b/apps/web/src/app/(routes)/layout.tsx
index 2192b0f1..4a3b8211 100644
--- a/apps/web/src/app/(routes)/layout.tsx
+++ b/apps/web/src/app/(routes)/layout.tsx
@@ -1,15 +1,39 @@
"use client";
+import clsx from "clsx";
+// import { type Metadata } from "next";
import localFont from "next/font/local";
import "~/styles/globals.css";
import { api } from "~/utils/api";
-import Footer from "../_components/Footer";
import Header from "../_components/Header";
+import MobilePlaceholder from "../_components/MobilePlaceholder";
import useCurrentChain from "../_hooks/useCurrentChain";
import Providers from "./providers";
+// export const metadata: Metadata = {
+// description: "",
+// // openGraph: {
+// // description:
+// // "",
+// // images: [""],
+// // title: "ArkProject",
+// // type: "website",
+// // url: "https://www.arkproject.dev",
+// // },
+// title: "ArkProject Bridge",
+// // twitter: {
+// // card: "summary_large_image",
+// // creator: "@ArkProjectNFTs",
+// // description:
+// // "",
+// // images: [""],
+// // site: "@ArkProjectNFTs",
+// // title: "ArkProject",
+// // },
+// };
+
const arkProjectFont = localFont({
src: [
{
@@ -46,11 +70,16 @@ const styreneAFont = localFont({
{
path: "../../font/StyreneA-Regular-Web.woff2",
style: "normal",
- weight: "500",
+ weight: "400",
},
{
path: "../../font/StyreneA-RegularItalic-Web.woff2",
style: "italic",
+ weight: "400",
+ },
+ {
+ path: "../../font/StyreneA-Medium-Web.woff2",
+ style: "normal",
weight: "500",
},
{
@@ -67,19 +96,26 @@ function RootLayout({ children }: { children: React.ReactNode }) {
return (
is updated before page load by next-themes
suppressHydrationWarning
>
- {children}
-
+ {children}
+ {/*
+ */}
+
+
+
);
diff --git a/apps/web/src/app/(routes)/legal-notice/page.tsx b/apps/web/src/app/(routes)/legal-notice/page.tsx
new file mode 100644
index 00000000..258a3a39
--- /dev/null
+++ b/apps/web/src/app/(routes)/legal-notice/page.tsx
@@ -0,0 +1,89 @@
+import { Typography } from "design-system";
+
+import Footer from "~/app/_components/Footer";
+import MainPageContainer from "~/app/_components/MainPageContainer";
+
+export default function LegalNoticePage() {
+ return (
+ <>
+
+
+
+
+ Legal notice
+
+
+ Last updated on March 15, 2024
+
+
+
+
+
+ Editor
+
+
+ {`Screenshot Labs, a French simplified joint-stock company with a
+ single shareholder corporation established at 7 place de l'Hotel
+ de Ville, 93600 Aulnay-sous-bois and registered to the trade and
+ companies register of Bobigny under number 898 763 958, with a
+ share capital of 100 euros. For any inquiries or questions, you
+ can email us at `}
+
+ account@arkproject.dev
+
+ .
+
+
+
+ Hosting provider
+
+
+
+ - Vercel Inc.
+ - Privately Held Company
+ -
+ 440 N Barranca Ave #4133 Covina, California 91723 United
+ States
+
+ - Phone number: +1 559 288 7060
+ -
+ Email address:{" "}
+
+ dmca@vercel.com
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/(routes)/lounge/[transactionHash]/page.tsx b/apps/web/src/app/(routes)/lounge/[transactionHash]/page.tsx
new file mode 100644
index 00000000..b61338c5
--- /dev/null
+++ b/apps/web/src/app/(routes)/lounge/[transactionHash]/page.tsx
@@ -0,0 +1,61 @@
+"use client";
+import Image from "next/image";
+import { redirect } from "next/navigation";
+import { useEffect } from "react";
+
+import { api } from "~/utils/api";
+
+import useNftSelection from "../../bridge/_hooks/useNftSelection";
+
+interface PageProps {
+ params: { transactionHash: string };
+ searchParams: { from: string };
+}
+
+/**
+ * Page used when waiting for a deposit transaction to be detected by the bridge
+ */
+export default function Page({
+ params: { transactionHash },
+ searchParams,
+}: PageProps) {
+ const { data: hasBridgeRequestBeenIndexed } =
+ api.bridgeRequest.getHasBridgeRequestIndexed.useQuery(
+ {
+ transactionHash,
+ },
+ { refetchInterval: 2500 }
+ );
+ const { deselectAllNfts, totalSelectedNfts } = useNftSelection();
+
+ useEffect(() => {
+ if (totalSelectedNfts > 0) {
+ deselectAllNfts();
+ }
+ }, [totalSelectedNfts, deselectAllNfts]);
+
+ if (hasBridgeRequestBeenIndexed) {
+ redirect(
+ `/lounge${searchParams.from === "ethereum" ? "?fromEthereum=" : ""}`
+ );
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/(routes)/lounge/_components/Banner.tsx b/apps/web/src/app/(routes)/lounge/_components/Banner.tsx
index aaf2764a..b180a146 100644
--- a/apps/web/src/app/(routes)/lounge/_components/Banner.tsx
+++ b/apps/web/src/app/(routes)/lounge/_components/Banner.tsx
@@ -9,7 +9,7 @@ export default function Banner() {
const { targetChain } = useCurrentChain();
return (
-
+
{targetChain === "Ethereum" ? (
diff --git a/apps/web/src/app/(routes)/lounge/_components/ChainSwitch.tsx b/apps/web/src/app/(routes)/lounge/_components/ChainSwitch.tsx
index 5100d474..1064a4e9 100644
--- a/apps/web/src/app/(routes)/lounge/_components/ChainSwitch.tsx
+++ b/apps/web/src/app/(routes)/lounge/_components/ChainSwitch.tsx
@@ -1,3 +1,6 @@
+"use client";
+
+import clsx from "clsx";
import Image from "next/image";
import useCurrentChain from "~/app/_hooks/useCurrentChain";
@@ -8,13 +11,14 @@ export default function ChainSwitch() {
const { setTargetChain, targetChain } = useCurrentChain();
return (
-
+
+ );
+}
+
+function EveraiCards() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ 123
+
+
+ Everais bridged
+
+
+
+
+
+
+ );
+}
+
+interface CongratsModalProps {
+ isFromTransfer: boolean;
+}
+
+export default function CongratsModal({ isFromTransfer }: CongratsModalProps) {
+ const [open, setOpen] = useState(isFromTransfer);
+
+ const router = useRouter();
+
+ useEffect(() => {
+ router.push("/lounge");
+ }, [router]);
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/lounge/_components/NftCardStatus.tsx b/apps/web/src/app/(routes)/lounge/_components/NftCardStatus.tsx
deleted file mode 100644
index a807fa94..00000000
--- a/apps/web/src/app/(routes)/lounge/_components/NftCardStatus.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Typography } from "design-system";
-
-import { type BridgeRequestEventStatus } from "~/server/api/routers/bridgeRequest";
-
-const variants: Record
= {
- deposit_initiated_l1:
- "bg-playground-purple-50 text-playground-purple-600 dark:bg-playground-purple-200 dark:text-playground-purple-900",
- deposit_initiated_l2:
- "bg-playground-purple-50 text-playground-purple-600 dark:bg-playground-purple-200 dark:text-playground-purple-900",
- error: "bg-folly-red-50 text-folly-red-source",
- withdraw_completed_l1:
- "bg-mantis-green-50 text-mantis-green-500 dark:bg-mantis-green-200 dark:text-mantis-green-900",
- withdraw_completed_l2:
- "bg-mantis-green-50 text-mantis-green-500 dark:bg-mantis-green-200 dark:text-mantis-green-900",
-};
-
-const variantsToStatusText: Record = {
- deposit_initiated_l1: "Transfer in progress",
- deposit_initiated_l2: "Transfer in progress",
- error: "Error transfer",
- withdraw_completed_l1: "Successfully transfered",
- withdraw_completed_l2: "Successfully transfered",
-};
-
-interface NftCardStatusProps {
- status: keyof typeof variants;
-}
-
-export default function NftCardStatus({ status }: NftCardStatusProps) {
- return (
- <>
-
- {variantsToStatusText[status]}
-
- >
- );
-}
diff --git a/apps/web/src/app/(routes)/lounge/_components/NftTransferCard.tsx b/apps/web/src/app/(routes)/lounge/_components/NftTransferCard.tsx
deleted file mode 100644
index 034477eb..00000000
--- a/apps/web/src/app/(routes)/lounge/_components/NftTransferCard.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { Typography } from "design-system";
-import Image from "next/image";
-import { useState } from "react";
-
-import NftCardStackBackground from "~/app/_components/NftCard/NftCardStackBackground";
-import { type BridgeRequestEventStatus } from "~/server/api/routers/bridgeRequest";
-
-import NftCardStatus from "./NftCardStatus";
-import NftTransferModal from "./NftTransferModal";
-
-interface NftTransferCard {
- image?: string;
- name: string;
- status: BridgeRequestEventStatus;
- statusTimestamp: number;
-}
-
-function utcUnixSecondsToIso8601(utcTs: number) {
- const utcMs = utcTs * 1000;
-
- const localDt = new Date();
-
- // Get the local timezone offset in minutes and convert it to milliseconds.
- const localOffset = localDt.getTimezoneOffset() * 60 * 1000;
- // Calculate the local time.
- const localTs = utcMs + localOffset;
-
- const date = new Date(localTs);
-
- // Get day, month, and year components from the Date object
- const day = date.getDate();
- const month = date.getMonth() + 1; // Months are zero-based, so add 1
- const year = date.getFullYear();
-
- // Get hours and minutes components from the Date object
- const hours = date.getHours();
- const minutes = date.getMinutes();
-
- // Format day, month, hours, and minutes to have leading zeros if necessary
- const formattedDay = day < 10 ? "0" + day.toString() : day;
- const formattedMonth = month < 10 ? "0" + month.toString() : month;
- const formattedHours = (hours < 10 ? "0" : "") + hours.toString();
- const formattedMinutes = minutes < 10 ? "0" + String(minutes) : String(minutes);
-
- // Create the formatted date string in the format "dd/mm/yyyy HH:mm"
- const formattedDateTime = `${formattedDay}/${formattedMonth}/${year} ${formattedHours}:${formattedMinutes}`;
-
- return formattedDateTime;
-}
-
-export default function NftTransferCard({
- image,
- name,
- status,
- statusTimestamp,
-}: NftTransferCard) {
- const [isModalOpen, setIsModalOpen] = useState(false);
- const readableDate = utcUnixSecondsToIso8601(statusTimestamp);
-
- function handleOpenModal() {
- setIsModalOpen(true);
- }
-
- return (
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(routes)/lounge/_components/NftTransferItem.tsx b/apps/web/src/app/(routes)/lounge/_components/NftTransferItem.tsx
new file mode 100644
index 00000000..6c2d0123
--- /dev/null
+++ b/apps/web/src/app/(routes)/lounge/_components/NftTransferItem.tsx
@@ -0,0 +1,184 @@
+import * as Collapsible from "@radix-ui/react-collapsible";
+import { useStarkName } from "@starknet-react/core";
+import clsx from "clsx";
+import { MinusIcon, PlusIcon, Typography } from "design-system";
+import Image from "next/image";
+import { useMemo, useState } from "react";
+import { useEnsName } from "wagmi";
+
+import Media from "~/app/_components/Media";
+import { type Chain } from "~/app/_types";
+
+import NftTransferItemContent from "./NftTransferItemContent";
+import NftTransferStatus from "./NftTransferStatus";
+import WithdrawButton from "./WithdrawButton";
+
+interface NftTransferItemProps {
+ arrivalAddress: string;
+ arrivalChain: Chain;
+ arrivalTimestamp?: number;
+ collectionImage: string | undefined;
+ collectionName: string;
+ contractAddress: string;
+ onWithdrawSuccess: () => void;
+ requestContent: Array;
+ status:
+ | "deposit_initiated_l1"
+ | "deposit_initiated_l2"
+ | "error"
+ | "withdraw_available_l1"
+ | "withdraw_completed_l1"
+ | "withdraw_completed_l2";
+ tokenIds: Array;
+ totalCount: number;
+}
+
+function getDisplayedDate(timestamp?: number) {
+ if (timestamp === undefined) {
+ return;
+ }
+
+ const date = new Date(timestamp * 1000);
+
+ return `${
+ date.getMonth() + 1
+ }/${date.getDate()}/${date.getFullYear()} - ${date.getHours()}:${date.getMinutes()}`;
+}
+
+export default function NftTransferItem({
+ arrivalAddress,
+ arrivalChain,
+ arrivalTimestamp,
+ collectionImage,
+ collectionName,
+ contractAddress,
+ onWithdrawSuccess,
+ requestContent,
+ status,
+ tokenIds,
+ totalCount,
+}: NftTransferItemProps) {
+ const [open, setOpen] = useState(false);
+
+ const arrivalShortAddress = useMemo(() => {
+ return arrivalAddress
+ ? `${arrivalAddress.slice(0, 8)}...${arrivalAddress.slice(-6)}`
+ : "";
+ }, [arrivalAddress]);
+
+ const { data: starkName } = useStarkName({ address: arrivalAddress });
+ const { data: ens } = useEnsName({
+ address: arrivalAddress as `0x${string}`,
+ });
+
+ const displayedArrivalAddress = starkName ?? ens ?? arrivalShortAddress;
+
+ return (
+
+
+
+ {collectionImage ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+ {collectionName} Collection
+
+
+ {totalCount} {totalCount > 1 ? "Nfts" : "Nft"}
+
+
+
+
+
+
+
+
+ {getDisplayedDate(arrivalTimestamp)}
+
+
+ {arrivalChain === "Ethereum" ? (
+
+ ) : (
+
+ )}
+
+
+ {displayedArrivalAddress}
+
+
+
+
+
+ {status === "withdraw_available_l1" && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/lounge/_components/NftTransferItemContent.tsx b/apps/web/src/app/(routes)/lounge/_components/NftTransferItemContent.tsx
new file mode 100644
index 00000000..941eb4b0
--- /dev/null
+++ b/apps/web/src/app/(routes)/lounge/_components/NftTransferItemContent.tsx
@@ -0,0 +1,166 @@
+import * as Collapsible from "@radix-ui/react-collapsible";
+import { Typography } from "design-system";
+import Image from "next/image";
+
+import Media from "~/app/_components/Media";
+import { type Chain } from "~/app/_types";
+import { api } from "~/utils/api";
+
+import NftTransferStatus from "./NftTransferStatus";
+
+interface NftTransferItemContentProps {
+ arrivalChain: Chain;
+ contractAddress: string;
+ displayedArrivalAddress: string;
+ open: boolean;
+ status:
+ | "deposit_initiated_l1"
+ | "deposit_initiated_l2"
+ | "error"
+ | "withdraw_available_l1"
+ | "withdraw_completed_l1"
+ | "withdraw_completed_l2";
+ tokenIds: Array;
+}
+
+interface NftTransferItemContentLoadingStateProps {
+ totalCount: number;
+}
+
+function NftTransferItemContentLoadingState({
+ totalCount,
+}: NftTransferItemContentLoadingStateProps) {
+ return (
+
+
+
+
+ {Array(totalCount)
+ .fill(0)
+ .map((_, index) => {
+ return (
+
+ );
+ })}
+
+
+
+ );
+}
+
+export default function NftTransferItemContent({
+ arrivalChain,
+ contractAddress,
+ displayedArrivalAddress,
+ open,
+ status,
+ tokenIds,
+}: NftTransferItemContentProps) {
+ const { data: l1Nfts } = api.l1Nfts.getNftMetadataBatch.useQuery(
+ {
+ contractAddress,
+ tokenIds,
+ },
+ {
+ enabled: open && arrivalChain === "Starknet",
+ }
+ );
+ const { data: l2Nfts } = api.l2Nfts.getNftMetadataBatch.useQuery(
+ {
+ contractAddress,
+ tokenIds,
+ },
+ {
+ enabled: open && arrivalChain === "Ethereum",
+ }
+ );
+
+ const nfts = arrivalChain === "Starknet" ? l1Nfts : l2Nfts;
+
+ if (nfts === undefined) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {nfts.map((nft) => {
+ return (
+
+
+
+ {nft.image !== undefined ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+ {nft.collectionName}
+
+
+ {nft.tokenName}
+
+
+
+
+
+
+ {arrivalChain === "Ethereum" ? (
+
+ ) : (
+
+ )}
+
+
+ {displayedArrivalAddress}
+
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/lounge/_components/NftTransferList.tsx b/apps/web/src/app/(routes)/lounge/_components/NftTransferList.tsx
index 8941298a..32366ee4 100644
--- a/apps/web/src/app/(routes)/lounge/_components/NftTransferList.tsx
+++ b/apps/web/src/app/(routes)/lounge/_components/NftTransferList.tsx
@@ -1,295 +1,197 @@
"use client";
-
-import * as Toolbar from "@radix-ui/react-toolbar";
+import clsx from "clsx";
import { Typography } from "design-system";
-import Image from "next/image";
import { useState } from "react";
-import NftsEmptyState from "~/app/_components/NftsEmptyState";
-import NftsLoadingState from "~/app/_components/NftsLoadingState";
+import CollectionNftsEmptyState from "~/app/_components/CollectionNftsEmptyState";
import useAccountFromChain from "~/app/_hooks/useAccountFromChain";
import useCurrentChain from "~/app/_hooks/useCurrentChain";
+import useIsFullyConnected from "~/app/_hooks/useIsFullyConnected";
import { api } from "~/utils/api";
-import NftTransferCard from "./NftTransferCard";
+import NftTransferItem from "./NftTransferItem";
+import NftTransferListLoadingState from "./NftTransferListLoadingState";
+import SuccessWithdrawModal from "./SuccessWithdrawModal.tsx";
+
+interface NftTransferHeaderProps {
+ className?: string;
+ status: "past" | "transit";
+ totalCount: number;
+}
-export default function NftTransferList() {
- const [displayOption, setDisplayOption] = useState("card");
- const { targetChain } = useCurrentChain();
- const { address } = useAccountFromChain(targetChain);
+function NftTransferHeader({
+ className,
+ status,
+ totalCount,
+}: NftTransferHeaderProps) {
+ return (
+
+
+ Nfts {status === "past" ? "transferred" : "in transit"} ({totalCount})
+
+
+ Transfer status
+
+
+ Arrival
+
+
+ );
+}
+
+interface NftTransferListProps {
+ className?: string;
+ variant?: "lounge";
+}
+
+export default function NftTransferList({
+ className,
+ variant,
+}: NftTransferListProps) {
+ const { sourceChain, targetChain } = useCurrentChain();
+ const { address: targetAddress } = useAccountFromChain(targetChain);
+ const { address: sourceAddress } = useAccountFromChain(sourceChain);
+ const isFullyConnected = useIsFullyConnected();
+ const [withdrawModalOpen, setWithdrawModalOpen] = useState(false);
+
+ const { data: targetBridgeRequests } =
+ api.bridgeRequest.getBridgeRequestsFromAddress.useQuery(
+ {
+ address: targetAddress ?? "",
+ },
+ { enabled: isFullyConnected, refetchInterval: 10000 }
+ );
- const { data: bridgeRequestData } =
+ const { data: sourceBridgeRequests } =
api.bridgeRequest.getBridgeRequestsFromAddress.useQuery(
{
- address: address ?? "",
+ address: sourceAddress ?? "",
},
- { enabled: address !== undefined, refetchInterval: 3000 }
+ { enabled: isFullyConnected, refetchInterval: 10000 }
);
- if (bridgeRequestData === undefined) {
- return ;
+ if (
+ targetBridgeRequests === undefined ||
+ !isFullyConnected ||
+ sourceBridgeRequests === undefined
+ ) {
+ return ;
}
- return bridgeRequestData.length === 0 ? (
- <>
-
-
- There is nothing there...
-
- >
- ) : (
-
-
-
- Nfts in transit ({bridgeRequestData.length})
+ if (
+ targetBridgeRequests.inTransit.requests.length === 0 &&
+ targetBridgeRequests.past.requests.length === 0 &&
+ (variant !== "lounge" ||
+ (sourceBridgeRequests?.inTransit.requests.length === 0 &&
+ sourceBridgeRequests.past.requests.length === 0))
+ ) {
+ return variant !== "lounge" ? (
+ <>
+
+ There is nothing there...
-
- {
- if (value) {
- setDisplayOption(value);
- }
- }}
- aria-label="Display options"
- className="hidden overflow-hidden rounded-md border border-[#d3e2e1] dark:border-dark-blue-600 sm:flex"
- type="single"
- value={displayOption}
- >
-
-
-
-
-
-
-
-
-
+
+ >
+ ) : (
+ <>>
+ );
+ }
+
+ const inTransitRequests = targetBridgeRequests.inTransit.requests;
+ const inTransitTotalCount = targetBridgeRequests.inTransit.totalCount;
+ const pastRequests =
+ variant === "lounge"
+ ? [
+ ...targetBridgeRequests.past.requests,
+ ...sourceBridgeRequests?.past.requests,
+ ]
+ : targetBridgeRequests.past.requests;
- {displayOption === "card" ? (
-
- {bridgeRequestData.map((bridgeRequest, index) => {
- return (
-
- );
- })}
-
- ) : (
-
-
-
-
- Nfts in transit ({bridgeRequestData.length})
-
-
- Transfer status
-
-
- Arrival
-
-
- Grid options
-
-
-
+ const pastRequestsTotalCount =
+ variant === "lounge"
+ ? targetBridgeRequests.past.totalCount +
+ sourceBridgeRequests.past.totalCount
+ : targetBridgeRequests.past.totalCount;
-
- {bridgeRequestData.map((bridgeRequest, index) => {
+ return (
+
+ {inTransitRequests.length > 0 && variant !== "lounge" && (
+ <>
+
+
+ {inTransitRequests.map((bridgeRequest) => {
return (
-
-
-
-
-
-
- {bridgeRequest.sourceCollection}
-
-
- No Nft name
-
-
-
- |
+ setWithdrawModalOpen(true)}
+ requestContent={bridgeRequest.requestContent}
+ status={bridgeRequest.status}
+ tokenIds={bridgeRequest.tokenIds}
+ totalCount={bridgeRequest.totalCount}
+ />
+ );
+ })}
+
+ >
+ )}
-
- {/* TODO @YohanTz: Extract this badge to its own component (used in cards also) */}
-
- {bridgeRequest.status}
-
- |
-
-
-
-
- Estimated arrival
-
-
- {bridgeRequest.statusTimestamp}
-
-
- {/* */}
-
- |
-
- +
- |
-
+ {pastRequests.length > 0 && (
+ <>
+
+ Your past transactions
+
+
+
+
+
+
+ {pastRequests.map((bridgeRequest) => {
+ return (
+ setWithdrawModalOpen(true)}
+ requestContent={bridgeRequest.requestContent}
+ status={bridgeRequest.status}
+ tokenIds={bridgeRequest.tokenIds}
+ totalCount={bridgeRequest.totalCount}
+ />
);
})}
-
-
+
+ >
)}
+
);
}
diff --git a/apps/web/src/app/(routes)/lounge/_components/NftTransferListLoadingState.tsx b/apps/web/src/app/(routes)/lounge/_components/NftTransferListLoadingState.tsx
new file mode 100644
index 00000000..ce627da1
--- /dev/null
+++ b/apps/web/src/app/(routes)/lounge/_components/NftTransferListLoadingState.tsx
@@ -0,0 +1,78 @@
+import clsx from "clsx";
+import { Typography } from "design-system";
+import Image from "next/image";
+
+interface NftTransferItemLoadingStateProps {
+ className?: string;
+}
+
+function NftTransferItemLoadingState({
+ className,
+}: NftTransferItemLoadingStateProps) {
+ return (
+
+ );
+}
+
+interface NftTransferListLoadingStateProps {
+ className?: string;
+}
+
+export default function NftTransferListLoadingState({
+ className,
+}: NftTransferListLoadingStateProps) {
+ return (
+ <>
+
+
+ Nfts in transit
+
+
+ Transfer status
+
+
+ Arrival
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/(routes)/lounge/_components/NftTransferModal.tsx b/apps/web/src/app/(routes)/lounge/_components/NftTransferModal.tsx
deleted file mode 100644
index 07ce1fd0..00000000
--- a/apps/web/src/app/(routes)/lounge/_components/NftTransferModal.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import { Modal, Typography } from "design-system";
-import Image from "next/image";
-
-import useCurrentChain from "~/app/_hooks/useCurrentChain";
-
-import { CHAIN_LOGOS_BY_NAME } from "../../../_lib/utils/connectors";
-
-interface NftTransferModalProps {
- image?: string;
- isOpen: boolean;
- name: string;
- onOpenChange: (open: boolean) => void;
-}
-
-export default function NftTransferModal({
- image,
- isOpen,
- name,
- onOpenChange,
-}: NftTransferModalProps) {
- const { sourceChain, targetChain } = useCurrentChain();
- return (
-
-
-
-
- {image ? (
-
- ) : (
-
- No metadata
-
- )}
-
-
-
- {name}
-
- Migration in Progress
-
-
- Your asset cross the bridge, the small walk will take 15 minutes
-
-
-
-
- {/*
*/}
-
-
-
- 🌈
- Assets en route to {targetChain}
-
-
-
-
-
-
- Transaction sent
- 1/1
-
-
-
- Transaction confirmed
-
- 1/1
-
-
-
- Nfts received on {targetChain}
-
- 1/1
-
-
-
-
- Note that it will not cancel the gas fee.
-
- {/* */}
-
-
-
- );
-}
diff --git a/apps/web/src/app/(routes)/lounge/_components/NftTransferStatus.tsx b/apps/web/src/app/(routes)/lounge/_components/NftTransferStatus.tsx
new file mode 100644
index 00000000..f52079f8
--- /dev/null
+++ b/apps/web/src/app/(routes)/lounge/_components/NftTransferStatus.tsx
@@ -0,0 +1,65 @@
+import clsx from "clsx";
+import { Typography } from "design-system";
+
+import { type BridgeRequestEventStatus } from "~/server/api/routers/bridgeRequest";
+
+const variants: Record
= {
+ deposit_initiated_l1:
+ "bg-playground-purple-50 text-playground-purple-600 dark:bg-playground-purple-200 dark:text-playground-purple-900",
+ deposit_initiated_l2:
+ "bg-playground-purple-50 text-playground-purple-600 dark:bg-playground-purple-200 dark:text-playground-purple-900",
+ error: "bg-folly-red-50 text-folly-red-source",
+ withdraw_available_l1:
+ "bg-space-blue-100 text-space-blue-source dark:bg-space-blue-800 dark:text-space-blue-400",
+ withdraw_completed_l1:
+ "bg-mantis-green-50 text-mantis-green-500 dark:bg-mantis-green-200 dark:text-mantis-green-900",
+ withdraw_completed_l2:
+ "bg-mantis-green-50 text-mantis-green-500 dark:bg-mantis-green-200 dark:text-mantis-green-900",
+};
+
+const variantsToStatusText: Record = {
+ deposit_initiated_l1: "Transfer in progress",
+ deposit_initiated_l2: "Transfer in progress",
+ error: "Error transfer",
+ withdraw_available_l1: "Ready for withdrawal",
+ withdraw_completed_l1: "Successfully transferred",
+ withdraw_completed_l2: "Successfully transferred",
+};
+
+interface NftCardStatusProps {
+ className?: string;
+ status: keyof typeof variants;
+}
+
+export default function NftCardStatus({
+ className,
+ status,
+}: NftCardStatusProps) {
+ return (
+
+
+ {variantsToStatusText[status]}
+
+ {(status === "deposit_initiated_l1" ||
+ status === "deposit_initiated_l2") && (
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/lounge/_components/SuccessWithdrawModal.tsx.tsx b/apps/web/src/app/(routes)/lounge/_components/SuccessWithdrawModal.tsx.tsx
new file mode 100644
index 00000000..350a77c6
--- /dev/null
+++ b/apps/web/src/app/(routes)/lounge/_components/SuccessWithdrawModal.tsx.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { Dialog, DialogContent, Typography } from "design-system";
+import Link from "next/link";
+
+interface SuccessWithdrawModalProps {
+ onOpenChange: (open: boolean) => void;
+ open: boolean;
+}
+
+export default function SuccessWithdrawModal({
+ onOpenChange,
+ open,
+}: SuccessWithdrawModalProps) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/lounge/_components/WithdrawButton.tsx b/apps/web/src/app/(routes)/lounge/_components/WithdrawButton.tsx
new file mode 100644
index 00000000..949c87e2
--- /dev/null
+++ b/apps/web/src/app/(routes)/lounge/_components/WithdrawButton.tsx
@@ -0,0 +1,30 @@
+import { Typography } from "design-system";
+
+import useL1Withdraw from "../_hooks/useL1Withdraw";
+
+interface WithdrawButtonProps {
+ onSuccess: () => void;
+ requestContent: Array;
+}
+
+export default function WithdrawButton({
+ onSuccess,
+ requestContent,
+}: WithdrawButtonProps) {
+ const { isSigning, isWithdrawLoading, withdraw } = useL1Withdraw({
+ onSuccess,
+ });
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/lounge/_hooks/useL1Withdraw.ts b/apps/web/src/app/(routes)/lounge/_hooks/useL1Withdraw.ts
new file mode 100644
index 00000000..eea3178e
--- /dev/null
+++ b/apps/web/src/app/(routes)/lounge/_hooks/useL1Withdraw.ts
@@ -0,0 +1,54 @@
+import { useEffect } from "react";
+import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
+
+interface UseL1WithdrawProps {
+ onSuccess?: () => void;
+}
+
+export default function useL1Withdraw({ onSuccess }: UseL1WithdrawProps) {
+ const {
+ data: withdrawHash,
+ isLoading: isSigning,
+ writeContract: writeContractWithdraw,
+ } = useWriteContract({ mutation: { onSuccess } });
+
+ const { isLoading: isWithdrawLoading, isSuccess: isWithdrawSuccess } =
+ useWaitForTransactionReceipt({
+ hash: withdrawHash,
+ });
+
+ function withdraw(requestContent: Array) {
+ writeContractWithdraw({
+ abi: [
+ {
+ inputs: [
+ { internalType: "uint256[]", name: "request", type: "uint256[]" },
+ ],
+ name: "withdrawTokens",
+ outputs: [{ internalType: "address", name: "", type: "address" }],
+ stateMutability: "payable",
+ type: "function",
+ },
+ { inputs: [], name: "CairoWrapError", type: "error" },
+ { inputs: [], name: "NotSupportedYetError", type: "error" },
+ { inputs: [], name: "WithdrawAlreadyError", type: "error" },
+ { inputs: [], name: "WithdrawMethodError", type: "error" },
+ ],
+ address: process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`,
+ args: [requestContent],
+ functionName: "withdrawTokens",
+ });
+ }
+
+ useEffect(() => {
+ if (isWithdrawSuccess) {
+ onSuccess?.();
+ }
+ }, [isWithdrawSuccess]);
+
+ return {
+ isSigning,
+ isWithdrawLoading: isWithdrawLoading && withdrawHash !== undefined,
+ withdraw,
+ };
+}
diff --git a/apps/web/src/app/(routes)/lounge/page.tsx b/apps/web/src/app/(routes)/lounge/page.tsx
index 686327b9..68213a53 100644
--- a/apps/web/src/app/(routes)/lounge/page.tsx
+++ b/apps/web/src/app/(routes)/lounge/page.tsx
@@ -1,18 +1,32 @@
-"use client";
+import Footer from "~/app/_components/Footer";
import MainPageContainer from "../../_components/MainPageContainer";
import Banner from "./_components/Banner";
import ChainSwitch from "./_components/ChainSwitch";
+import CongratsModal from "./_components/CongratsModal";
import NftTransferList from "./_components/NftTransferList";
-export default function Page() {
+interface LoungePageProps {
+ searchParams: {
+ fromEthereum?: string;
+ };
+}
+
+export default function LoungePage({ searchParams }: LoungePageProps) {
return (
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+ >
);
}
diff --git a/apps/web/src/app/(routes)/page.tsx b/apps/web/src/app/(routes)/page.tsx
index ba7ef8d6..f762a3f8 100644
--- a/apps/web/src/app/(routes)/page.tsx
+++ b/apps/web/src/app/(routes)/page.tsx
@@ -1,36 +1,97 @@
+"use client";
+
+import { useAccount as useStarknetAccount } from "@starknet-react/core";
import { Typography } from "design-system";
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+import { useAccount as useEthereumAccount } from "wagmi";
import ConnectWalletsButton from "../_components/ConnectWalletsButton";
-import MainPageContainer from "../_components/MainPageContainer";
-import NftsEmptyState from "../_components/NftsEmptyState";
+import Footer from "../_components/Footer";
export default function Page() {
+ const { address: starknetAddress } = useStarknetAccount();
+ const { address: ethereumAddress } = useEthereumAccount();
+
+ const router = useRouter();
+
+ useEffect(() => {
+ if (starknetAddress !== undefined && ethereumAddress !== undefined) {
+ void router.push("/bridge");
+ }
+ }, [starknetAddress, ethereumAddress, router]);
+
return (
-
- {/* TODO @YohanTz: Extract magic values like this to CSS variable (top-[5.75rem]) */}
-
-
- Connect your wallets
-
- to start moving your Digital Goods
-
-
-
-
-
-
- In this space, you can explore and enjoy your digital treasures from
- any blockchain.
-
-
-
+ <>
+
+
+
+
+
+
+
+
+ Start moving your Everai
+
+ on Starknet
+
+
+
+ Bridge your Everai NFTs and complete your first
+
+ ArkProject quests.
+
+
+
+
+
+
+
+ >
);
}
diff --git a/apps/web/src/app/(routes)/portfolio/_components/Banner.tsx b/apps/web/src/app/(routes)/portfolio/_components/Banner.tsx
index b8f8f2be..d31d9f6c 100644
--- a/apps/web/src/app/(routes)/portfolio/_components/Banner.tsx
+++ b/apps/web/src/app/(routes)/portfolio/_components/Banner.tsx
@@ -4,7 +4,7 @@ import Link from "next/link";
export default function Banner() {
return (
-
+
+
+ {"You have no NFT(s) in your wallets..."}
+
+
+
+ );
+ }
+
+ if (l1NftsData === undefined || l2NftsData === undefined) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {l1NftsData.pages.map((page) => {
+ return page.ownedNfts.map((nft) => {
+ return (
+
+ );
+ });
+ })}
+
+ {!hasNextL1NftsPage &&
+ l2NftsData.pages.map((page) => {
+ return page.ownedNfts.map((nft) => {
+ return (
+
+ );
+ });
+ })}
+
+ fetchNextL1NftsPage()}
+ hasNextPage={hasNextL1NftsPage}
+ isFetchingNextPage={isFetchingNextL1NftsPage}
+ />
+
+ );
+}
+
+function CollectionsTabsContent() {
+ const {
+ data: l1CollectionsData,
+ fetchNextPage: fetchNextL1CollectionsPage,
+ hasNextPage: hasNextL1CollectionsPage,
+ isFetchingNextPage: isFetchingNextL1CollectionsPage,
+ } = useInfiniteEthereumCollections({ pageSize: 5 });
+
+ const {
+ data: l2CollectionsData,
+ // fetchNextPage: fetchNextL2CollectionsPage,
+ // hasNextPage: hasNextL2CollectionsPage,
+ // isFetchingNextPage: isFetchingNextL2CollectionsPage,
+ } = useInfiniteStarknetCollections();
+
+ const isFullyConnected = useIsFullyConnected();
+
+ if (
+ (l1CollectionsData?.pages[0]?.collections.length === 0 &&
+ l2CollectionsData?.pages[0]?.collections.length === 0) ||
+ !isFullyConnected
+ ) {
+ return (
+
+
+ {"You have no NFT(s) in your wallets..."}
+
+
+
+ );
+ }
+
+ if (l1CollectionsData === undefined || l2CollectionsData === undefined) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {l1CollectionsData.pages.map((page) => {
+ return page.collections.map((collection) => {
+ return (
+
+ );
+ });
+ })}
+ {!hasNextL1CollectionsPage &&
+ l2CollectionsData.pages.map((page) => {
+ return page.collections.map((collection) => {
+ return (
+
+ );
+ });
+ })}
+
+ fetchNextL1CollectionsPage()}
+ hasNextPage={hasNextL1CollectionsPage}
+ isFetchingNextPage={isFetchingNextL1CollectionsPage}
+ />
+
+ );
+}
+
+function EthereumNTabsContent() {
+ const {
+ data: l1NftsData,
+ fetchNextPage: fetchNextL1NftsPage,
+ hasNextPage: hasNextL1NftsPage,
+ isFetchingNextPage: isFetchingNextL1NftsPage,
+ } = useInfiniteEthereumNfts({ pageSize: 5 });
+
+ const isFullyConnected = useIsFullyConnected();
+
+ if (l1NftsData?.pages[0]?.totalCount === 0 || !isFullyConnected) {
+ return (
+
+
+ {"You have no NFT(s) in your Ethereum wallet..."}
+
+
+
+ );
+ }
+
+ if (l1NftsData === undefined) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {l1NftsData.pages.map((page) => {
+ return page.ownedNfts.map((nft) => {
+ return (
+
+ );
+ });
+ })}
+
+ fetchNextL1NftsPage()}
+ hasNextPage={hasNextL1NftsPage}
+ isFetchingNextPage={isFetchingNextL1NftsPage}
+ />
+
+ );
+}
+
+function StarknetTabsContent() {
+ const {
+ data: l2NftsData,
+ // fetchNextPage: fetchNextL2NftsPage,
+ // hasNextPage: hasNextL2NftsPage,
+ // isFetchingNextPage: isFetchingNextL2NftsPage,
+ } = useInfiniteStarknetNfts();
+
+ const isFullyConnected = useIsFullyConnected();
+
+ if (l2NftsData?.pages[0]?.ownedNfts.length === 0 || !isFullyConnected) {
+ return (
+
+
+ {"You have no NFT(s) in your Starknet wallet..."}
+
+
+
+ );
+ }
+
+ if (l2NftsData === undefined) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {l2NftsData.pages.map((page) => {
+ return page.ownedNfts.map((nft) => {
+ return (
+
+ );
+ });
+ })}
+
+ );
+}
+
+export default function NftTabsContent() {
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/portfolio/_components/NftTabsList.tsx b/apps/web/src/app/(routes)/portfolio/_components/NftTabsList.tsx
new file mode 100644
index 00000000..631be9e0
--- /dev/null
+++ b/apps/web/src/app/(routes)/portfolio/_components/NftTabsList.tsx
@@ -0,0 +1,67 @@
+import * as Tabs from "@radix-ui/react-tabs";
+
+import useInfiniteEthereumCollections from "~/app/_hooks/useInfiniteEthereumCollections";
+import useInfiniteEthereumNfts from "~/app/_hooks/useInfiniteEthereumNfts";
+import useInfiniteStarknetCollections from "~/app/_hooks/useInfiniteStarknetCollections";
+import useInfiniteStarknetNfts from "~/app/_hooks/useInfiniteStarknetNfts";
+
+import NftTabsTrigger from "./NftTabsTrigger";
+
+export default function NftTabsList() {
+ const { isLoading: isl1NftsLoading, totalCount: l1NftsTotalCount } =
+ useInfiniteEthereumNfts();
+
+ const {
+ isLoading: isl1CollectionsLoading,
+ totalCount: l1CollectionsTotalCount,
+ } = useInfiniteEthereumCollections();
+
+ const { isLoading: isl2NftsLoading, totalCount: l2NftsTotalCount } =
+ useInfiniteStarknetNfts();
+
+ const {
+ isLoading: isl2CollectionsLoading,
+ totalCount: l2CollectionsTotalCount,
+ } = useInfiniteStarknetCollections();
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/portfolio/_components/NftTabsTrigger.tsx b/apps/web/src/app/(routes)/portfolio/_components/NftTabsTrigger.tsx
new file mode 100644
index 00000000..d565bc1d
--- /dev/null
+++ b/apps/web/src/app/(routes)/portfolio/_components/NftTabsTrigger.tsx
@@ -0,0 +1,47 @@
+import * as Tabs from "@radix-ui/react-tabs";
+import clsx from "clsx";
+import { Typography } from "design-system";
+
+import useIsFullyConnected from "~/app/_hooks/useIsFullyConnected";
+
+interface NftTabsTriggerProps {
+ className?: string;
+ isLoading: boolean;
+ tabName: string;
+ tabValue: string;
+ totalCount: number | undefined;
+}
+
+export default function NftTabsTrigger({
+ className,
+ isLoading,
+ tabName,
+ tabValue,
+ totalCount,
+}: NftTabsTriggerProps) {
+ const isFullyConnected = useIsFullyConnected();
+
+ return (
+
+ {tabName}
+
+ {!isFullyConnected ? (
+ 0
+ ) : isLoading ? (
+
+ ) : (
+ totalCount
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/app/(routes)/portfolio/_components/NftsTabs.tsx b/apps/web/src/app/(routes)/portfolio/_components/NftsTabs.tsx
index 57cb3391..7515a9f2 100644
--- a/apps/web/src/app/(routes)/portfolio/_components/NftsTabs.tsx
+++ b/apps/web/src/app/(routes)/portfolio/_components/NftsTabs.tsx
@@ -1,211 +1,15 @@
"use client";
import * as Tabs from "@radix-ui/react-tabs";
-import { useAccount as useStarknetAccount } from "@starknet-react/core";
-import { Typography } from "design-system";
-import { useAccount as useEthereumAccount } from "wagmi";
-import NftCard from "~/app/_components/NftCard/NftCard";
-import NftsLoadingState from "~/app/_components/NftsLoadingState";
-import { api } from "~/utils/api";
-
-interface NftTabsTriggerProps {
- className?: string;
- nftNumber: number;
- tabName: string;
- tabValue: string;
-}
-
-function NftTabsTrigger({
- className,
- nftNumber,
- tabName,
- tabValue,
-}: NftTabsTriggerProps) {
- return (
-
- {tabName}
-
- {nftNumber}
-
-
- );
-}
+import NftTabsContent from "./NftTabsContent";
+import NftTabsList from "./NftTabsList";
export default function NftsTabs() {
- const { address: ethereumAddress } = useEthereumAccount();
- const { address: starknetAddress } = useStarknetAccount();
-
- const { data: l1Nfts } = api.nfts.getL1NftsByCollection.useQuery(
- {
- address: ethereumAddress ?? "",
- },
- { enabled: ethereumAddress !== undefined }
- );
-
- const { data: l2Nfts } = api.nfts.getL2NftsByCollection.useQuery(
- {
- address: starknetAddress ?? "",
- },
- { enabled: starknetAddress !== undefined }
- );
-
- if (l1Nfts === undefined || l2Nfts === undefined) {
- return ;
- }
-
return (
-
-
-
-
-
-
-
-
-
- {l1Nfts.raw.map((nft) => {
- return (
- 0
- ? nft.title
- : `${nft.collectionName} #${nft.tokenId}`
- }
- cardType="nft"
- chain="Ethereum"
- image={nft.image}
- isSelected={false}
- key={nft.id}
- />
- );
- })}
- {l2Nfts.raw.map((nft) => {
- return (
- 0
- ? nft.title
- : `${nft.collectionName} #${nft.tokenId}`
- }
- cardType="nft"
- chain="Starknet"
- image={nft.image}
- isSelected={false}
- key={nft.id}
- />
- );
- })}
-
-
- {/* TODO @YohanTz: Add Starknet here */}
- {Object.entries(l1Nfts.byCollection).map(([collectionName, nfts]) => {
- return (
-
- );
- })}
- {Object.entries(l2Nfts.byCollection).map(([collectionName, nfts]) => {
- return (
-
- );
- })}
-
-
- {l1Nfts.raw.map((nft) => {
- return (
- 0
- ? nft.title
- : `${nft.collectionName} #${nft.tokenId}`
- }
- cardType="nft"
- chain="Ethereum"
- image={nft.image}
- isSelected={false}
- key={nft.id}
- />
- );
- })}
-
-
- {l2Nfts.raw.map((nft) => {
- return (
- 0
- ? nft.title
- : `${nft.collectionName} #${nft.tokenId}`
- }
- cardType="nft"
- chain="Starknet"
- image={nft.image}
- isSelected={false}
- key={nft.id}
- />
- );
- })}
-
-
+
+
);
}
diff --git a/apps/web/src/app/(routes)/portfolio/page.tsx b/apps/web/src/app/(routes)/portfolio/page.tsx
index c4e14173..a6af33d6 100644
--- a/apps/web/src/app/(routes)/portfolio/page.tsx
+++ b/apps/web/src/app/(routes)/portfolio/page.tsx
@@ -1,27 +1,26 @@
-// import { Typography } from "design-system";
+import Footer from "~/app/_components/Footer";
import MainPageContainer from "../../_components/MainPageContainer";
+import BridgingQuestBanner from "../bridge/_components/BridgingQuestBanner";
+import NftTransferList from "../lounge/_components/NftTransferList";
import Banner from "./_components/Banner";
import NftsTabs from "./_components/NftsTabs";
export default function Page() {
return (
-
-
-
+ <>
+
+
+
-
+
- {/*
- Your past transactions
-
-
- */}
-
-
+
+
+
+
+
+
+ >
);
}
diff --git a/apps/web/src/app/(routes)/privacy/page.tsx b/apps/web/src/app/(routes)/privacy/page.tsx
new file mode 100644
index 00000000..30fa878f
--- /dev/null
+++ b/apps/web/src/app/(routes)/privacy/page.tsx
@@ -0,0 +1,531 @@
+import { Typography } from "design-system";
+
+import Footer from "~/app/_components/Footer";
+import MainPageContainer from "~/app/_components/MainPageContainer";
+
+export default function PrivacyPolicyPage() {
+ return (
+ <>
+
+
+
+
+ Privacy policy
+
+
+ Last updated on March 15, 2024
+
+
+
+
+
+ Introduction
+
+
+ {`Welcome to the ArkProject Bridge and LeaderBoard privacy policy.
+ This Privacy Policy explains how personal data relating to you is
+ collected and used by Screenshot Labs (“Screenshot Labs,” “we,”
+ “our,” “ours,” and “us”), a French corporation established at 7
+ place de l'Hotel de Ville, 93600 Aulnay-sous-bois, FRANCE.`}
+
+
+ {`The ArkProject Bridge (the "Bridge"), located at `}
+
+ https://bridge.arkproject.dev/
+
+ {`, allows users to connect their
+ digital wallets for the purpose of transferring NFTs between the
+ Ethereum Blockchain and the Starknet Blockchain.`}
+
+
+ The ArkProject LeaderBoard (the “LeaderBoard”) located at [insert
+ URL], allows users to validate Ark quest to gather points and get
+ rewards.
+
+
+ This Privacy Policy applies to personal data we collect when you
+ use the Bridge and/or the LeaderBoard and the websites operated by
+ us and located at{" "}
+
+ https://bridge.arkproject.dev/
+ {" "}
+ or [insert URL] or otherwise interact with the services or tools
+ we provide (together the “ArkProject Services”). We are committed
+ to protecting and respecting your privacy.
+
+
+
+ Personal Data We Collect
+
+
+ When using the ArkProject Services, we collect the following types
+ of data relating to you:
+
+ -
+ Crypto Wallet Addresses: You will need to connect your
+ Ethereum and Starknet wallets in order to (i) allow the
+ transfer of NFTs through the Bridge, or (ii) validate quests
+ relating to the Bridge or validate any other relevant quests.
+ We will therefore process your digital wallets addresses for
+ executing transactions and providing you the ArkProject
+ Services.
+
+ -
+ NFT Collections Data: We also collect information about
+ the NFT collections in your Ethereum and Starknet wallets,
+ including but not limited to, transaction history, ownership
+ details, and metadata. This data allows us to provide relevant
+ services and improve user experience on the ArkProject
+ Services.
+
+ -
+ X (Twitter) Account Access Information: If you opt to
+ connect your X (Twitter) account with the Bridge or if you
+ wish to validate quests relating to X (Twitter) actions on the
+ Leaderboard, we will collect your Twitter username and public
+ profile information. This information is used to provide the
+ relevant ArkProject Services.
+
+ -
+ Marketplace Interaction Data: After using the
+ ArkProject Bridge, you may access various NFT marketplaces
+ (e.g. Element, Pyramid, Unframed, Flex, Ventory, OpenSea, etc)
+ directly from the ArkProject Services. We will track your
+ interactions with such marketplaces (until you leave the
+ ArkProject Services), including tracking clicks and user
+ navigation patterns on the ArkProject Services, to understand
+ user preferences and improve service offerings.
+
+ -
+
+ Information Collected by Cookies and Other Tracking
+ Technologies when you use the ArkProject Services
+
+ : We may use various technologies to collect information,
+ including cookies and web beacons. Cookies are small data
+ files stored on your hard drive or in device memory that help
+ us improve our services and your experience, see which areas
+ and features of our services are popular and count visits. Web
+ beacons are electronic images that may be used in our services
+ or emails and help deliver cookies, count visits and
+ understand usage and campaign effectiveness. For more
+ information which cookies are used and how to disable them,
+ please see sections “Data Recipients and processors” and “Your
+ Rights” below. When you access or use the ArkProject Services,
+ we may automatically collect information about you, including:
+
+ -
+ Log Information: We may collect log information
+ about your use of the ArkProject Services, including the
+ type of browser you use, access times, pages viewed, your
+ IP address and the page you visited before navigating to
+ the ArkProject Services.
+
+ -
+ Device Information: We may collect information
+ about the computer or mobile device you use to access the
+ ArkProject Services, including the hardware model,
+ operating system and version, unique device identifiers
+ and mobile network information.
+
+
+
+
+
+
+
+ How We Use Your Personal Data
+
+
+ The data collected via the ArkProject Services is used to:
+
+ - Vercel Inc.
+ -
+ Facilitate the transfer and management of NFTs between
+ blockchains.
+
+ -
+ Personalize and enhance your experience on the ArkProject
+ Services.
+
+ -
+ Analyze and improve our service offerings and user interface.
+
+ -
+ To provide, maintain, and improve our services;
+
+ -
+ Monitor and analyze trends, usage and activities in connection
+ with our services;
+
+ -
+ Detect, investigate and prevent fraudulent transactions and
+ other illegal activities and protect the rights and property
+ of Screenshot Labs and others; and
+
+ -
+ Carry out any other purpose described to you at the time the
+ infor
+
+
+ These uses are either based on the performance of our agreement
+ with you to provide you with the relevant services or on our
+ legitimate interest to process your personal data for the above
+ purposes.
+
+
+
+ Data Sharing, data recipients and processors
+
+
+ We may share the personal data relating to you and collected
+ through the ArkProject Services with:
+
+ -
+ Partnered NFT marketplaces, to enhance interoperability and
+ user experience across platforms. Any data sharing will be
+ conducted in accordance with this Privacy Policy and with the
+ aim of providing seamless service integration.
+
+ -
+ Service providers and third-party vendors who assist us in
+ providing the ArkProject Services, subject to strict data
+ protection and privacy conditions.
+
+ -
+ We may also use or share personal data about you as follows or
+ as otherwise described in this Privacy Policy:
+
+ -
+ With consultants and other service providers who need
+ access to such information to carry out work on our
+ behalf;
+
+ -
+ In response to a request for information if we believe
+ disclosure is in accordance with, or required by, any
+ applicable law, regulation or legal process;
+
+ -
+ In connection with, or during negotiations of, any merger,
+ sale of company assets, financing or acquisition of all or
+ a portion of our business by another company;
+
+ -
+ Between and among Screenshot Labs and our current and
+ future parents, affiliates, subsidiaries and other
+ companies under common control and ownership; and
+
+ - With your consent or at your direction.
+
+
+
+ Our website uses cookies. Some cookies which are used for the sole
+ purpose of enabling or facilitating use of the website or which
+ are strictly necessary for the provision of an online
+ communication service at the express request of the user (such as
+ cookies intended for authentication to a service or to store the
+ contents of a shopping cart), are deemed “essential” and cannot be
+ refused. You can however accept or refuse non-essential cookies or
+ tracking technologies which are implemented by the following third
+ parties that act as our processors:
+
+ -
+ Mixpanel: Our website utilizes{" "}
+
+ Mixpanel
+
+ , a web analytics service provided by Mixpanel Inc. Mixpanel
+ uses cookies and other tracking technologies to help us
+ understand how users engage with our website by reporting on
+ their interactions and usage patterns. Mixpanel collects data
+ about your use of our site, such as:
+
+ -
+ How you interact with our website, including the pages you
+ visit and the links you click on.
+
+ -
+ Information about the device and browser you use to access
+ our site, including type, settings, and unique
+ identifiers.
+
+ -
+ Actions you take within our application, including the
+ features you use and the time spent on our application.
+
+
+
+ -
+ Hotjar: Our website uses{" "}
+
+ Hotjar
+
+ {`, an analysis and
+ feedback tool provided by Hotjar Ltd, which helps us to
+ understand how our website's users use it (e.g., how much time
+ they spend on which pages, which links they choose to click,
+ what users do and don't like, etc.) and this enables us to
+ build and maintain our service with user feedback. Hotjar uses
+ cookies and other technologies to collect data on our users'
+ behavior and their devices. This includes:`}
+
+ -
+ Device-specific data, such as device type, screen size,
+ country of access, and browser information.
+
+ -
+ Log data such as referring domain, pages visited,
+ geographic location, preferred language used to display
+ our website, and the date and time of access.
+
+ -
+ User interactions like mouse events (movements, location,
+ and clicks) and keypresses.
+
+
+
+
+ We do not sell or share your personal information with third
+ parties for their marketing purposes without your explicit
+ consent.
+
+
+
+ Data Security
+
+
+ We are committed to protecting your personal information and have
+ implemented appropriate security measures to protect your
+ information from unauthorized access, use, or disclosure.
+
+
+
+ International transfers
+
+
+ {`You are informed that your personal information may be hosted in
+ the US by our processors. Transfers of personal data from the
+ European Union to the US are covered by the Data Privacy Framework
+ and the European Commission's adequacy decision or by the Standard
+ Contractual Clauses. For more information regarding the transfers
+ of personal information, you can email us at: `}
+
+ account@arkproject.dev.
+
+
+
+
+ Your Rights and Choices
+
+
+ Under the terms of the regulations, you may exercise the rights
+ detailed below at any time:
+
+ 1. Right of access: you may obtain information on the
+ nature, origin and use of your personal data. If your personal
+ data is transmitted to third parties, you may also obtain
+ information concerning the identity or categories of recipients;
+
+
+ 2. Right of rectification: you may request that inaccurate
+ or incomplete personal data be rectified or completed;
+
+
+ 3. Right to erasure: you may request the erasure of your
+ personal data, in particular if the personal data is no longer
+ required for the purposes of processing, except in the cases
+ provided for by the regulations;
+
+
+ 4. Right to limit processing: you may request that your
+ personal data be made temporarily inaccessible in order to limit
+ their future processing in the situations provided for by the
+ applicable regulations;
+
+
+ 5. Right to object: you may object to certain processing of
+ your personal data on grounds relating to your particular
+ situation unless there are compelling legitimate grounds for the
+ processing which override your interests, rights and freedoms or
+ for the establishment, exercise or defense of legal claims. You
+ may also object to the processing of your personal data at any
+ time and without giving any reason for commercial prospecting
+ purposes.
+
+
+ 6. right to portability: in applicable cases, you may
+ request to receive communication of the personal data that you
+ have provided to us, in a structured and commonly used computer
+ format.
+
+
+ 7. right to communicate instructions regarding the fate of your
+ personal data in the event of your death.
+
+
+ You may exercise these rights by writing by email to:{" "}
+
+ account@arkproject.dev
+
+ . You may also make a complaint to your supervisory authority in
+ the event that you consider that the processing of personal data
+ does not comply with the applicable regulations.
+
+
+ Cookies. Most web browsers are set to accept cookies by
+ default. If you prefer, you can usually choose to set your browser
+ to remove or reject browser cookies. Removing or rejecting browser
+ cookies does not necessarily affect third party cookies used in
+ connection with the ArkProject Services. Please note that if you
+ choose to remove or reject cookies, this could affect the
+ availability and functionality of the ArkProject Services.
+
+
+
+ Retention
+
+
+ We will only store your personal information as long as necessary
+ for the purposes specified in 2. above.
+
+
+
+ Changes to this Privacy Policy
+
+
+ {`We may update this privacy policy from time to time. The updated
+ policy will be posted on this page with a new "Last Updated" date.`}
+
+
+
+ Contact Us
+
+
+ If you have any questions about this privacy policy, please
+ contact us at{" "}
+
+ account@arkproject.dev
+
+ .
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/(routes)/providers.tsx b/apps/web/src/app/(routes)/providers.tsx
index 324162bf..493dade0 100644
--- a/apps/web/src/app/(routes)/providers.tsx
+++ b/apps/web/src/app/(routes)/providers.tsx
@@ -1,38 +1,65 @@
"use client";
-import { StarknetConfig } from "@starknet-react/core";
+import {
+ type Chain,
+ // goerli as starknetGoerli,
+ mainnet as starknetMainnet,
+} from "@starknet-react/chains";
+import {
+ StarknetConfig,
+ jsonRpcProvider,
+ // publicProvider,
+} from "@starknet-react/core";
import { ThemeProvider } from "next-themes";
-import { WagmiConfig, configureChains, createConfig } from "wagmi";
-import { goerli } from "wagmi/chains";
-import { publicProvider } from "wagmi/providers/public";
+import { WagmiProvider, createConfig, http } from "wagmi";
+import { mainnet } from "wagmi/chains";
+import { WalletModalsProvider } from "../_components/WalletModals/WalletModalsContext";
import {
ethereumConnectors,
starknetConnectors,
} from "../_lib/utils/connectors";
-const { publicClient, webSocketPublicClient } = configureChains(
- [goerli],
- [publicProvider()]
-);
-
const wagmiConfig = createConfig({
- autoConnect: true,
+ // chains: [goerli],
+ chains: [mainnet],
connectors: ethereumConnectors,
- publicClient,
- webSocketPublicClient,
+ ssr: true,
+ transports: {
+ [mainnet.id]: http(
+ process.env.NEXT_PUBLIC_ALCHEMY_ETHEREUM_RPC_ENDPOINT ?? ""
+ ),
+ },
});
+function starknetRpc(chain: Chain) {
+ if (chain.network === "goerli")
+ return { nodeUrl: `https://juno.testnet.arkproject.dev/` };
+
+ return { nodeUrl: `https://juno.mainnet.arkproject.dev/` };
+}
+
+// const starknetProvider = publicProvider();
+
interface ProvidersProps {
children: React.ReactNode;
}
export default function Providers({ children }: ProvidersProps) {
return (
-
-
- {children}
-
+
+
+
+ {children}
+
+
);
}
diff --git a/apps/web/src/app/(routes)/terms-of-use/page.tsx b/apps/web/src/app/(routes)/terms-of-use/page.tsx
new file mode 100644
index 00000000..e3ad91c1
--- /dev/null
+++ b/apps/web/src/app/(routes)/terms-of-use/page.tsx
@@ -0,0 +1,522 @@
+import { Typography } from "design-system";
+
+import Footer from "~/app/_components/Footer";
+import MainPageContainer from "~/app/_components/MainPageContainer";
+
+export default function TermsOfUsePage() {
+ return (
+ <>
+
+
+
+
+ Terms of use
+
+
+ Last updated on March 12, 2024
+
+
+
+
+
+ Introduction to ArkProject Bridge
+
+
+ {`The ArkProject Bridge (the "Bridge"), located at
+ https://bridge.arkproject.dev/, allows users to connect their
+ digital wallets for the purpose of transferring NFTs between the
+ Ethereum Blockchain and the Starknet Blockchain.`}
+
+
+
+ Introduction to ArkProject Leaderboard
+
+
+ {`The ArkProject LeaderBoard (the "LeaderBoard"), located at [insert URL] allows users to validate Ark quest to gather points and get rewards (to be determined and announced at a later stage). `}
+
+
+
+ Scope and acceptance of the Terms
+
+
+ These Terms of Use (the “Terms,”) govern your relationship with
+ Screenshot Labs (or “us”, “we”) for the use of the Bridge and/or
+ the LeaderBoard and the websites operated by us and located at{" "}
+
+ https://www.bridge.arkproject.dev/
+ {" "}
+ or [insert URL] or otherwise interact with the services or tools
+ we provide (together the “ArkProject Services”).
+
+
+ Please read these Terms and our Privacy Policy located at{" "}
+
+ https://bridge.arkproject.dev/privacy
+ {" "}
+ and [insert URL for Leaderboard] carefully.
+
+
+ You warrant that you are of legal age to enter into these Terms
+ and are legally permitted to use services that involve digital
+ asset transactions in your jurisdiction.
+
+
+ By using the ArkProject Services, you agree to be bound by these
+ Terms. If you disagree with any part of these Terms, you must not
+ use the ArkProject Services.
+
+
+
+ Amendment to the Terms
+
+
+ You are advised to check these Terms periodically as they may be
+ updated. Changes will take effect immediately from posting of the
+ revised Terms our websites.
+
+
+ You agree that continued use of the ArkProject Services shall
+ constitute acceptance of such amendments.
+
+
+
+ Conditions to use the ArkProject Services
+
+
+ To use the ArkProject Services, you must connect a digital wallet
+ supported. You are responsible for maintaining the security of
+ your wallet and any transactions made through it.
+
+
+ You agree to comply with all applicable laws and regulations in
+ your use of the ArkProject Services.
+
+
+
+ Changes to the ArkProject Services
+
+
+ We reserve the right to modify, suspend, or discontinue the
+ ArkProject Services at any time without notice or liability.
+
+
+ If the Bridge is suspended or discontinued, you acknowledge that
+ you no longer will be able to transfer your NFTs between Ethereum
+ (L1) and Starknet (L2) by using the Bridge. You will have to find
+ alternative solutions to transfer your NFTs otherwise they will
+ remain on the blockchain where they were located at the moment of
+ suspension and discontinuance.
+
+
+ If the LeaderBoard is suspended or discontinued, you acknowledge
+ that you no longer be able to validate Ark quests and that you
+ will lose your points and rewards.
+
+
+ We will do our best effort to make a general announcement and give
+ prior notice of suspension or discontinuance.
+
+
+ Access to the ArkProject Services is provided on a “as is” and “as
+ available” basis only. We do not guarantee that the ArkProject
+ Services, or any content on our websites will always be available
+ or uninterrupted.
+
+
+ From time to time, access may be interrupted, suspended or
+ restricted, including because of a fault, error or unforeseen
+ circumstances or because we are carrying out planned maintenance.
+
+
+ We may also suspend or disable your access to the ArkProject
+ Services if we consider it reasonable to do so, e.g. you breach
+ these Terms.
+
+
+
+ Acknowledgment on consequences and risks of the use of the Bridge
+
+
+ Transactions facilitated by the Bridge are irreversible and
+ recorded on the respective blockchains. You acknowledge the public
+ nature of blockchain transactions and the associated risks.
+
+
+ You understand the inherent risks associated with using blockchain
+ technology and digital assets, including but not limited to,
+ volatility risks, regulatory uncertainty, and technological risks.
+ You acknowledge that the use of the Bridge is entirely at your own
+ risk.
+
+
+
+ Free Service
+
+
+ The ArkProject Services are provided to you by Screenshot Labs as
+ free services.
+
+
+ You are however responsible for all network fees and any other
+ costs associated with using the Bridge or making transactions on
+ the blockchain.
+
+
+
+ Ark Quests Leaderboard
+
+
+ To validate quests relating to your use of the Bridge, you will
+ need to connect the same digital wallets as the ones you used to
+ bridge your NFTs.
+
+
+ Use of the Leaderboard may require you to connect through X (ex.
+ Twitter) and to tweet on your use of the Bridge or any other
+ ArkProject Services.
+
+
+
+ Prohibited behaviour
+
+
+ You agree to access and use the ArkProject Services only for its
+ intended purpose and will not attempt to:
+
+
+ 1. Hack, make unauthorized alterations to, gain unauthorized
+ access to, or introduce any kind of malicious code to the
+ ArkProject Services by any means;
+
+
+ 2. Use the ArkProject Services for any purpose that is unlawful;
+
+
+ 3. Use the ArkProject Services in any manner that disrupts its
+ operation;
+
+
+ 4. Disguise or interfere in any way with the IP address of the
+ computer you are using to access the ArkProject Services or
+ otherwise take steps to prevent us from correctly identifying the
+ actual IP address of the computer you are using whilst using the
+ ArkProject Services.
+
+
+ You acknowledge that your use of the Bridge contains certain
+ risks, including without limitation the risks that the ArkProject
+ Services may be suspended or terminated for any or no reason.
+ Accordingly, you expressly agree that you assume all risk in
+ connection with your access and use of the ArkProject Services.
+
+
+
+ Third party links
+
+
+ {`The ArkProject Services may contain hyperlinks or references to
+ third party websites. Any such hyperlinks or references are
+ provided for your information and convenience only. We have no
+ control over third party websites and accept no legal
+ responsibility for any content, material or information contained
+ in them. The display of any hyperlink and reference to any
+ third-party website does not mean that we endorse that third
+ party's website, products or services. Your use of a third-party
+ site may be governed by the terms and conditions of that
+ third-party site.`}
+
+
+
+ Privacy Policy
+
+
+ The ArkProject Services may collect personal data relating to you.
+ You can find more information about how we will process your
+ personal data in our Privacy Policy.
+
+
+
+ Intellectual Property
+
+
+ We are the owner of all intellectual property rights in the
+ ArkProject Services. These works are protected by copyright laws
+ and all such rights are reserved. We grant you a worldwide,
+ non-exclusive, royalty-free, revocable license to use the
+ ArkProject Services for non commercial purposes.
+
+
+ Use of the ArkProject Services does not grant you any ownership in
+ the software or intellectual property of the ArkProject Services,
+ except for the limited rights to use the ArkProject Services as
+ specified in these Terms.
+
+
+ You agree not to copy our web pages or any content without our
+ prior consent. Any unauthorized use or reproduction may be
+ prosecuted.
+
+
+
+ Disclaimers
+
+
+
+ You agree that your use of the ArkProject Services will be at
+ your sole risk. To the fullest extent permitted by law, we
+ disclaim all warranties, express or implied, in connection with
+ the ArkProject Services and your use thereof.
+
+
+
+ The Bridge, Ethereum, Starknet and other blockchain networks are
+ decentralized systems that are still under active development, and
+ therefore:
+
+
+ (a) may contain bugs, errors and defects,
+
+ (b) may function improperly or be subject to periods of downtime
+ and unavailability,
+
+ (c) may result in total or partial loss or corruption of NFTs and
+ cryptocurrencies with respect to which they are used and/or data,
+
+ (d) may be modified at any time, including through the release of
+ subsequent versions, all with or without notice to you, or
+
+ (e) may have security vulnerabilities and be subject to hacking.
+
+
+ Screenshot Labs will not be liable or responsible for any losses
+ or damages to you, including without limitation any loss of NFTs
+ on Ethereum and/or Starknet with which you conduct your
+ transactions using the Bridge, as a result of any of the
+ foregoing.
+
+
+ {`The ArkProject Services are provided on an "as is" and "as
+ available" basis, as free services and therefore without any
+ warranty. We do not guarantee uninterrupted or error-free
+ operation of the ArkProject Services nor that the ArkProject
+ Services will be secure or free from bugs or viruses. You are
+ responsible for checking their security and performance before
+ using them and for using your own virus protection software.`}
+
+
+
+ Miscellaneous
+
+
+
+ -
+ We may perform any of our obligations, and exercise any of the
+ rights granted to us under these Terms, through a third-party.
+ We may assign any or all our rights and obligations under
+ these Terms to any third-party.
+
+ -
+ If any of the provisions in these Terms are found to be
+ illegal, invalid or unenforceable by any court of competent
+ jurisdiction, the remainder shall continue in full force and
+ effect.
+
+ -
+ All disclaimers, indemnities and exclusions in these Terms
+ shall survive termination of the Terms and shall continue to
+ apply during any suspension or any period during which the
+ Services are not available for you to use for any reason
+ whatsoever.
+
+ -
+ These Terms and the documents referred to in them set out the
+ entire agreement between you and us with respect to your use
+ of the Services and supersede any and all prior or
+ contemporaneous representations, communications or agreements
+ (written or oral) made between you or us.
+
+
+
+
+
+ Governing Law and Dispute Resolution
+
+
+ The applicable law shall be French law. Any dispute, controversy,
+ or claim arising out of or in relation to these Terms, including
+ the validity, invalidity, breach or termination thereof, shall be
+ submitted to the competent Court of Paris (France), except if
+ otherwise provided by applicable law.
+
+
+
+ Contacting us
+
+
+ Should you have any questions about these Terms, or wish to
+ contact us for any reason whatsoever, please contact us by sending
+ an email to{" "}
+
+ account@arkproject.dev
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/_components/BridgeCountIndicator.tsx b/apps/web/src/app/_components/BridgeCountIndicator.tsx
deleted file mode 100644
index a5d69a54..00000000
--- a/apps/web/src/app/_components/BridgeCountIndicator.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Typography } from "design-system";
-import { usePathname } from "next/navigation";
-
-import useNftSelection from "../(routes)/bridge/_hooks/useNftSelection";
-import { useIsSSR } from "../_hooks/useIsSSR";
-
-export default function BrigetCountIndicator() {
- const { selectedNftIds } = useNftSelection();
-
- const isSSR = useIsSSR();
-
- const pathname = usePathname();
- return (
-
-
-
- {!isSSR && selectedNftIds.length > 0 && pathname === "/bridge" && (
-
- {selectedNftIds.length}
-
- )}
-
- );
-}
diff --git a/apps/web/src/app/_components/CollectionNftsEmptyState.tsx b/apps/web/src/app/_components/CollectionNftsEmptyState.tsx
new file mode 100644
index 00000000..71df714d
--- /dev/null
+++ b/apps/web/src/app/_components/CollectionNftsEmptyState.tsx
@@ -0,0 +1,1654 @@
+interface NftsEmptyStateProps {
+ className?: string;
+}
+
+export default function CollectionNftsEmptyState({
+ className,
+}: NftsEmptyStateProps) {
+ return (
+
+
+
+
+
+
+
+ {/* DARK */}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/_components/ConditionalWrapper.tsx b/apps/web/src/app/_components/ConditionalWrapper.tsx
index fb171e4f..6ab965aa 100644
--- a/apps/web/src/app/_components/ConditionalWrapper.tsx
+++ b/apps/web/src/app/_components/ConditionalWrapper.tsx
@@ -1,6 +1,8 @@
+import { type ReactElement } from "react";
+
interface ConditionalWrapperProps {
children: React.ReactNode;
- wrapper: (children: React.ReactNode) => React.ReactNode;
+ wrapper: (children: React.ReactNode) => ReactElement;
}
export default function ConditionalWrapper({
diff --git a/apps/web/src/app/_components/ConnectEthereumButton.tsx b/apps/web/src/app/_components/ConnectEthereumButton.tsx
index b32c8f59..699566e2 100644
--- a/apps/web/src/app/_components/ConnectEthereumButton.tsx
+++ b/apps/web/src/app/_components/ConnectEthereumButton.tsx
@@ -10,19 +10,12 @@ import {
DEFAULT_ETHEREUM_CONNECTOR_LOGO,
WALLET_LOGOS_BY_ID,
} from "../_lib/utils/connectors";
-import ConnectModal from "./ConnectModal";
+import { useConnectModals } from "./WalletModals/WalletModalsContext";
-interface ConnectEthereumButtonProps {
- isModalOpen: boolean;
- onOpenModalChange: (open: boolean) => void;
-}
-
-export default function ConnectEthereumButton({
- isModalOpen,
- onOpenModalChange,
-}: ConnectEthereumButtonProps) {
+export default function ConnectEthereumButton() {
const isSSR = useIsSSR();
const { address, connector, isConnected } = useAccount();
+ const { toggleConnectEthereumWalletModal } = useConnectModals();
const { data: ensName } = useEnsName({
address: address,
});
@@ -39,10 +32,10 @@ export default function ConnectEthereumButton({
return (
<>
-
>
);
}
diff --git a/apps/web/src/app/_components/ConnectModal.tsx b/apps/web/src/app/_components/ConnectModal.tsx
deleted file mode 100644
index de69232e..00000000
--- a/apps/web/src/app/_components/ConnectModal.tsx
+++ /dev/null
@@ -1,247 +0,0 @@
-import * as RUIDialog from "@radix-ui/react-dialog";
-import {
- useAccount as useStarknetAccount,
- useBalance as useStarknetBalance,
-} from "@starknet-react/core";
-import { Dialog, Typography } from "design-system";
-import Image from "next/image";
-import { useEffect, useState } from "react";
-import {
- useAccount as useEthereumAccount,
- useBalance as useEthereumBalance,
-} from "wagmi";
-
-import useAccountFromChain from "../_hooks/useAccountFromChain";
-import useConnectFromChain from "../_hooks/useConnectFromChain";
-import useDisconnectFromChain from "../_hooks/useDisconnectFromChain";
-import {
- CHAIN_LOGOS_BY_NAME,
- CHAIN_WALLET_ILLUSTRATION_BY_NAME,
- CONNECTOR_LABELS_BY_ID,
- WALLET_LOGOS_BY_ID,
-} from "../_lib/utils/connectors";
-import { type Chain } from "../_types";
-
-interface ChainButtonProps {
- chain: Chain;
- onClick: () => void;
-}
-
-function ChainButton({ chain, onClick }: ChainButtonProps) {
- const { isConnected, shortAddress } = useAccountFromChain(chain);
-
- return (
-
- );
-}
-
-interface ConnectorButtonProps {
- id: string;
- onClick: () => void;
-}
-
-function ConnectorButton({ id, onClick }: ConnectorButtonProps) {
- return (
-
- );
-}
-
-interface ConnectorListProps {
- chain: Chain;
-}
-
-function ConnectorList({ chain }: ConnectorListProps) {
- const { address, isConnected, shortAddress } = useAccountFromChain(chain);
- const { connectors } = useConnectFromChain(chain);
- const { disconnect } = useDisconnectFromChain(chain);
-
- const { data: ethEthereumBalance } = useEthereumBalance({ address });
- const { data: ethStarknetBalance } = useStarknetBalance({ address });
-
- const ethBalance =
- chain === "Ethereum" ? ethEthereumBalance : ethStarknetBalance;
-
- return isConnected ? (
- <>
-
-
-
- {chain} Wallet
-
-
- {ethBalance?.formatted
- ? `${parseFloat(ethBalance.formatted).toFixed(4)} ETH`
- : null}
-
-
- {shortAddress}
-
-
-
- >
- ) : (
- <>
-
-
- Choose your {chain} wallet
-
-
- {connectors.map((connector) => {
- return (
- connector.connect()}
- />
- );
- })}
-
- >
- );
-}
-
-interface ConnectModalProps {
- /* Whether the modal should directly show specific chain connectors or ask the user to chose the chain first */
- initialChain?: Chain;
- isOpen: boolean;
- onOpenChange: (open: boolean) => void;
-}
-
-export default function ConnectModal({
- initialChain,
- isOpen,
- onOpenChange,
-}: ConnectModalProps) {
- const [displayedChain, setDisplayedChain] = useState(
- initialChain
- );
-
- function onWalletConnect() {
- if (initialChain === undefined) {
- setDisplayedChain(undefined);
- return;
- }
- onOpenChange(false);
- }
-
- useEthereumAccount({
- onConnect() {
- onWalletConnect();
- },
- });
-
- // TODO: Use onConnect like in the ethereum version
- const { address } = useStarknetAccount();
-
- useEffect(() => {
- if (address !== undefined) {
- onWalletConnect();
- }
- }, [address]);
-
- return (
-
- );
-}
diff --git a/apps/web/src/app/_components/ConnectStarkNetButton.tsx b/apps/web/src/app/_components/ConnectStarkNetButton.tsx
index 4c0ee30b..8209645b 100644
--- a/apps/web/src/app/_components/ConnectStarkNetButton.tsx
+++ b/apps/web/src/app/_components/ConnectStarkNetButton.tsx
@@ -1,4 +1,4 @@
-import { useAccount } from "@starknet-react/core";
+import { useAccount, useStarkName } from "@starknet-react/core";
import { Typography } from "design-system";
import Image from "next/image";
import { useMemo } from "react";
@@ -10,19 +10,14 @@ import {
DEFAULT_STARKNET_CONNECTOR_LOGO,
WALLET_LOGOS_BY_ID,
} from "../_lib/utils/connectors";
-import ConnectModal from "./ConnectModal";
+import { useConnectModals } from "./WalletModals/WalletModalsContext";
-interface ConnectStarknetButtonProps {
- isModalOpen: boolean;
- onOpenModalChange: (open: boolean) => void;
-}
-
-export default function ConnectStarknetButton({
- isModalOpen,
- onOpenModalChange,
-}: ConnectStarknetButtonProps) {
+export default function ConnectStarknetButton() {
const isSSR = useIsSSR();
const { address, connector, isConnected } = useAccount();
+ const { toggleConnectStarknetWalletModal } = useConnectModals();
+
+ const { data: starkName } = useStarkName({ address });
const shortAddress = useMemo(
() => (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ""),
@@ -36,11 +31,11 @@ export default function ConnectStarknetButton({
return (
<>
-
>
);
}
diff --git a/apps/web/src/app/_components/ConnectWalletsButton.tsx b/apps/web/src/app/_components/ConnectWalletsButton.tsx
index e49c40a0..3ec334b4 100644
--- a/apps/web/src/app/_components/ConnectWalletsButton.tsx
+++ b/apps/web/src/app/_components/ConnectWalletsButton.tsx
@@ -1,61 +1,49 @@
"use client";
-
+import { useAccount as useStarknetAccount } from "@starknet-react/core";
import { Typography } from "design-system";
-import { useState } from "react";
+import { useAccount as useEthereumAccount } from "wagmi";
-import ConnectModal from "./ConnectModal";
+import { useConnectModals } from "./WalletModals/WalletModalsContext";
export default function ConnectWalletsButton() {
- const [isModalOpen, setIsModalOpen] = useState(false);
+ const {
+ toggleConnectEthereumWalletModal,
+ toggleConnectStarknetWalletModal,
+ toggleConnectWalletsModal,
+ } = useConnectModals();
+ const { address: ethereumAddress } = useEthereumAccount();
+
+ const { address: starknetAddress } = useStarknetAccount();
- function openModal() {
- setIsModalOpen(true);
+ function toggleConnectModal() {
+ if (starknetAddress === undefined && ethereumAddress === undefined) {
+ toggleConnectWalletsModal();
+ return;
+ }
+ if (starknetAddress === undefined) {
+ toggleConnectStarknetWalletModal();
+ return;
+ }
+ if (ethereumAddress === undefined) {
+ toggleConnectEthereumWalletModal();
+ return;
+ }
+ toggleConnectWalletsModal;
}
return (
<>
- setIsModalOpen(open)}
- />
>
);
}
diff --git a/apps/web/src/app/_components/DarkModeButton.tsx b/apps/web/src/app/_components/DarkModeButton.tsx
index d73fbcc5..ea33fc80 100644
--- a/apps/web/src/app/_components/DarkModeButton.tsx
+++ b/apps/web/src/app/_components/DarkModeButton.tsx
@@ -1,14 +1,16 @@
-import Image from "next/image";
+"use client";
+
+import { DarkModeIcon, LightModeIcon } from "design-system";
import { useTheme } from "next-themes";
import { useIsSSR } from "~/app/_hooks/useIsSSR";
export default function DarkModeButton() {
const isSSR = useIsSSR();
- const { setTheme, theme } = useTheme();
+ const { resolvedTheme, setTheme } = useTheme();
function toggleTheme() {
- if (theme === "light") {
+ if (resolvedTheme === "light") {
setTheme("dark");
return;
}
@@ -20,15 +22,8 @@ export default function DarkModeButton() {
}
return (
-