diff --git a/apps/dashboard/src/@/components/contract-components/tables/contract-table.tsx b/apps/dashboard/src/@/components/contract-components/tables/contract-table.tsx index 84f8e6e8f02..b4cb047a9e8 100644 --- a/apps/dashboard/src/@/components/contract-components/tables/contract-table.tsx +++ b/apps/dashboard/src/@/components/contract-components/tables/contract-table.tsx @@ -300,6 +300,7 @@ const contractTypeToAssetTypeRecord: Record = { DropERC20: "Coin", DropERC721: "NFT Collection", DropERC1155: "NFT Collection", + ERC20Asset: "Coin", }; const NetworkFilterCell = React.memo(function NetworkFilterCell({ diff --git a/apps/dashboard/src/@/components/ui/tabs.tsx b/apps/dashboard/src/@/components/ui/tabs.tsx index 47acea12b5d..a2239f181ef 100644 --- a/apps/dashboard/src/@/components/ui/tabs.tsx +++ b/apps/dashboard/src/@/components/ui/tabs.tsx @@ -98,6 +98,7 @@ export function TabButtons(props: { shadowColor?: string; tabIconClassName?: string; hideBottomLine?: boolean; + bottomLineClassName?: string; }) { const { containerRef, lineRef, activeTabRef } = useUnderline(); @@ -106,7 +107,12 @@ export function TabButtons(props: {
{/* Bottom line */} {!props.hideBottomLine && ( -
+
)} { const res = await apiServerProxy({ body: JSON.stringify({ diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadata.ts b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadata.ts index c95e928e05b..df9b49592c9 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadata.ts +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadata.ts @@ -25,6 +25,7 @@ export type ContractPageMetadata = { isAccount: boolean; isAccountPermissionsSupported: boolean; functionSelectors: string[]; + showClaimRewards: boolean; }; export async function getContractPageMetadata(contract: ThirdwebContract) { diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadataSetup.ts b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadataSetup.ts index 5785ed02e8a..9448e740484 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadataSetup.ts +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadataSetup.ts @@ -1,6 +1,8 @@ import type { ThirdwebContract } from "thirdweb"; +import { getDeployedEntrypointERC20 } from "thirdweb/assets"; import { contractType as getContractType } from "thirdweb/extensions/thirdweb"; import { resolveFunctionSelectors } from "@/lib/selectors"; +import { getValidReward } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/rewards"; import { isERC20ClaimConditionsSupported, isERC721ClaimConditionsSupported, @@ -43,6 +45,7 @@ type ContractPageMetadata = { isAccount: boolean; isAccountPermissionsSupported: boolean; functionSelectors: string[]; + showClaimRewards: boolean; }; export async function getContractPageMetadataSetup( @@ -53,10 +56,14 @@ export async function getContractPageMetadataSetup( functionSelectorsResult, isInsightSupportedResult, contractTypeResult, + claimRewardResult, ] = await Promise.allSettled([ resolveFunctionSelectors(contract), isAnalyticsSupportedFn(contract.chain.id), getContractType({ contract }), + isClaimRewardsSupported({ + assetContract: contract, + }), ]); const functionSelectors = @@ -72,6 +79,11 @@ export async function getContractPageMetadataSetup( const contractType = contractTypeResult.status === "fulfilled" ? contractTypeResult.value : null; + const showClaimRewards = + claimRewardResult.status === "fulfilled" + ? !!claimRewardResult.value + : false; + return { embedType: getEmbedTypeToShow(functionSelectors), functionSelectors, @@ -93,5 +105,30 @@ export async function getContractPageMetadataSetup( isSplitSupported: contractType === "Split", isVoteContract: contractType === "VoteERC20", supportedERCs: supportedERCs(functionSelectors), + showClaimRewards, }; } + +async function isClaimRewardsSupported(params: { + assetContract: ThirdwebContract; +}): Promise { + try { + const entrypointContract = await getDeployedEntrypointERC20({ + chain: params.assetContract.chain, + client: params.assetContract.client, + }); + + if (!entrypointContract) { + return false; + } + + const reward = await getValidReward({ + assetContract: params.assetContract, + entrypointContract, + }); + + return !!reward; + } catch { + return false; + } +} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts index c6e714628d9..fedcce8af48 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts @@ -140,6 +140,12 @@ export function getContractPageSidebarLinks(data: { href: `${layoutPrefix}/permissions`, label: "Permissions", }, + { + exactMatch: true, + hide: !data.metadata.showClaimRewards, + href: `${layoutPrefix}/claim-rewards`, + label: "Claim Rewards", + }, ]; const extensionsToShow = extensionsLinks.filter((l) => !l.hide); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_apis/create-token-on-bridge.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_apis/create-token-on-bridge.ts new file mode 100644 index 00000000000..2d7eee9fef3 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_apis/create-token-on-bridge.ts @@ -0,0 +1,23 @@ +import type { ThirdwebClient } from "thirdweb"; +import { isProd } from "@/constants/env-utils"; + +export async function createTokenOnUniversalBridge(params: { + chainId: number; + tokenAddress: string; + client: ThirdwebClient; +}) { + const domain = isProd ? "thirdweb.com" : "thirdweb-dev.com"; + const res = await fetch(`https://bridge.${domain}/v1/tokens`, { + body: JSON.stringify({ + chainId: params.chainId.toString(), + tokenAddress: params.tokenAddress, + }), + headers: { + "Content-Type": "application/json", + "x-client-id": params.client.clientId, + }, + method: "POST", + }); + + return res; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/step-card.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/step-card.tsx index fa14752c80f..316ee8b824f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/step-card.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/step-card.tsx @@ -35,7 +35,7 @@ export function StepCard(props: { {props.children} {(props.prevButton || props.nextButton) && ( -
+
{props.prevButton && ( +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/page.tsx new file mode 100644 index 00000000000..5cd5d7de07e --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/page.tsx @@ -0,0 +1,91 @@ +import { notFound, redirect } from "next/navigation"; +import { getContract } from "thirdweb"; +import { + getDeployedEntrypointERC20, + getRewardLocker, + v3PositionManager as getV3PositionManager, +} from "thirdweb/assets"; +import { getProject } from "@/api/projects"; +import { getContractPageParamsInfo } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractFromParams"; +import type { ProjectContractPageParams } from "../types"; +import { ClaimRewardsPage } from "./components/claim-rewards-page"; +import { getValidReward } from "./utils/rewards"; + +export default async function Page(props: { + params: Promise; +}) { + const params = await props.params; + const project = await getProject(params.team_slug, params.project_slug); + + if (!project) { + notFound(); + } + + const info = await getContractPageParamsInfo({ + chainIdOrSlug: params.chainIdOrSlug, + contractAddress: params.contractAddress, + teamId: project.teamId, + }); + + if (!info) { + notFound(); + } + + const assetContractClient = info.clientContract; + + const entrypointContractClient = await getDeployedEntrypointERC20({ + chain: assetContractClient.chain, + client: assetContractClient.client, + }); + + const reward = await getValidReward({ + assetContract: assetContractClient, + entrypointContract: entrypointContractClient, + }); + + const rewardLocker = await getRewardLocker({ + contract: entrypointContractClient, + }).catch(() => null); + + if (!reward || !rewardLocker) { + redirect( + `/team/${params.team_slug}/${params.project_slug}/contract/${params.chainIdOrSlug}/${params.contractAddress}`, + ); + } + + const rewardLockerContractClient = getContract({ + address: rewardLocker, + chain: assetContractClient.chain, + client: assetContractClient.client, + }); + + const v3PositionManager = await getV3PositionManager({ + contract: rewardLockerContractClient, + }).catch(() => null); + + // const v4PositionManager = await getV4PositionManager({ + // contract: rewardLockerContractClient, + // }).catch(() => null); + + if (!v3PositionManager || v3PositionManager !== reward.positionManager) { + redirect( + `/team/${params.team_slug}/${params.project_slug}/contract/${params.chainIdOrSlug}/${params.contractAddress}`, + ); + } + + const v3PositionManagerContract = getContract({ + address: reward.positionManager, + chain: assetContractClient.chain, + client: assetContractClient.client, + }); + + return ( + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/rewards.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/rewards.ts new file mode 100644 index 00000000000..7c6283d0100 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/rewards.ts @@ -0,0 +1,28 @@ +import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb"; +import { getReward } from "thirdweb/assets"; + +export async function getValidReward(params: { + assetContract: ThirdwebContract; + entrypointContract: ThirdwebContract; +}) { + try { + const reward = await getReward({ + contract: params.entrypointContract, + asset: params.assetContract.address, + }); + + if ( + reward.positionManager === ZERO_ADDRESS || + reward.recipient === ZERO_ADDRESS || + reward.referrer === ZERO_ADDRESS || + reward.referrerBps === 0 || + reward.tokenId === BigInt(0) + ) { + return null; + } + + return reward; + } catch { + return null; + } +} diff --git a/packages/thirdweb/src/exports/assets.ts b/packages/thirdweb/src/exports/assets.ts index a2cbef1ae7a..71967483f99 100644 --- a/packages/thirdweb/src/exports/assets.ts +++ b/packages/thirdweb/src/exports/assets.ts @@ -18,5 +18,10 @@ export type { PoolConfig, TokenParams, } from "../assets/types.js"; -export { getInitBytecodeWithSalt } from "../utils/any-evm/get-init-bytecode-with-salt.js"; export { getReward } from "../extensions/assets/__generated__/ERC20AssetEntrypoint/read/getReward.js"; +export { getRewardLocker } from "../extensions/assets/__generated__/ERC20AssetEntrypoint/read/getRewardLocker.js"; +export { claimReward } from "../extensions/assets/__generated__/ERC20AssetEntrypoint/write/claimReward.js"; +export { positions } from "../extensions/assets/__generated__/RewardLocker/read/positions.js"; +export { v3PositionManager } from "../extensions/assets/__generated__/RewardLocker/read/v3PositionManager.js"; +export { v4PositionManager } from "../extensions/assets/__generated__/RewardLocker/read/v4PositionManager.js"; +export { getInitBytecodeWithSalt } from "../utils/any-evm/get-init-bytecode-with-salt.js";