Skip to content

Commit ad61b64

Browse files
committed
[Update] Refactor ERC4337 extension detection and UI (#4378)
### TL;DR Enhanced ERC4337 support with new functions and improved contract detection. ### What changed? - Added new ERC4337 extension functions for account management and permissions. - Implemented support checks for various ERC4337 functions. - Updated contract UI hooks to use new detection methods for ERC4337 features. - Removed redundant code and simplified contract UI components. - Improved type safety and consistency across ERC4337-related functions. ### How to test? 1. Test the new ERC4337 functions in a development environment. 2. Verify that the contract UI correctly detects and displays ERC4337 features. 3. Check that account management, permissions, and factory functionalities work as expected. 4. Ensure that the support checks for ERC4337 functions return correct results. ### Why make this change? This change improves the detection and support for ERC4337 smart contract accounts. It enhances the developer experience by providing more granular control over account features and permissions. The updates also streamline the codebase, making it easier to maintain and extend ERC4337 functionality in the future. --- <!-- start pr-codex --> --- ## PR-Codex overview This PR updates account permissions and extensions in the dashboard. ### Detailed summary - Updated imports and components in `AccountPermissionsPage`, `AccountsPage`, and `AccountPage`. - Added support for `isAddAdminSupported`, `isAddSessionKeySupported`, `isRemoveSessionKeySupported` functions in ERC4337 extensions. - Added support for `isGetAllActiveSignersSupported`, `isGetAllAdminsSupported`, `isGetAllSignersSupported`, `isIsActiveSignerSupported` functions in account permissions. > The following files were skipped due to too many changes: `packages/thirdweb/src/exports/extensions/erc4337.ts`, `apps/dashboard/src/contract-ui/hooks/useRouteConfig.tsx` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 57fb7fc commit ad61b64

File tree

9 files changed

+176
-144
lines changed

9 files changed

+176
-144
lines changed

apps/dashboard/src/contract-ui/hooks/useRouteConfig.tsx

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import { useMemo } from "react";
99
import type { ThirdwebContract } from "thirdweb";
1010
import * as CommonExt from "thirdweb/extensions/common";
1111
import * as ERC20Ext from "thirdweb/extensions/erc20";
12-
import { isERC721 } from "thirdweb/extensions/erc721";
13-
import { isERC1155 } from "thirdweb/extensions/erc1155";
12+
import * as ERC721Ext from "thirdweb/extensions/erc721";
13+
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
14+
import * as ERC4337Ext from "thirdweb/extensions/erc4337";
1415
import * as PermissionExt from "thirdweb/extensions/permissions";
15-
import { contractType } from "thirdweb/extensions/thirdweb";
16+
import * as ThirdwebExt from "thirdweb/extensions/thirdweb";
1617
import { useReadContract } from "thirdweb/react";
1718
import { useAnalyticsSupportedForChain } from "../../data/analytics/hooks";
1819
import { useContractFunctionSelectors } from "./useContractFunctionSelectors";
@@ -110,10 +111,12 @@ export function useContractRouteConfig(
110111
): EnhancedRoute[] {
111112
// new
112113
const functionSelectorQuery = useContractFunctionSelectors(contract);
113-
const contractTypeQuery = useReadContract(contractType, { contract });
114+
const contractTypeQuery = useReadContract(ThirdwebExt.contractType, {
115+
contract,
116+
});
114117
const analyticsSupported = useAnalyticsSupportedForChain(contract.chain.id);
115-
const isERC721Query = useReadContract(isERC721, { contract });
116-
const isERC1155Query = useReadContract(isERC1155, { contract });
118+
const isERC721Query = useReadContract(ERC721Ext.isERC721, { contract });
119+
const isERC1155Query = useReadContract(ERC1155Ext.isERC1155, { contract });
117120
const isERC20 = useMemo(
118121
() => ERC20Ext.isERC20(functionSelectorQuery.data),
119122
[functionSelectorQuery.data],
@@ -141,30 +144,39 @@ export function useContractRouteConfig(
141144
].every(Boolean);
142145
}, [isPermissions, functionSelectorQuery.data]);
143146

147+
const isAccount = useMemo(
148+
() => ERC4337Ext.isValidateUserOpSupported(functionSelectorQuery.data),
149+
[functionSelectorQuery.data],
150+
);
151+
152+
const accountPermissions = useMemo(() => {
153+
return [
154+
ERC4337Ext.isGetAllActiveSignersSupported(functionSelectorQuery.data),
155+
ERC4337Ext.isGetAllAdminsSupported(functionSelectorQuery.data),
156+
ERC4337Ext.isGetAllSignersSupported(functionSelectorQuery.data),
157+
ERC4337Ext.isIsActiveSignerSupported(functionSelectorQuery.data),
158+
ERC4337Ext.isIsAdminSupported(functionSelectorQuery.data),
159+
ERC4337Ext.isAddAdminSupported(functionSelectorQuery.data),
160+
].every(Boolean);
161+
}, [functionSelectorQuery.data]);
162+
163+
const accountFactory = useMemo(() => {
164+
return [
165+
ERC4337Ext.isGetAllAccountsSupported(functionSelectorQuery.data),
166+
ERC4337Ext.isGetAccountsSupported(functionSelectorQuery.data),
167+
ERC4337Ext.isTotalAccountsSupported(functionSelectorQuery.data),
168+
ERC4337Ext.isGetAccountsOfSignerSupported(functionSelectorQuery.data),
169+
ERC4337Ext.isPredictAccountAddressSupported(functionSelectorQuery.data),
170+
].every(Boolean);
171+
}, [functionSelectorQuery.data]);
172+
144173
// old
145174
const ensQuery = useEns(contract.address);
146175

147176
// TODO: remove
148177
const contractQuery = useContract(ensQuery.data?.address);
149178
// TODO: remove all below
150179
const contractData = useMemo(() => {
151-
// AccountPage
152-
const detectedAccountFeature = extensionDetectedState({
153-
contractQuery,
154-
feature: ["Account"],
155-
});
156-
157-
// AccountPermissionsPage
158-
const detectedAccountPermissionFeature = extensionDetectedState({
159-
contractQuery,
160-
feature: ["AccountPermissions", "AccountPermissionsV1"],
161-
});
162-
163-
const detectedAccountFactory = extensionDetectedState({
164-
contractQuery,
165-
feature: ["AccountFactory"],
166-
});
167-
168180
const detectedEnglishAuctions = extensionDetectedState({
169181
contractQuery,
170182
feature: "EnglishAuctions",
@@ -224,9 +236,6 @@ export function useContractRouteConfig(
224236

225237
return {
226238
claimconditionExtensionDetection,
227-
detectedAccountFeature,
228-
detectedAccountPermissionFeature,
229-
detectedAccountFactory,
230239
detectedEnglishAuctions,
231240
detectedDirectListings,
232241
detectedModularExtension,
@@ -419,36 +428,33 @@ export function useContractRouteConfig(
419428
{
420429
title: "Accounts",
421430
path: "accounts",
422-
isEnabled: contractData.detectedAccountFactory,
423-
component: () => (
424-
<LazyContractAccountsPage
425-
contract={contract}
426-
detectedAccountFactory={contractData.detectedAccountFactory}
427-
/>
428-
),
431+
isEnabled: accountFactory
432+
? "enabled"
433+
: functionSelectorQuery.isLoading
434+
? "loading"
435+
: "disabled",
436+
component: () => <LazyContractAccountsPage contract={contract} />,
429437
},
430438
{
431439
title: "Balance",
432440
path: "account",
433-
isEnabled: contractData.detectedAccountFeature,
434-
component: () => (
435-
<LazyContractAccountPage
436-
contract={contract}
437-
detectedAccountFeature={contractData.detectedAccountFeature}
438-
/>
439-
),
441+
isEnabled: isAccount
442+
? "enabled"
443+
: functionSelectorQuery.isLoading
444+
? "loading"
445+
: "disabled",
446+
component: () => <LazyContractAccountPage contract={contract} />,
440447
},
441448
{
442449
title: "Account Permissions",
443450
path: "account-permissions",
444-
isEnabled: contractData.detectedAccountPermissionFeature,
451+
isEnabled: accountPermissions
452+
? "enabled"
453+
: functionSelectorQuery.isLoading
454+
? "loading"
455+
: "disabled",
445456
component: () => (
446-
<LazyContractAccountPermissionsPage
447-
contract={contract}
448-
detectedPermissionFeature={
449-
contractData.detectedAccountPermissionFeature
450-
}
451-
/>
457+
<LazyContractAccountPermissionsPage contract={contract} />
452458
),
453459
},
454460
{

apps/dashboard/src/contract-ui/tabs/account-permissions/page.tsx

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,15 @@
1-
import { Box, Flex } from "@chakra-ui/react";
2-
import type { ExtensionDetectedState } from "components/buttons/ExtensionDetectButton";
1+
import { Flex } from "@chakra-ui/react";
32
import type { ThirdwebContract } from "thirdweb";
4-
import { Card, Heading, LinkButton, Text } from "tw-components";
3+
import { Heading } from "tw-components";
54
import { AccountSigners } from "./components/account-signers";
65

76
interface AccountPermissionsPageProps {
87
contract: ThirdwebContract;
9-
detectedPermissionFeature: ExtensionDetectedState;
108
}
119

1210
export const AccountPermissionsPage: React.FC<AccountPermissionsPageProps> = ({
1311
contract,
14-
detectedPermissionFeature,
1512
}) => {
16-
if (!detectedPermissionFeature) {
17-
return (
18-
<Card as={Flex} flexDir="column" gap={3}>
19-
{/* TODO extract this out into it's own component and make it better */}
20-
<Heading size="subtitle.md">No Account extension enabled</Heading>
21-
<Text>This contract is not a smart account.</Text>
22-
<Box>
23-
<LinkButton
24-
isExternal
25-
href="https://portal.thirdweb.com/contracts/build/extensions/erc-4337/SmartWallet"
26-
colorScheme="purple"
27-
>
28-
Learn more
29-
</LinkButton>
30-
</Box>
31-
</Card>
32-
);
33-
}
34-
3513
return (
3614
<Flex direction="column" gap={6}>
3715
<Flex direction="row" justify="space-between" align="center">

apps/dashboard/src/contract-ui/tabs/account/page.tsx

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,22 @@
11
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
2-
import { Box, Flex } from "@chakra-ui/react";
3-
import type { ExtensionDetectedState } from "components/buttons/ExtensionDetectButton";
2+
import { Flex } from "@chakra-ui/react";
43
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
54
import type { ThirdwebContract } from "thirdweb";
6-
import { Card, Heading, LinkButton, Text } from "tw-components";
5+
import { Heading } from "tw-components";
76
import { AccountBalance } from "./components/account-balance";
87
import { DepositNative } from "./components/deposit-native";
98
import { NftsOwned } from "./components/nfts-owned";
109

1110
interface AccountPageProps {
1211
contract: ThirdwebContract;
13-
detectedAccountFeature: ExtensionDetectedState;
1412
}
1513

16-
export const AccountPage: React.FC<AccountPageProps> = ({
17-
contract,
18-
detectedAccountFeature,
19-
}) => {
14+
export const AccountPage: React.FC<AccountPageProps> = ({ contract }) => {
2015
const configuredChainsRecord = useSupportedChainsRecord();
2116
const chainId = useDashboardEVMChainId();
2217
const chain = chainId ? configuredChainsRecord[chainId] : undefined;
2318
const symbol = chain?.nativeCurrency.symbol || "Native Token";
2419

25-
if (!detectedAccountFeature) {
26-
return (
27-
<Card as={Flex} flexDir="column" gap={3}>
28-
{/* TODO extract this out into it's own component and make it better */}
29-
<Heading size="subtitle.md">No Account extension enabled</Heading>
30-
<Text>This contract is not a smart account.</Text>
31-
<Box>
32-
<LinkButton
33-
isExternal
34-
href="https://portal.thirdweb.com/contracts/build/extensions/erc-4337/SmartWallet"
35-
colorScheme="purple"
36-
>
37-
Learn more
38-
</LinkButton>
39-
</Box>
40-
</Card>
41-
);
42-
}
4320
return (
4421
<Flex direction="column" gap={6}>
4522
<Flex direction="row" justify="space-between" align="center">

apps/dashboard/src/contract-ui/tabs/accounts/page.tsx

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,15 @@
1-
import { Box, ButtonGroup, Flex } from "@chakra-ui/react";
2-
import type { ExtensionDetectedState } from "components/buttons/ExtensionDetectButton";
1+
import { ButtonGroup, Flex } from "@chakra-ui/react";
32
import type { ThirdwebContract } from "thirdweb";
4-
import {
5-
Card,
6-
Heading,
7-
LinkButton,
8-
Text,
9-
TrackedLinkButton,
10-
} from "tw-components";
3+
import { Heading, TrackedLinkButton } from "tw-components";
114
import { AccountsCount } from "./components/accounts-count";
125
import { AccountsTable } from "./components/accounts-table";
136
import { CreateAccountButton } from "./components/create-account-button";
147

158
interface AccountsPageProps {
169
contract: ThirdwebContract;
17-
detectedAccountFactory: ExtensionDetectedState;
1810
}
1911

20-
export const AccountsPage: React.FC<AccountsPageProps> = ({
21-
contract,
22-
detectedAccountFactory,
23-
}) => {
24-
if (!detectedAccountFactory) {
25-
return (
26-
<Card as={Flex} flexDir="column" gap={3}>
27-
{/* TODO extract this out into it's own component and make it better */}
28-
<Heading size="subtitle.md">No Accounts extension enabled</Heading>
29-
<Text>
30-
To enable Accounts factory features you will have to extend an
31-
interface on your contract.
32-
</Text>
33-
<Box>
34-
<LinkButton
35-
isExternal
36-
href="https://portal.thirdweb.com/contracts/build/extensions/erc-4337/SmartWalletFactory"
37-
colorScheme="purple"
38-
>
39-
Learn more
40-
</LinkButton>
41-
</Box>
42-
</Card>
43-
);
44-
}
45-
12+
export const AccountsPage: React.FC<AccountsPageProps> = ({ contract }) => {
4613
return (
4714
<Flex direction="column" gap={6}>
4815
<Flex

packages/thirdweb/src/exports/extensions/erc4337.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// ACCOUNT
2+
export { isValidateUserOpSupported } from "../../extensions/erc4337/__generated__/IAccount/write/validateUserOp.js";
23
export {
34
type AddAdminOptions,
45
addAdmin,
6+
isAddAdminSupported,
57
} from "../../extensions/erc4337/account/addAdmin.js";
68

79
export {
@@ -12,40 +14,66 @@ export {
1214
export {
1315
type AddSessionKeyOptions,
1416
addSessionKey,
17+
isAddSessionKeySupported,
1518
} from "../../extensions/erc4337/account/addSessionKey.js";
1619

1720
export {
1821
type RemoveSessionKeyOptions,
1922
removeSessionKey,
23+
isRemoveSessionKeySupported,
2024
} from "../../extensions/erc4337/account/removeSessionKey.js";
2125

22-
export { getAllActiveSigners } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getAllActiveSigners.js";
23-
export { getAllAdmins } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getAllAdmins.js";
24-
export { getAllSigners } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getAllSigners.js";
26+
export {
27+
getAllActiveSigners,
28+
isGetAllActiveSignersSupported,
29+
} from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getAllActiveSigners.js";
30+
export {
31+
getAllAdmins,
32+
isGetAllAdminsSupported,
33+
} from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getAllAdmins.js";
34+
export {
35+
getAllSigners,
36+
isGetAllSignersSupported,
37+
} from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getAllSigners.js";
2538
export {
2639
getPermissionsForSigner,
2740
type GetPermissionsForSignerParams,
2841
} from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getPermissionsForSigner.js";
2942
export {
3043
isActiveSigner,
44+
isIsActiveSignerSupported,
3145
type IsActiveSignerParams,
3246
} from "../../extensions/erc4337/__generated__/IAccountPermissions/read/isActiveSigner.js";
33-
export { isAdmin } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/isAdmin.js";
47+
export {
48+
isAdmin,
49+
isIsAdminSupported,
50+
} from "../../extensions/erc4337/__generated__/IAccountPermissions/read/isAdmin.js";
3451
export { adminUpdatedEvent } from "../../extensions/erc4337/__generated__/IAccountPermissions/events/AdminUpdated.js";
3552
export { signerPermissionsUpdatedEvent } from "../../extensions/erc4337/__generated__/IAccountPermissions/events/SignerPermissionsUpdated.js";
3653

3754
// FACTORY
38-
export { getAllAccounts } from "../../extensions/erc4337/__generated__/IAccountFactory/read/getAllAccounts.js";
39-
export { getAccounts } from "../../extensions/erc4337/__generated__/IAccountFactory/read/getAccounts.js";
40-
export { totalAccounts } from "../../extensions/erc4337/__generated__/IAccountFactory/read/totalAccounts.js";
55+
export {
56+
getAllAccounts,
57+
isGetAllAccountsSupported,
58+
} from "../../extensions/erc4337/__generated__/IAccountFactory/read/getAllAccounts.js";
59+
export {
60+
getAccounts,
61+
isGetAccountsSupported,
62+
} from "../../extensions/erc4337/__generated__/IAccountFactory/read/getAccounts.js";
63+
export {
64+
totalAccounts,
65+
isTotalAccountsSupported,
66+
} from "../../extensions/erc4337/__generated__/IAccountFactory/read/totalAccounts.js";
4167
export { isRegistered } from "../../extensions/erc4337/__generated__/IAccountFactory/read/isRegistered.js";
4268
export {
4369
getAccountsOfSigner,
4470
type GetAccountsOfSignerParams,
71+
isGetAccountsOfSignerSupported,
4572
} from "../../extensions/erc4337/__generated__/IAccountFactory/read/getAccountsOfSigner.js";
4673
export {
4774
getAddress as predictAccountAddress,
4875
type GetAddressParams as PredictAccountAddressParams,
76+
isGetAddressSupported as isPredictAccountAddressSupported,
4977
} from "../../extensions/erc4337/__generated__/IAccountFactory/read/getAddress.js";
5078

5179
// ENTRYPOINT

0 commit comments

Comments
 (0)