Skip to content

Commit df8e5a4

Browse files
committed
add claim rewards page
1 parent 3c745cc commit df8e5a4

File tree

7 files changed

+269
-1
lines changed

7 files changed

+269
-1
lines changed

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadata.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type ContractPageMetadata = {
2525
isAccount: boolean;
2626
isAccountPermissionsSupported: boolean;
2727
functionSelectors: string[];
28+
showClaimRewards: boolean;
2829
};
2930

3031
export async function getContractPageMetadata(contract: ThirdwebContract) {

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadataSetup.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { ThirdwebContract } from "thirdweb";
2+
import { getDeployedEntrypointERC20 } from "thirdweb/assets";
23
import { contractType as getContractType } from "thirdweb/extensions/thirdweb";
34
import { resolveFunctionSelectors } from "@/lib/selectors";
5+
import { getValidReward } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/rewards";
46
import {
57
isERC20ClaimConditionsSupported,
68
isERC721ClaimConditionsSupported,
@@ -43,6 +45,7 @@ type ContractPageMetadata = {
4345
isAccount: boolean;
4446
isAccountPermissionsSupported: boolean;
4547
functionSelectors: string[];
48+
showClaimRewards: boolean;
4649
};
4750

4851
export async function getContractPageMetadataSetup(
@@ -53,10 +56,14 @@ export async function getContractPageMetadataSetup(
5356
functionSelectorsResult,
5457
isInsightSupportedResult,
5558
contractTypeResult,
59+
claimRewardResult,
5660
] = await Promise.allSettled([
5761
resolveFunctionSelectors(contract),
5862
isAnalyticsSupportedFn(contract.chain.id),
5963
getContractType({ contract }),
64+
isClaimRewardsSupported({
65+
assetContract: contract,
66+
}),
6067
]);
6168

6269
const functionSelectors =
@@ -72,6 +79,11 @@ export async function getContractPageMetadataSetup(
7279
const contractType =
7380
contractTypeResult.status === "fulfilled" ? contractTypeResult.value : null;
7481

82+
const showClaimRewards =
83+
claimRewardResult.status === "fulfilled"
84+
? !!claimRewardResult.value
85+
: false;
86+
7587
return {
7688
embedType: getEmbedTypeToShow(functionSelectors),
7789
functionSelectors,
@@ -93,5 +105,30 @@ export async function getContractPageMetadataSetup(
93105
isSplitSupported: contractType === "Split",
94106
isVoteContract: contractType === "VoteERC20",
95107
supportedERCs: supportedERCs(functionSelectors),
108+
showClaimRewards,
96109
};
97110
}
111+
112+
async function isClaimRewardsSupported(params: {
113+
assetContract: ThirdwebContract;
114+
}): Promise<boolean> {
115+
try {
116+
const entrypointContract = await getDeployedEntrypointERC20({
117+
chain: params.assetContract.chain,
118+
client: params.assetContract.client,
119+
});
120+
121+
if (!entrypointContract) {
122+
return false;
123+
}
124+
125+
const reward = await getValidReward({
126+
assetContract: params.assetContract,
127+
entrypointContract,
128+
});
129+
130+
return !!reward;
131+
} catch {
132+
return false;
133+
}
134+
}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ export function getContractPageSidebarLinks(data: {
140140
href: `${layoutPrefix}/permissions`,
141141
label: "Permissions",
142142
},
143+
{
144+
exactMatch: true,
145+
hide: !data.metadata.showClaimRewards,
146+
href: `${layoutPrefix}/claim-rewards`,
147+
label: "Claim Rewards",
148+
},
143149
];
144150

145151
const extensionsToShow = extensionsLinks.filter((l) => !l.hide);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"use client";
2+
3+
import { useQuery } from "@tanstack/react-query";
4+
import { ArrowRightIcon } from "lucide-react";
5+
import { readContract, type ThirdwebContract } from "thirdweb";
6+
import { claimReward } from "thirdweb/assets";
7+
import { useSendAndConfirmTransaction } from "thirdweb/react";
8+
import { Button } from "@/components/ui/button";
9+
import { Spinner } from "@/components/ui/Spinner/Spinner";
10+
import { parseError } from "@/utils/errorParser";
11+
import type { getValidReward } from "../utils/rewards";
12+
13+
const maxInt128 = 2n ** (128n - 1n) - 1n;
14+
15+
export function ClaimRewardsPage(props: {
16+
assetContractClient: ThirdwebContract;
17+
entrypointContractClient: ThirdwebContract;
18+
reward: NonNullable<Awaited<ReturnType<typeof getValidReward>>>;
19+
rewardLockerContractClient: ThirdwebContract;
20+
v3PositionManagerContractClient: ThirdwebContract;
21+
}) {
22+
const sendAndConfirmTransaction = useSendAndConfirmTransaction();
23+
24+
async function handleClaim() {
25+
const tx = claimReward({
26+
asset: props.assetContractClient.address,
27+
contract: props.rewardLockerContractClient,
28+
});
29+
30+
await sendAndConfirmTransaction.mutateAsync(tx);
31+
}
32+
33+
const collectionQuery = useQuery({
34+
queryKey: [
35+
"unclaimed-fees",
36+
{
37+
...props,
38+
reward: {
39+
...props.reward,
40+
tokenId: props.reward?.tokenId.toString(),
41+
},
42+
},
43+
],
44+
queryFn: async () => {
45+
const result = await readContract({
46+
contract: props.v3PositionManagerContractClient,
47+
method:
48+
"function collect((uint256 tokenId,address recipient,uint128 amount0Max,uint128 amount1Max)) returns (uint256,uint256)",
49+
params: [
50+
{
51+
tokenId: props.reward.tokenId,
52+
recipient: props.reward.recipient,
53+
amount0Max: maxInt128,
54+
amount1Max: maxInt128,
55+
},
56+
],
57+
});
58+
59+
return result;
60+
},
61+
});
62+
63+
return (
64+
<div>
65+
<h2 className="font-semibold text-2xl tracking-tight mb-3">
66+
Claim Rewards
67+
</h2>
68+
69+
<div className="mb-4">
70+
{collectionQuery.isPending && (
71+
<div className="flex items-center gap-2">
72+
<Spinner className="size-4" />
73+
Loading unclaimed fees
74+
</div>
75+
)}
76+
{collectionQuery.error && (
77+
<div className="text-red-500">
78+
Failed to load unclaimed fees {parseError(collectionQuery.error)}
79+
</div>
80+
)}
81+
82+
{collectionQuery.data && (
83+
<div className="flex items-center gap-2">
84+
<p> Amount0: {collectionQuery.data[0]} </p>
85+
<p> Amount1: {collectionQuery.data[1]} </p>
86+
</div>
87+
)}
88+
</div>
89+
90+
<Button onClick={handleClaim} className="gap-2">
91+
Claim Rewards
92+
{sendAndConfirmTransaction.isPending ? (
93+
<Spinner className="size-4" />
94+
) : (
95+
<ArrowRightIcon className="size-4" />
96+
)}
97+
</Button>
98+
</div>
99+
);
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { notFound, redirect } from "next/navigation";
2+
import { getContract } from "thirdweb";
3+
import {
4+
getDeployedEntrypointERC20,
5+
getRewardLocker,
6+
v3PositionManager as getV3PositionManager,
7+
} from "thirdweb/assets";
8+
import { getProject } from "@/api/projects";
9+
import { getContractPageParamsInfo } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractFromParams";
10+
import type { ProjectContractPageParams } from "../types";
11+
import { ClaimRewardsPage } from "./components/claim-rewards-page";
12+
import { getValidReward } from "./utils/rewards";
13+
14+
export default async function Page(props: {
15+
params: Promise<ProjectContractPageParams>;
16+
}) {
17+
const params = await props.params;
18+
const project = await getProject(params.team_slug, params.project_slug);
19+
20+
if (!project) {
21+
notFound();
22+
}
23+
24+
const info = await getContractPageParamsInfo({
25+
chainIdOrSlug: params.chainIdOrSlug,
26+
contractAddress: params.contractAddress,
27+
teamId: project.teamId,
28+
});
29+
30+
if (!info) {
31+
notFound();
32+
}
33+
34+
const assetContractClient = info.clientContract;
35+
36+
const entrypointContractClient = await getDeployedEntrypointERC20({
37+
chain: assetContractClient.chain,
38+
client: assetContractClient.client,
39+
});
40+
41+
const reward = await getValidReward({
42+
assetContract: assetContractClient,
43+
entrypointContract: entrypointContractClient,
44+
});
45+
46+
const rewardLocker = await getRewardLocker({
47+
contract: entrypointContractClient,
48+
}).catch(() => null);
49+
50+
if (!reward || !rewardLocker) {
51+
redirect(
52+
`/team/${params.team_slug}/${params.project_slug}/contract/${params.chainIdOrSlug}/${params.contractAddress}`,
53+
);
54+
}
55+
56+
const rewardLockerContractClient = getContract({
57+
address: rewardLocker,
58+
chain: assetContractClient.chain,
59+
client: assetContractClient.client,
60+
});
61+
62+
const v3PositionManager = await getV3PositionManager({
63+
contract: rewardLockerContractClient,
64+
}).catch(() => null);
65+
66+
// const v4PositionManager = await getV4PositionManager({
67+
// contract: rewardLockerContractClient,
68+
// }).catch(() => null);
69+
70+
if (!v3PositionManager || v3PositionManager !== reward.positionManager) {
71+
redirect(
72+
`/team/${params.team_slug}/${params.project_slug}/contract/${params.chainIdOrSlug}/${params.contractAddress}`,
73+
);
74+
}
75+
76+
const v3PositionManagerContract = getContract({
77+
address: reward.positionManager,
78+
chain: assetContractClient.chain,
79+
client: assetContractClient.client,
80+
});
81+
82+
return (
83+
<ClaimRewardsPage
84+
assetContractClient={assetContractClient}
85+
entrypointContractClient={entrypointContractClient}
86+
rewardLockerContractClient={rewardLockerContractClient}
87+
reward={reward}
88+
v3PositionManagerContractClient={v3PositionManagerContract}
89+
/>
90+
);
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
2+
import { getReward } from "thirdweb/assets";
3+
4+
export async function getValidReward(params: {
5+
assetContract: ThirdwebContract;
6+
entrypointContract: ThirdwebContract;
7+
}) {
8+
try {
9+
const reward = await getReward({
10+
contract: params.entrypointContract,
11+
asset: params.assetContract.address,
12+
});
13+
14+
if (
15+
reward.positionManager === ZERO_ADDRESS ||
16+
reward.recipient === ZERO_ADDRESS ||
17+
reward.referrer === ZERO_ADDRESS ||
18+
reward.referrerBps === 0 ||
19+
reward.tokenId === BigInt(0)
20+
) {
21+
return null;
22+
}
23+
24+
return reward;
25+
} catch {
26+
return null;
27+
}
28+
}

packages/thirdweb/src/exports/assets.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,10 @@ export type {
1818
PoolConfig,
1919
TokenParams,
2020
} from "../assets/types.js";
21-
export { getInitBytecodeWithSalt } from "../utils/any-evm/get-init-bytecode-with-salt.js";
2221
export { getReward } from "../extensions/assets/__generated__/ERC20AssetEntrypoint/read/getReward.js";
22+
export { getRewardLocker } from "../extensions/assets/__generated__/ERC20AssetEntrypoint/read/getRewardLocker.js";
23+
export { claimReward } from "../extensions/assets/__generated__/ERC20AssetEntrypoint/write/claimReward.js";
24+
export { positions } from "../extensions/assets/__generated__/RewardLocker/read/positions.js";
25+
export { v3PositionManager } from "../extensions/assets/__generated__/RewardLocker/read/v3PositionManager.js";
26+
export { v4PositionManager } from "../extensions/assets/__generated__/RewardLocker/read/v4PositionManager.js";
27+
export { getInitBytecodeWithSalt } from "../utils/any-evm/get-init-bytecode-with-salt.js";

0 commit comments

Comments
 (0)