Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
942d20a5d9 Version Packages (#6266)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-17 02:46:05 -08:00
Joaquim VergesandGitHub 739a2ae3c5 [wagmi-adapter] fix: Improve autoconnect handling and storage (#6273) 2025-02-17 01:35:00 -08:00
Joaquim VergesandGitHub 4158a796a8 [SDK] fix: Adjust swap summary border styling (#6272) 2025-02-17 00:12:52 -08:00
Joaquim VergesandGitHub a258e7eaf0 [SDK] feat: Redesign token selection flow in PayEmbed (#6268) 2025-02-16 19:10:19 -08:00
warengonzaga e386ac9476 Update page.mdx (#6270)
Update "extension" to "extensions"

<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the import path for the `balanceOf` function from the `thirdweb/extension/erc1155` to `thirdweb/extensions/erc1155`, ensuring that the correct module is referenced for checking wallet balances.

### Detailed summary
- Changed the import statement for `balanceOf` from `thirdweb/extension/erc1155` to `thirdweb/extensions/erc1155`.
- Added a newline at the end of the file.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-17 03:01:03 +00:00
Joaquim VergesandGitHub 0574eac02c [SDK] feat: Enable chain switching for EIP1193 provider (#6269) 2025-02-16 14:03:13 -08:00
gregandGitHub 52cbcd2d57 [Pay] Fix: fix fiat value to 2 decimals (#6243) 2025-02-15 12:54:12 -08:00
Joaquim VergesandGitHub 387891dde0 [SDK] feat: Redesign payment method selector UI (#6265) 2025-02-15 12:53:45 -08:00
Joaquim VergesandGitHub 562c534d37 [SDK] feat: new PayEmbed UI for selecting tokens (#6257) 2025-02-15 12:52:40 -08:00
MananTank b882e29935 [TOOL-3430] SDK: Fix ConnectEmbed showing a flash of Sign In prompt screen even if user is already signed in (#6262)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on improving the user experience in the `ConnectEmbed` component by addressing the display of a "Sign In" prompt when the login status is uncertain. Instead, it shows a loading indicator during this period.

### Detailed summary
- Updated the condition for rendering the loading screen in `SignatureScreen.tsx`.
- The loading screen is now shown if either `wallet` is not available or `siweAuth.isLoading` is true, enhancing user feedback during the login process.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-15 08:43:56 +00:00
MananTank 9ad1d3cbbf [TOOL-3429] Dashboard: Delete Insight playground, Add Playground Link (#6261)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/fb78547c-031f-44e0-bbd3-410a534d237d.png)
2025-02-15 02:10:24 +00:00
MananTank aab3da0729 [TOOL-3431] Dashboard: Add toggle theme button on logged out page header and other UI tweaks (#6263)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring components related to the `TeamHeader` and `HeaderLoggedOut`, improving code organization and consistency, and updating UI elements for better styling.

### Detailed summary
- Renamed `TeamHeaderLoggedOut` to `HeaderLoggedOut` and updated its import paths.
- Consolidated the header components for logged-out states.
- Changed class names for styling consistency.
- Refactored color mode toggle component to `ToggleThemeButton`.
- Updated button variants and styles in several components.
- Adjusted layout and spacing in various UI elements for improved design.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-15 01:51:40 +00:00
kumaryash90 78c9d2fe13 [Dashboard] Crosschain tab UI fixes (#6264)
TOOL-3433

<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the user experience in the dashboard by improving messaging for contract deployment, adding asynchronous handling for network selection, and integrating a mutation for adding chains, which provides feedback during the process.

### Detailed summary
- Updated message in `no-crosschain-prompt.tsx` to clarify contract functionality.
- Modified `SingleNetworkSelector` to include `isAddingRow` prop and changed `handleChange` to be asynchronous.
- Added mutation for adding rows in `DataTable`, with feedback for processing and error states.
- Removed redundant `handleAddRow` function from `DataTable`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-14 22:11:22 +00:00
GWSzeto 557d3e3dbd [Dashboard] Feature: UI for crosschain modules (#5399)
https://linear.app/thirdweb/project/[modular-contracts]-op-interoperability-module-87740e1390a9/overview

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new features and improvements related to cross-chain contract deployment in the `thirdweb` framework, enhancing functionality and usability for developers.

### Detailed summary
- Added `getDeployedCloneFactoryContract` export in `contract.ts`.
- Introduced `moduleInstalledEvent` export in `modules.ts`.
- Updated TypeScript documentation links.
- Enhanced sidebar links in `getContractPageSidebarLinks.ts`.
- Improved `namehash.ts` with typed return value.
- Created `NoCrossChainPrompt` component for user feedback.
- Added `nonce` and `initializeData` options in `installPublishedModule.ts`.
- Updated event handling for `ProxyDeployedV2`.
- Implemented cross-chain deployment logic in various files.
- Updated tests to cover new cross-chain functionality.
- Refactored contract deployment methods to support new parameters.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-14 18:46:16 +00:00
Jonas DanielsandGitHub 06d6504545 add teamId parameter to webhook endpoints (#6260) 2025-02-14 07:48:54 -08:00
kumaryash90 e201e3d423 Handle single unnamed param (#6242)
TOOL-3401

Dashboard handles single unnamed param as "*". Handle this in sdk. Throw error for all other cases.

<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `normalizeFunctionParams` function to handle cases where parameter names may be missing or empty. It introduces error handling for unnamed parameters and updates the associated tests to cover these scenarios.

### Detailed summary
- Updated the condition to check for both `undefined` and empty string in parameter names.
- Added a comment for future handling of multiple unnamed parameters.
- Modified test cases to include checks for empty parameter names.
- Ensured that unnamed parameters can be accessed via the `params["*"]` syntax.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-14 06:22:30 +00:00
Jonas DanielsandGitHub c8ef941d8e fetch gas sponsored chains from API instead of hardcoding (#6256) 2025-02-13 22:21:12 -08:00
gregfromstl 39a5b5c8a3 [Portal] Docs: Get Users Docs (#6255)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the user experience by adding a new feature to fetch users through an API and updating the API endpoint for wallet pregeneration.

### Detailed summary
- Added a new menu item `Fetch Users` in `sidebar.tsx`.
- Updated the API endpoint in `page.mdx` for wallet pregeneration.
- Introduced documentation for fetching users via REST API in `get-users/page.mdx`.
- Provided example curl command and response format for the new feature.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-14 04:08:25 +00:00
MananTank 6f381d768c [TOOL-3378] Dashboard: Add Contract to Project UI updates (#6233)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on refactoring and improving the structure of the dashboard application by removing obsolete files, enhancing component functionality, and updating UI elements for better user experience.

### Detailed summary
- Deleted several unused files related to contract management.
- Updated `className` properties for better styling consistency.
- Added `accountAddress` prop to various components for enhanced functionality.
- Introduced new hooks and functions for managing project contracts.
- Improved error handling in contract import functionality.
- Enhanced UI components for better responsiveness and clarity.

> The following files were skipped due to too many changes: `apps/dashboard/src/components/contract-components/tables/cells.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/add-to-project-card.tsx`, `apps/dashboard/src/components/smart-wallets/AccountFactories/index.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx`, `apps/dashboard/src/components/contract-components/tables/contract-table.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-14 01:41:11 +00:00
MananTank 0c5a51c5bf [TOOL-3243] Dashboard: Add redirect for support old engine import link (#6254)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `Page` function in `page.tsx` to handle `searchParams`, allowing for an import URL to trigger a redirect when present.

### Detailed summary
- Added `searchParams` to the `Page` function's props.
- Used `Promise.all` to await both `params` and `searchParams`.
- Implemented a redirect to an import URL if `searchParams.importUrl` is provided.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-13 23:45:52 +00:00
MananTank 90428b4574 [TOOL-3422] Dashboard: Remove unused assets, Add Delete script (#6253)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on the deletion of numerous unused asset files from the `apps/dashboard/public/assets` directory and introduces a script for managing the deletion of these files.

### Detailed summary
- Deleted multiple unused asset files from various directories in `apps/dashboard/public/assets`.
- Introduced `deleteUnusedAssets.ts` script to handle the deletion of specified files.
- Added functionality to log deleted files and handle errors when files are not found.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-13 19:54:05 +00:00
MananTank e24547bed1 [NEB-95] Dashboard: Fix Nebula analytics crash, add missing use client directive (#6252) 2025-02-13 18:58:25 +00:00
MananTank 3ff8c3fbb2 [NEB-95] Dashboard: Nebula Analytics UI updates (#6248)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/6a8f069c-904b-447b-8757-54dde4b13c84.png)

<!-- start pr-codex -->

## PR-Codex overview
This PR primarily focuses on enhancing the `ThirdwebBarChart` and `ThirdwebAreaChart` components by introducing a `header` prop for improved chart display, modifying chart aspect ratios, and refining random data generation for analytics. It also updates the `NebulaAnalyticsPage` structure.

### Detailed summary
- Added `header` prop to `ThirdwebBarChart` and `ThirdwebAreaChart` for titles and descriptions.
- Changed aspect ratio from `lg:aspect-[4.5]` to `lg:aspect-[3.5]` in `Analytics.tsx`.
- Updated import path for `NebulaAnalyticsPage`.
- Refined random data generation logic in `nebula-analytics-ui.stories.tsx`.
- Improved chart configurations in multiple analytics components.
- Introduced `AnalyticsChart` component for better organization of chart data and props.
- Adjusted styling and structure of various charts for consistency.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-13 17:56:48 +00:00
MananTank 4bd03b06e7 [TOOL-3415] Dashboard: Fix Explore page header showing logged out state (#6250) 2025-02-13 15:12:38 +00:00
Jonas DanielsandGitHub b7158c2b54 add projectId parameter to pay analytics components (#6245) 2025-02-12 21:54:34 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
5f26883fbd Version Packages (#6247)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-12 19:38:31 -08:00
Joaquim VergesandGitHub 98b6198687 [SDK] fix: Prevent popup when using auth redirect mode (#6246) 2025-02-12 19:31:09 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
880148b793 Version Packages (#6241)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-12 14:34:11 -08:00
Joaquim VergesandGitHub ee1bc3eb07 [SDK] fix: show token + dollar values for wallet balances (#6240) 2025-02-12 14:20:16 -08:00
MananTank e9ab8cb770 [TOOL-3398] Dashboard: Fix DialogOverlay and DialogContent missing default shadcn z-index (#6239) 2025-02-12 21:08:03 +00:00
MananTank 9c33981b72 [TOOL-3396] Dashboard: Fix inputMode for OTP input, Use text instead of default numeric (#6238) 2025-02-12 11:43:10 +00:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
c645b42684 Version Packages (#6208)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-12 01:36:43 -08:00
Joaquim VergesandGitHub 7583074c06 Update page.mdx (#6235)
Signed-off-by: Joaquim Verges <[email protected]>
2025-02-12 01:30:39 -08:00
Joaquim VergesandGitHub fbafb65f5c [SDK] fix: Polish PayEmbed UI and balance display (#6237) 2025-02-12 01:30:20 -08:00
Joaquim VergesandGitHub 6b286c7e83 [SDK] fix: Disable amount editing for non-wallet funding modes (#6236) 2025-02-12 01:23:28 -08:00
joaquim-verges 159ffbfaee [SDK] feat: Add verifyClaim export and canClaim function (#6234)
Fixes TOOL-3381

<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on exposing the `canClaim` extension function for ERC20, ERC721, and ERC1155 drops, allowing for better claim verification and improving the overall functionality of the drops system.

### Detailed summary
- Added `canClaim` function to `erc20`, `erc721`, and `erc1155` extensions.
- Updated tests to include `canClaim` functionality.
- Modified JSON ABI files to include `verifyClaim` function.
- Adjusted gas values in tests.
- Enhanced error handling in `extract-error.ts`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-12 07:56:01 +00:00
kumaryash90 0c1bb6ccae [Dashboard] Deploy config updates (#6159)
TOOL-3389

<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a default fee configuration for contract deployments, updates the handling of platform fees, and improves the UI components related to fee management, ensuring that default values are used consistently throughout the application.

### Detailed summary
- Added `DEFAULT_FEE_RECIPIENT` and `DEFAULT_FEE_BPS` to `addresses.ts`.
- Introduced `hasDefaultFeeConfig` in `ContractSettingsPage`.
- Filtered out specific functions related to platform fees in `contract-function.tsx`.
- Integrated platform fee logic in `CustomContractForm`.
- Updated `PlatformFeeFieldset` to reflect marketplace conditions.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-11 22:00:08 +00:00
Shubham ParkhiandGitHub dceb47de8b [SDK/Dashboard/Portal] Feature/Fix: fix typo in docs (#6229)
Signed-off-by: Shubham Parkhi <[email protected]>
2025-02-10 21:19:02 -08:00
Jonas DanielsandGitHub dfbdbd06e5 use supportPlan instead of billingPlan for support tickets (#6228) 2025-02-10 21:17:24 -08:00
66bbedbe3a [SDK] Feature: add treasure and treasure topaz (#6191)
Co-authored-by: Joaquim Verges <[email protected]>
2025-02-10 19:42:15 -08:00
Joaquim VergesandGitHub 2fbf105d15 [SDK] fix: Disable quote retries and show fiat values (#6210) 2025-02-10 19:41:42 -08:00
FirekeeperandGitHub afcaa91873 Docs remove legacy enc key arg (#6220) 2025-02-10 19:41:08 -08:00
Manan TankandGitHub 51f955352d [TOOL-3377] Portal: Remove v4 SDK links, setup missing redirects (#6222) 2025-02-10 19:40:48 -08:00
Joaquim VergesandGitHub 6b2a90ac89 [SDK] feat: Add session key update handling and expose utility (#6217) 2025-02-10 19:40:06 -08:00
MananTank cf53184bb1 [TOOL-3379] Dashboard: Add team selector in support form (#6227) 2025-02-10 22:21:18 +00:00
MananTank 815b9b1ce1 [NEB-92] Dashboard: Add Nebula Analytics dashboard (#6198)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily focuses on enhancing the `Nebula` analytics functionality by adding new components and improving data handling for analytics. It introduces new utilities for managing date ranges and search parameters, along with updates to the UI for better user interaction.

### Detailed summary
- Added `getNebulaAnalyticsRangeFromSearchParams` function in `utils.ts`.
- Updated `ProjectTabs` to remove `isOnNebulaWaitList` prop.
- Introduced `normalizeTime` function in `time.ts` for date normalization.
- Created `NebulaAnalyticsFilter` component for filtering analytics data.
- Implemented `NebulaAnalyticsPage` and `NebulaAnalyticDashboard` for displaying analytics.
- Enhanced `DateRangeSelector` with popover alignment options.
- Added `NebulaAnalyticsDashboardUI` for rendering analytics data and charts.
- Integrated `responsive-rsc` for responsive search parameters.
- Updated `package.json` to include `responsive-rsc` dependency.
- Removed calls to `getTeamNebulaWaitList` in `layout.tsx` and associated logic.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-10 20:05:31 +00:00
Maxim EvtushandGitHub ae1baa5105 fix: typos in documentation files (#6215)
Signed-off-by: Maxim Evtush <[email protected]>
2025-02-10 10:40:32 -08:00
saminacodes 92266b7459 [Portal] Cosmetic Changes (#6218)
CORE-0000

---

## PR-Codex overview
This PR focuses on updating content and structure across various pages in the `apps/portal` application, including the addition of new features, improved descriptions, and adjustments to the sidebar navigation.

### Detailed summary
- Deleted `supported-chains.png` and `context-filters.png`.
- Added new content for `faqs`, `troubleshoot`, and `Nebula` guides.
- Updated the sidebar with new links and icons.
- Changed import paths for images.
- Enhanced descriptions in `Deploy`, `Contracts`, and `Insight` pages.
- Added a new section for "Supported Chains" in `Contracts` and `Insight` pages.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the documentation and content for the `thirdweb` platform, particularly for the `Insight` and `Nebula` sections, while also restructuring some sidebar links and enhancing the user interface with new images.

### Detailed summary
- Deleted `supported-chains` image and `page.mdx` files.
- Added new content for `Insight FAQs` and `Troubleshoot Guide`.
- Updated `Nebula` description and added a support link.
- Changed sidebar items for `Insight` and `Contracts`.
- Introduced new features and layout in `Insight` and `Contracts` pages.
- Added support for displaying `Supported Chains` images in relevant sections.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updates to the `insight` and `nebula` sections of the application, including the addition of new content, restructuring of sidebar navigation, and changes to image imports. It also enhances the user interface with improved documentation and guidance.

### Detailed summary
- Deleted `apps/portal/src/app/insight/supported-chains/page.mdx`.
- Added new content for `Insight FAQs`, `Insight Troubleshoot Guide`, and `Nebula Troubleshoot Guide`.
- Updated sidebar navigation in `apps/portal/src/app/insight/sidebar.tsx`.
- Changed the name and href for several sidebar items.
- Reorganized `Supported Chains` references and added a new image import in multiple files.
- Enhanced content descriptions in `apps/portal/src/app/contracts/deploy/overview/page.mdx`.
- Added new features and video tutorials in `apps/portal/src/app/insight/page.mdx`.
- Updated layout and structure of `Contracts` section in `apps/portal/src/app/contracts/page.mdx`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
2025-02-10 17:19:27 +00:00
MananTank 80ad84b803 [TOOL-3376] Dashboard: Fix 404 on publish contract page (#6221) 2025-02-10 17:01:59 +00:00
saminacodes 633f0cadf1 [Portal] Nebula documentation update (#6201)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the documentation for `thirdweb Nebula`, adding detailed metadata, explanations of new features, and clarifying existing concepts. It includes updates to various pages, improving user understanding of plugins, sessions, context filters, and response handling.

### Detailed summary
- Added `metadata` exports to multiple `.mdx` files.
- Enhanced FAQ entries with links and additional context.
- Updated `sidebar` to include new key concepts.
- Added sections on `Chat & Execute endpoints`, `Execute Configuration`, and `Context Filters`.
- Introduced new content on `Response Handling` and streaming vs non-streaming responses.
- Clarified `Sessions` management and benefits.
- Replaced `Stack` with `Grid` in the `page.mdx` and updated links.
- Improved descriptions and examples for API usage and configurations.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-10 16:00:45 +00:00
Phillip HoandGitHub 91b0367b23 [service-utils] fix auth cache for cf workers (#6219) 2025-02-10 23:51:58 +08:00
MananTank c86e13b522 [TOOL-3366] Dashboard: Contract Deployment UI improvements, Reduce Chakra Usage (#6211)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refining the UI components and improving the overall structure of the contract deployment forms. It introduces new styles, modifies existing components, and enhances accessibility.

### Detailed summary
- Updated `ThemeProvider` to enable color scheme.
- Changed class names for various components to improve styling.
- Simplified props for `SolidityInput` components.
- Replaced `FormControl` with `FormFieldSetup` for better structure.
- Enhanced accessibility by modifying error messages and labels.
- Streamlined imports and removed unnecessary components.
- Improved the layout and styling of various fieldsets and buttons.
- Added new utility functions for better handling of form states.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-09 21:35:45 +05:30
MananTank bc42c2fdfb [TOOL-3368] Dashboard: Improve page load perf of explore page and other few others (#6214)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating the contract fetching logic in the `dashboard` application to improve the retrieval of the latest published contracts. It replaces older functions with new ones that specifically fetch the latest contract versions.

### Detailed summary
- Added `export const dynamic = "force-static";` to `page.tsx` files.
- Replaced `getPublishedContractsWithPublisherMapping` with `getLatestPublishedContractsWithPublisherMapping` in multiple files.
- Introduced `getLatestPublishedContractsWithPublisherMapping` function to fetch the latest contract version.
- Updated `fetchPublishedContractVersions` to use `getSortedPublishedContractVersions`.
- Created `fetchLatestPublishedContractVersion` for fetching the latest version details.
- Modified `ContractCard` to use `ClientOnly` for rendering the `ContractPublisher`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-09 15:33:14 +00:00
Phillip HoandGitHub cad9c74bef [service-utils] Omit team_id for client usageV2 events (#6213) 2025-02-09 17:59:56 +08:00
Phillip HoandGitHub 6f2f92ddea [service-utils]H elper to call client usageV2 reporting endpoint (#6212) 2025-02-09 17:26:08 +08:00
Joaquim VergesandGitHub 97c9ab7e1c [SDK] feat: Batch approvals and swaps for smart wallets (#6209) 2025-02-08 03:04:11 -08:00
d4mr b73fa4b16f [dashboard-engine] updates for circle and wallet credentials (#6202)
closes TOOL-3351

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces support for managing Circle wallet credentials in the thirdweb Engine, enhancing the wallet management features by allowing users to create, update, and configure Circle wallet credentials.

### Detailed summary
- Added `Wallet Credentials` section in the sidebar.
- Introduced `Circle` wallet option in backend wallet configurations.
- Created new components for managing Circle wallet credentials.
- Implemented credential creation and update functionalities.
- Updated UI components to support Circle wallet management.
- Enhanced documentation for Circle wallet setup and usage.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-02-08 09:45:46 +00:00
Phillip HoandGitHub cbfe9ec459 [service-utils] Allow sending client-side usageV2 events (#6207) 2025-02-08 13:07:57 +08:00
374 changed files with 9753 additions and 6256 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
+1 -1
View File
@@ -72,7 +72,6 @@
"next-plausible": "^3.12.4",
"next-themes": "^0.4.4",
"nextjs-toploader": "^1.6.12",
"openapi-types": "^12.1.3",
"p-limit": "^6.2.0",
"papaparse": "^5.5.2",
"pluralize": "^8.0.0",
@@ -89,6 +88,7 @@
"react-table": "^7.8.0",
"recharts": "2.15.1",
"remark-gfm": "^4.0.0",
"responsive-rsc": "0.0.7",
"server-only": "^0.0.1",
"shiki": "1.27.0",
"sonner": "^1.7.4",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

@@ -0,0 +1,28 @@
import { promises as fs } from "node:fs";
import path from "node:path";
// after running logUnusedAssets.ts,
// 1. validate the output
// 2. If you think that a file should not be deleted, remove it from the output
// 3. paste the output files in `filesToDelete` and run this script to delete them
const filesToDelete: string[] = [];
async function deleteFiles() {
for (const filePath of filesToDelete) {
try {
const fullPath = path.join(process.cwd(), filePath);
await fs.unlink(fullPath);
console.log(`Deleted: ${filePath}`);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
console.warn(`File not found: ${filePath}`);
} else {
console.error(`Error deleting ${filePath}:`, error);
}
}
}
console.log("--- DONE ---");
}
deleteFiles();
@@ -26,9 +26,6 @@ const filesToIgnore = new Set([
// macOS stuff
"public/assets/product-pages/.DS_Store",
"public/assets/.DS_Store",
// pdfs
"public/Thirdweb_Terms_of_Service.pdf",
"public/thirdweb_Privacy_Policy_May_2022.pdf",
]);
function getAllFilesInFolder(folderPath: string) {
+34
View File
@@ -0,0 +1,34 @@
import "server-only";
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "../constants/env";
export async function getGasSponsoredChains() {
if (!THIRDWEB_API_SECRET) {
throw new Error("API_SERVER_SECRET is not set");
}
const res = await fetch(`${API_SERVER_URL}/v1/chains/gas-sponsored`, {
headers: {
"Content-Type": "application/json",
"x-service-api-key": THIRDWEB_API_SECRET,
},
next: {
revalidate: 15 * 60, //15 minutes
},
});
if (!res.ok) {
console.error(
"Failed to fetch gas sponsored chains",
res.status,
res.statusText,
);
res.body?.cancel();
return [];
}
try {
return (await res.json()).data as number[];
} catch (e) {
console.error("Failed to parse gas sponsored chains", e);
return [];
}
}
+5
View File
@@ -24,6 +24,7 @@ export type Team = {
image?: string;
billingPlan: "pro" | "growth" | "free" | "starter";
billingStatus: "validPayment" | (string & {}) | null;
supportPlan: "pro" | "growth" | "free" | "starter";
billingEmail: string | null;
growthTrialEligible: false;
enabledScopes: EnabledTeamScope[];
@@ -47,6 +48,10 @@ export async function getTeamBySlug(slug: string) {
return null;
}
export function getTeamById(id: string) {
return getTeamBySlug(id);
}
export async function getTeams() {
const token = await getAuthToken();
if (!token) {
@@ -28,7 +28,7 @@ export const Mobile: Story = {
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
<BadgeContainer label="Base">
<DangerSettingCard
title="This is a title"
@@ -1,10 +1,11 @@
import { Label } from "@/components/ui/label";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { AsteriskIcon, InfoIcon } from "lucide-react";
import type React from "react";
export function FormFieldSetup(props: {
htmlFor?: string;
label: string;
label: React.ReactNode;
errorMessage: React.ReactNode | undefined;
children: React.ReactNode;
tooltip?: React.ReactNode;
@@ -1,5 +1,12 @@
"use client";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
@@ -15,8 +22,14 @@ import {
EmptyChartState,
LoadingChartState,
} from "../../../../components/analytics/empty-chart-state";
import { cn } from "../../../lib/utils";
type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
header?: {
title: string;
description?: string;
titleClassName?: string;
};
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -25,78 +38,99 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
// chart className
chartClassName?: string;
isPending: boolean;
hideLabel?: boolean;
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
};
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
props: ThirdwebAreaChartProps<TConfig>,
) {
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
return (
<div className="rounded-lg border border-border px-4 pt-10 pb-4">
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
<LoadingChartState />
) : props.data.length === 0 ? (
<EmptyChartState />
) : (
<AreaChart
accessibilityLayer
data={props.data}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={20}
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<defs>
{configKeys.map((key) => (
<linearGradient
key={key}
id={`fill_${key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor={`var(--color-${key})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${key})`}
stopOpacity={0.1}
/>
</linearGradient>
))}
</defs>
{configKeys.map((key) => (
<Area
key={key}
dataKey={key}
type="natural"
fill={`url(#fill_${key})`}
fillOpacity={0.4}
stroke={`var(--color-${key})`}
stackId="a"
/>
))}
{props.showLegend && (
<ChartLegend content={<ChartLegendContent />} className="pt-8" />
)}
</AreaChart>
)}
</ChartContainer>
</div>
return (
<Card>
{props.header && (
<CardHeader>
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
{props.header.title}
</CardTitle>
{props.header.description && (
<CardDescription>{props.header.description}</CardDescription>
)}
</CardHeader>
)}
<CardContent className={cn(!props.header && "pt-6")}>
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
<LoadingChartState />
) : props.data.length === 0 ? (
<EmptyChartState />
) : (
<AreaChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={20}
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
hideLabel={
props.hideLabel !== undefined ? props.hideLabel : true
}
labelFormatter={props.toolTipLabelFormatter}
/>
}
/>
<defs>
{configKeys.map((key) => (
<linearGradient
key={key}
id={`fill_${key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor={`var(--color-${key})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${key})`}
stopOpacity={0.1}
/>
</linearGradient>
))}
</defs>
{configKeys.map((key) => (
<Area
key={key}
dataKey={key}
type="natural"
fill={`url(#fill_${key})`}
fillOpacity={0.4}
stroke={`var(--color-${key})`}
stackId="a"
/>
))}
{props.showLegend && (
<ChartLegend
content={<ChartLegendContent className="pt-8" />}
/>
)}
</AreaChart>
)}
</ChartContainer>
</CardContent>
</Card>
);
}
@@ -27,8 +27,11 @@ import { cn } from "../../../lib/utils";
type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
// metadata
title: string;
description?: string;
header?: {
title: string;
description?: string;
titleClassName?: string;
};
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -39,7 +42,6 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
isPending: boolean;
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
hideLabel?: boolean;
titleClassName?: string;
};
export function ThirdwebBarChart<TConfig extends ChartConfig>(
@@ -52,15 +54,18 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
return (
<Card>
<CardHeader>
<CardTitle className={cn("mb-2", props.titleClassName)}>
{props.title}
</CardTitle>
{props.description && (
<CardDescription>{props.description}</CardDescription>
)}
</CardHeader>
<CardContent>
{props.header && (
<CardHeader>
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
{props.header.title}
</CardTitle>
{props.header.description && (
<CardDescription>{props.header.description}</CardDescription>
)}
</CardHeader>
)}
<CardContent className={cn(!props.header && "pt-6")}>
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
<LoadingChartState />
@@ -87,7 +92,9 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
}
/>
{props.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
<ChartLegend
content={<ChartLegendContent className="pt-5" />}
/>
)}
{configKeys.map((key, idx) => (
<Bar
@@ -2,27 +2,29 @@
import { Button } from "@/components/ui/button";
import { ClientOnly } from "components/ClientOnly/ClientOnly";
import { Moon, Sun } from "lucide-react";
import { MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { Skeleton } from "./ui/skeleton";
export function ColorModeToggle() {
export function ToggleThemeButton() {
const { setTheme, theme } = useTheme();
return (
<ClientOnly ssr={<Skeleton className="size-10 border bg-accent" />}>
<ClientOnly
ssr={<Skeleton className="size-[34px] rounded-full border bg-accent" />}
>
<Button
variant="ghost"
size="icon"
variant="outline"
onClick={() => {
setTheme(theme === "dark" ? "light" : "dark");
}}
aria-label="Toggle theme"
className="h-auto w-auto rounded-full border border-border bg-background p-2"
>
{theme === "light" ? (
<Sun strokeWidth={1} className="size-5" />
<SunIcon className="size-4" />
) : (
<Moon strokeWidth={1} className="size-5" />
<MoonIcon className="size-4" />
)}
</Button>
</ClientOnly>
@@ -27,6 +27,7 @@ export function DatePickerWithRange(props: {
header?: React.ReactNode;
footer?: React.ReactNode;
labelOverride?: string;
popoverAlign?: "start" | "end" | "center";
}) {
const [screen, setScreen] = React.useState<"from" | "to">("from");
const { from, to, setFrom, setTo } = props;
@@ -65,7 +66,11 @@ export function DatePickerWithRange(props: {
</PopoverTrigger>
{/* Popover */}
<PopoverContent className="w-auto p-0" align="start">
<PopoverContent
className="w-auto p-0"
align={props.popoverAlign || "start"}
sideOffset={10}
>
<DynamicHeight>
<div>
{!isValid && (
@@ -0,0 +1,16 @@
import Link from "next/link";
import { cn } from "../../lib/utils";
type LinkProps = React.ComponentProps<typeof Link>;
export function UnderlineLink(props: LinkProps) {
return (
<Link
{...props}
className={cn(
"underline decoration-muted-foreground/50 decoration-dotted underline-offset-[5px] hover:text-foreground hover:decoration-foreground hover:decoration-solid",
props.className,
)}
/>
);
}
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
className,
)}
{...props}
@@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
"fixed z-50 grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
"right-0 bottom-0 left-0",
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+2
View File
@@ -4,6 +4,8 @@ export const DASHBOARD_THIRDWEB_CLIENT_ID =
export const DASHBOARD_THIRDWEB_SECRET_KEY =
process.env.DASHBOARD_SECRET_KEY || "";
export const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
export const IPFS_GATEWAY_URL =
(process.env.NEXT_PUBLIC_IPFS_GATEWAY_URL as string) ||
"https://{clientId}.ipfscdn.io/ipfs/{cid}/{path}";
+1 -1
View File
@@ -117,7 +117,7 @@ function DashboardRouterTopProgressBarInner() {
const width = isLoading ? progress : 100;
return (
<span
className="fixed top-0 block h-[2px] bg-blue-500"
className="fixed top-0 block h-[3px] bg-foreground"
style={{
opacity: isLoading ? "100" : "0",
width: `${width}%`,
@@ -125,4 +125,6 @@ export const engineKeys = {
[...engineKeys.all, engineId, "alerts"] as const,
notificationChannels: (engineId: string) =>
[...engineKeys.all, engineId, "notificationChannels"] as const,
walletCredentials: (instance: string) =>
[...engineKeys.all, instance, "walletCredentials"] as const,
};
@@ -15,6 +15,7 @@ export function useDashboardContractMetadata(contract: ThirdwebContract) {
return useQuery({
queryKey: ["contract-metadata-header", contract.chain.id, contract.address],
queryFn: () => fetchDashboardContractMetadata(contract),
refetchOnWindowFocus: false,
});
}
@@ -93,7 +93,8 @@ type EngineFeature =
| "CONTRACT_SUBSCRIPTIONS"
| "IP_ALLOWLIST"
| "HETEROGENEOUS_WALLET_TYPES"
| "SMART_BACKEND_WALLETS";
| "SMART_BACKEND_WALLETS"
| "WALLET_CREDENTIALS";
interface EngineSystemHealth {
status: string;
@@ -860,6 +861,10 @@ export type SetWalletConfigInput =
gcpKmsKeyRingId: string;
gcpApplicationCredentialEmail: string;
gcpApplicationCredentialPrivateKey: string;
}
| {
type: "circle";
circleApiKey: string;
};
export function useEngineSetWalletConfig(params: {
@@ -869,8 +874,8 @@ export function useEngineSetWalletConfig(params: {
const { instanceUrl, authToken } = params;
const queryClient = useQueryClient();
return useMutation<WalletConfigResponse, void, SetWalletConfigInput>({
mutationFn: async (input) => {
return useMutation({
mutationFn: async (input: SetWalletConfigInput) => {
invariant(instanceUrl, "instance is required");
const res = await fetch(`${instanceUrl}configuration/wallets`, {
@@ -884,7 +889,7 @@ export function useEngineSetWalletConfig(params: {
throw new Error(json.error.message);
}
return json.result;
return json.result as WalletConfigResponse;
},
onSuccess: () => {
return queryClient.invalidateQueries({
@@ -894,10 +899,17 @@ export function useEngineSetWalletConfig(params: {
});
}
export type CreateBackendWalletInput = {
type: EngineBackendWalletType;
label?: string;
};
export type CreateBackendWalletInput =
| {
type: Exclude<EngineBackendWalletType, "circle" | "smart:circle">;
label?: string;
}
| {
type: "circle" | "smart:circle";
label?: string;
credentialId: string;
isTestnet: boolean;
};
export function useEngineCreateBackendWallet(params: {
instanceUrl: string;
@@ -1851,3 +1863,118 @@ export function useEngineDeleteNotificationChannel(engineId: string) {
},
});
}
export interface WalletCredential {
id: string;
type: string;
label: string;
isDefault: boolean | null;
createdAt: string;
updatedAt: string;
}
interface CreateWalletCredentialInput {
type: "circle";
label: string;
entitySecret?: string;
isDefault?: boolean;
}
export function useEngineWalletCredentials(params: {
instanceUrl: string;
authToken: string;
page?: number;
limit?: number;
}) {
const { instanceUrl, authToken, page = 1, limit = 100 } = params;
return useQuery({
queryKey: [...engineKeys.walletCredentials(instanceUrl), page, limit],
queryFn: async () => {
const res = await fetch(
`${instanceUrl}wallet-credentials?page=${page}&limit=${limit}`,
{
method: "GET",
headers: getEngineRequestHeaders(authToken),
},
);
const json = await res.json();
return (json.result as WalletCredential[]) || [];
},
enabled: !!instanceUrl,
});
}
export function useEngineCreateWalletCredential(params: {
instanceUrl: string;
authToken: string;
}) {
const { instanceUrl, authToken } = params;
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: CreateWalletCredentialInput) => {
invariant(instanceUrl, "instance is required");
const res = await fetch(`${instanceUrl}wallet-credentials`, {
method: "POST",
headers: getEngineRequestHeaders(authToken),
body: JSON.stringify(input),
});
const json = await res.json();
if (json.error) {
throw new Error(json.error.message);
}
return json.result as WalletCredential;
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.walletCredentials(instanceUrl),
});
},
});
}
interface UpdateWalletCredentialInput {
label?: string;
isDefault?: boolean;
entitySecret?: string;
}
export function useEngineUpdateWalletCredential(params: {
instanceUrl: string;
authToken: string;
}) {
const { instanceUrl, authToken } = params;
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
id,
...input
}: UpdateWalletCredentialInput & { id: string }) => {
invariant(instanceUrl, "instance is required");
const res = await fetch(`${instanceUrl}wallet-credentials/${id}`, {
method: "PUT",
headers: getEngineRequestHeaders(authToken),
body: JSON.stringify(input),
});
const json = await res.json();
if (json.error) {
throw new Error(json.error.message);
}
return json.result as WalletCredential;
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.walletCredentials(instanceUrl),
});
},
});
}
@@ -1,149 +0,0 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { addContractToMultiChainRegistry } from "components/contract-components/utils";
import { MULTICHAIN_REGISTRY_CONTRACT } from "constants/contracts";
import {
DASHBOARD_ENGINE_RELAYER_URL,
DASHBOARD_FORWARDER_ADDRESS,
} from "constants/misc";
import type { BasicContract } from "contract-ui/types/types";
import { getAllMultichainRegistry } from "dashboard-extensions/common/read/getAllMultichainRegistry";
import { useAllChainsData } from "hooks/chains/allChains";
import { useMemo } from "react";
import { sendAndConfirmTransaction } from "thirdweb";
import { remove } from "thirdweb/extensions/thirdweb";
import { useActiveAccount } from "thirdweb/react";
import invariant from "tiny-invariant";
export function useMultiChainRegContractList(walletAddress?: string) {
return useQuery({
queryKey: ["dashboard-registry", walletAddress, "multichain-contract-list"],
queryFn: async () => {
invariant(walletAddress, "walletAddress is required");
const contracts = await getAllMultichainRegistry({
contract: MULTICHAIN_REGISTRY_CONTRACT,
address: walletAddress,
});
return contracts;
},
enabled: !!walletAddress,
});
}
interface Options {
onlyMainnet?: boolean;
}
export const useAllContractList = (
walletAddress: string | undefined,
{ onlyMainnet }: Options = { onlyMainnet: false },
) => {
const multiChainQuery = useMultiChainRegContractList(walletAddress);
// TODO - instead of using ALL chains, fetch only the ones used here
const { idToChain } = useAllChainsData();
const contractList = useMemo(() => {
const data = multiChainQuery.data || [];
const mainnets: BasicContract[] = [];
const testnets: BasicContract[] = [];
// biome-ignore lint/complexity/noForEach: FIXME
data.forEach((net) => {
const chn = idToChain.get(net.chainId);
if (chn && chn.status !== "deprecated") {
if (chn.testnet) {
testnets.push(net);
} else {
mainnets.push(net);
}
}
});
mainnets.sort((a, b) => a.chainId - b.chainId);
if (onlyMainnet) {
return mainnets;
}
testnets.sort((a, b) => a.chainId - b.chainId);
return mainnets.concat(testnets);
}, [multiChainQuery.data, onlyMainnet, idToChain]);
return {
...multiChainQuery,
data: contractList,
};
};
type RemoveContractParams = {
contractAddress: string;
chainId: number;
};
export function useRemoveContractMutation() {
const queryClient = useQueryClient();
const account = useActiveAccount();
return useMutation({
mutationFn: async (data: RemoveContractParams) => {
invariant(data.chainId, "chainId not provided");
invariant(account, "No wallet connected");
const { contractAddress, chainId } = data;
const transaction = remove({
contract: MULTICHAIN_REGISTRY_CONTRACT,
deployer: account.address,
deployment: contractAddress,
chainId: BigInt(chainId),
});
return await sendAndConfirmTransaction({
transaction,
account,
gasless: {
experimentalChainlessSupport: true,
provider: "engine",
relayerUrl: DASHBOARD_ENGINE_RELAYER_URL,
relayerForwarderAddress: DASHBOARD_FORWARDER_ADDRESS,
},
});
},
onSettled: () => {
return queryClient.invalidateQueries({
queryKey: ["dashboard-registry"],
});
},
});
}
type AddContractParams = {
contractAddress: string;
chainId: number;
};
export function useAddContractMutation() {
const account = useActiveAccount();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: AddContractParams) => {
invariant(account, "cannot add a contract without an address");
return await addContractToMultiChainRegistry(
{
address: data.contractAddress,
chainId: data.chainId,
},
account,
300000n,
);
},
onSettled: () => {
return queryClient.invalidateQueries({
queryKey: ["dashboard-registry"],
});
},
});
}
@@ -1,8 +1,9 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import type { ThirdwebContract } from "thirdweb";
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import type { MinimalTeamsAndProjects } from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
@@ -13,6 +14,8 @@ export function ContractPageLayoutClient(props: {
chainMetadata: ChainMetadata;
contract: ThirdwebContract;
children: React.ReactNode;
teamsAndProjects: MinimalTeamsAndProjects | undefined;
client: ThirdwebClient;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
const headerMetadataQuery = useQuery({
@@ -3,8 +3,9 @@ import type { SidebarLink } from "@/components/blocks/Sidebar";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
import type { ThirdwebContract } from "thirdweb";
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import type { MinimalTeamsAndProjects } from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { ContractMetadata } from "./contract-metadata";
import { PrimaryDashboardButton } from "./primary-dashboard-button";
@@ -14,6 +15,8 @@ export function ContractPageLayout(props: {
children: React.ReactNode;
sidebarLinks: SidebarLink[];
dashboardContractMetadata: DashboardContractMetadata | undefined;
client: ThirdwebClient;
teamsAndProjects: MinimalTeamsAndProjects | undefined;
externalLinks:
| {
name: string;
@@ -27,6 +30,8 @@ export function ContractPageLayout(props: {
sidebarLinks,
dashboardContractMetadata,
externalLinks,
teamsAndProjects,
client,
} = props;
return (
@@ -49,6 +54,8 @@ export function ContractPageLayout(props: {
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
teamsAndProjects={teamsAndProjects}
client={client}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
@@ -1,19 +1,31 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import type { EVMContractInfo } from "@3rdweb-sdk/react";
import {
useAddContractMutation,
useAllContractList,
} from "@3rdweb-sdk/react/hooks/useRegistry";
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import type { EVMContractInfo } from "@3rdweb-sdk/react";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { CodeIcon, PlusIcon } from "lucide-react";
import { CircleAlertIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import type { Chain } from "thirdweb";
import { useActiveAccount } from "thirdweb/react";
import { useState } from "react";
import { toast } from "sonner";
import type { Chain, ThirdwebClient } from "thirdweb";
import {
AddToProjectSelector,
type MinimalTeamsAndProjects,
} from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/hooks/project-contracts";
const TRACKING_CATEGORY = "add_to_dashboard_upsell";
@@ -22,6 +34,8 @@ type AddToDashboardCardProps = {
chain: Chain;
contractInfo: EVMContractInfo;
hideCodePageLink?: boolean;
teamsAndProjects: MinimalTeamsAndProjects | undefined;
client: ThirdwebClient;
};
export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
@@ -29,40 +43,13 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
chain,
contractInfo,
hideCodePageLink,
teamsAndProjects,
client,
}) => {
const addContract = useAddContractMutation();
const trackEvent = useTrack();
const walletAddress = useActiveAccount()?.address;
const pathname = usePathname();
const registry = useAllContractList(walletAddress);
const { onSuccess: onAddSuccess, onError: onAddError } = useTxNotifications(
"Successfully imported",
"Failed to import",
);
const isInRegistry =
registry.isFetched &&
registry.data?.find(
(c) =>
contractAddress &&
// compare address...
c.address.toLowerCase() === contractAddress.toLowerCase() &&
// ... and chainId
c.chainId === chain.id,
) &&
registry.isSuccess;
if (
!walletAddress ||
!contractAddress ||
!chain ||
pathname?.includes("payments")
) {
return null;
}
if (isInRegistry) {
// if user is not logged in
if (!teamsAndProjects) {
if (hideCodePageLink) {
return null;
}
@@ -84,54 +71,163 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
}
return (
<Button
className="gap-2"
variant="outline"
onClick={() => {
if (!chain) {
return;
}
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "attempt",
contractAddress,
});
addContract.mutate(
{
contractAddress,
chainId: chain.id,
},
{
onSuccess: () => {
onAddSuccess();
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "success",
contractAddress,
});
},
onError: (err) => {
onAddError(err);
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "error",
contractAddress,
error: err,
});
},
},
);
}}
>
{addContract.isPending ? (
<Spinner className="size-4" />
) : (
<PlusIcon className="size-4" />
)}
Import contract
</Button>
<AddToProjectButton
contractAddress={contractAddress}
teamsAndProjects={teamsAndProjects}
chainId={chain.id.toString()}
client={client}
/>
);
};
function AddToProjectButton(props: {
contractAddress: string;
teamsAndProjects: MinimalTeamsAndProjects;
chainId: string;
client: ThirdwebClient;
}) {
return (
<Dialog>
<DialogTrigger asChild>
<Button className="gap-2" variant="outline">
<PlusIcon className="size-4" />
Add To Project
</Button>
</DialogTrigger>
<DialogContent className="gap-0 overflow-hidden p-0">
<DialogHeader className="p-6">
<DialogTitle>Add To Project</DialogTitle>
<DialogDescription>
Add the contract in a project's contract list on dashboard
</DialogDescription>
</DialogHeader>
<AddToProjectModalContent
teamsAndProjects={props.teamsAndProjects}
chainId={props.chainId}
contractAddress={props.contractAddress}
client={props.client}
/>
</DialogContent>
</Dialog>
);
}
function AddToProjectModalContent(props: {
teamsAndProjects: MinimalTeamsAndProjects;
chainId: string;
contractAddress: string;
client: ThirdwebClient;
}) {
const trackEvent = useTrack();
const addContractToProject = useAddContractToProject();
const [importSelection, setImportSelection] = useState({
team: props.teamsAndProjects[0]?.team,
project: props.teamsAndProjects[0]?.projects[0],
});
const selectedTeam = props.teamsAndProjects.find(
(t) => t.team.id === importSelection.team?.id,
);
function handleImport(params: {
teamId: string;
projectId: string;
}) {
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "attempt",
contractAddress: props.contractAddress,
});
addContractToProject.mutate(
{
contractAddress: props.contractAddress,
teamId: params.teamId,
projectId: params.projectId,
chainId: props.chainId,
},
{
onSuccess: () => {
toast.success("Contract added to the project successfully");
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "success",
contractAddress: props.contractAddress,
});
},
onError: (err) => {
if (err.message.includes("PROJECT_CONTRACT_ALREADY_EXISTS")) {
toast.error("Contract is already added to the project");
} else {
toast.error("Failed to import contract");
}
trackEvent({
category: TRACKING_CATEGORY,
action: "add-to-dashboard",
label: "error",
contractAddress: props.contractAddress,
error: err,
});
},
},
);
}
const isImportEnabled = !!importSelection.project && !!importSelection.team;
return (
<div>
<div className="flex flex-col gap-4 border-border border-t px-6 py-6">
<AddToProjectSelector
selection={importSelection}
onSelectionChange={setImportSelection}
teamsAndProjects={props.teamsAndProjects}
client={props.client}
/>
{/* No projects alert */}
{selectedTeam?.projects.length === 0 && (
<Alert variant="info">
<CircleAlertIcon className="size-5" />
<AlertTitle> Selected team has no projects </AlertTitle>
<AlertDescription>
<UnderlineLink
href={`/team/${selectedTeam.team.slug}`}
className="inline-flex items-center gap-2"
target="_blank"
>
Create Project <ExternalLinkIcon className="size-3.5" />
</UnderlineLink>
</AlertDescription>
</Alert>
)}
</div>
<div className="mt-3 flex justify-end border-border border-t bg-card p-6">
<Button
disabled={!isImportEnabled}
className="gap-2"
onClick={() => {
if (!importSelection.team || !importSelection.project) {
return;
}
handleImport({
teamId: importSelection.team.id,
projectId: importSelection.project.id,
});
}}
>
{addContractToProject.isPending ? (
<Spinner className="size-4" />
) : (
<PlusIcon className="size-4" />
)}
Add to Project
</Button>
</div>
</div>
);
}
@@ -24,6 +24,11 @@ export function getContractPageSidebarLinks(data: {
hide: !data.metadata.isModularCore,
exactMatch: true,
},
{
label: "Cross Chain (Beta)",
href: `${layoutPrefix}/cross-chain`,
exactMatch: true,
},
{
label: "Code Snippets",
href: `${layoutPrefix}/code`,
@@ -126,8 +126,11 @@ function UniqueWalletsChart(props: ChartProps) {
return (
<ThirdwebBarChart
title="Unique Wallets"
description="The number of unique wallet addresses that have sent a transaction to this contract."
header={{
title: "Unique Wallets",
description:
"The number of unique wallet addresses that have sent a transaction to this contract.",
}}
data={analyticsQuery.data || []}
isPending={analyticsQuery.isPending}
config={{
@@ -151,8 +154,11 @@ function TotalTransactionsChart(props: ChartProps) {
return (
<ThirdwebBarChart
title="Total Transactions"
description="The number of transactions that have been sent to this contract."
header={{
title: "Total Transactions",
description:
"The number of transactions that have been sent to this contract.",
}}
data={analyticsQuery.data || []}
isPending={analyticsQuery.isPending}
config={{
@@ -171,8 +177,11 @@ function TotalEventsChart(props: ChartProps) {
return (
<ThirdwebBarChart
title="Total Events"
description="The number of on-chain events that have been emitted from this contract."
header={{
title: "Total Events",
description:
"The number of on-chain events that have been emitted from this contract.",
}}
data={analyticsQuery.data || []}
isPending={analyticsQuery.isPending}
config={{
@@ -216,8 +225,11 @@ function FunctionBreakdownChart(
return (
<ThirdwebBarChart
title="Function Breakdown"
description="The breakdown of calls to each write function from transactions."
header={{
title: "Function Breakdown",
description:
"The breakdown of calls to each write function from transactions.",
}}
data={mappedQueryData || []}
isPending={analyticsQuery.isPending}
config={Object.keys(mappedQueryData?.[0] || {}).reduce(
@@ -269,8 +281,10 @@ function EventBreakdownChart(
return (
<ThirdwebBarChart
title="Event Breakdown"
description="The breakdown of events emitted by this contract."
header={{
title: "Event Breakdown",
description: "The breakdown of events emitted by this contract.",
}}
data={mappedQueryData || []}
isPending={analyticsQuery.isPending}
config={Object.keys(mappedQueryData?.[0] || {}).reduce(
@@ -319,7 +333,7 @@ const AnalyticsStat: React.FC<AnalyticsStatProps> = ({
const AnalyticsSkeleton: React.FC<{ label: string }> = ({ label }) => {
return (
<Card as={Stat}>
<Card as={Stat} className="bg-card">
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
<Skeleton isLoaded={false}>
<StatNumber>{0}</StatNumber>
@@ -350,7 +364,7 @@ const AnalyticsData: React.FC<AnalyticsStatProps> = ({
}, [totalQuery.data]);
return (
<Card as={Stat}>
<Card as={Stat} className="bg-card">
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
<Skeleton isLoaded={totalQuery.isFetched}>
<StatNumber>{data.toLocaleString()}</StatNumber>
@@ -0,0 +1,389 @@
"use client";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import {
type ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { verifyContract } from "app/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/ContractSourcesPage";
import {
type DeployModalStep,
DeployStatusModal,
useDeployStatusModal,
} from "components/contract-components/contract-deploy-form/deploy-context-modal";
import { useTxNotifications } from "hooks/useTxNotifications";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import {
defineChain,
eth_getCode,
getRpcClient,
prepareTransaction,
sendAndConfirmTransaction,
} from "thirdweb";
import type {
FetchDeployMetadataResult,
ThirdwebContract,
} from "thirdweb/contract";
import {
deployContractfromDeployMetadata,
getOrDeployInfraForPublishedContract,
} from "thirdweb/deploys";
import { useActiveAccount, useSwitchActiveWalletChain } from "thirdweb/react";
import { concatHex, padHex } from "thirdweb/utils";
import { z } from "zod";
import { SingleNetworkSelector } from "./single-network-selector";
type CrossChain = {
id: number;
network: string;
chainId: number;
status: "DEPLOYED" | "NOT_DEPLOYED";
};
const formSchema = z.object({
amounts: z.object({
"84532": z.string(),
"11155420": z.string(),
"919": z.string(),
"111557560": z.string(),
"999999999": z.string(),
"11155111": z.string(),
"421614": z.string(),
}),
});
type FormSchema = z.output<typeof formSchema>;
export function DataTable({
data,
coreMetadata,
coreContract,
modulesMetadata,
initializeData,
inputSalt,
initCode,
isDirectDeploy,
}: {
data: CrossChain[];
coreMetadata: FetchDeployMetadataResult;
coreContract: ThirdwebContract;
modulesMetadata?: FetchDeployMetadataResult[];
initializeData?: `0x${string}`;
inputSalt?: `0x${string}`;
initCode?: `0x${string}`;
isDirectDeploy: boolean;
}) {
const activeAccount = useActiveAccount();
const switchChain = useSwitchActiveWalletChain();
const deployStatusModal = useDeployStatusModal();
const { onError } = useTxNotifications(
"Successfully deployed contract",
"Failed to deploy contract",
);
const addRowMutation = useMutation({
mutationFn: async (chain: { chainId: number; name: string }) => {
const code = await eth_getCode(
getRpcClient({
client: getThirdwebClient(),
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(chain.chainId),
}),
{ address: coreContract.address },
);
const newRow: CrossChain = {
id: chain.chainId,
network: chain.name,
chainId: chain.chainId,
status: code?.length > 2 ? "DEPLOYED" : "NOT_DEPLOYED",
};
if (!customChainData.some((row) => row.chainId === chain.chainId)) {
setCustomChainData((prevData) => [...prevData, newRow]);
}
},
});
const [customChainData, setCustomChainData] = useState<CrossChain[]>(() => {
try {
const storedData = window.localStorage.getItem(
`crosschain-${coreContract.address}`,
);
return storedData ? JSON.parse(storedData) : [];
} catch (error) {
console.error("Failed to read from localStorage", error);
return [];
}
});
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
try {
window.localStorage.setItem(
`crosschain-${coreContract.address}`,
JSON.stringify(customChainData),
);
} catch (error) {
console.error("Failed to write to localStorage", error);
}
}, [customChainData, coreContract.address]);
const mergedChainData = useMemo(() => {
const chainMap = new Map<number, CrossChain>();
for (const item of [...data, ...customChainData]) {
chainMap.set(item.chainId, item); // Deduplicate by chainId
}
return Array.from(chainMap.values());
}, [data, customChainData]);
const form = useForm<FormSchema>({
resolver: zodResolver(formSchema),
values: {
amounts: {
"84532": "", // Base
"11155420": "", // OP testnet
"919": "", // Mode Network
"111557560": "", // Cyber
"999999999": "", // Zora
"11155111": "", // Sepolia
"421614": "",
},
},
});
const columns: ColumnDef<CrossChain>[] = [
{
accessorKey: "network",
header: "Network",
cell: ({ row }) => {
if (row.getValue("status") === "DEPLOYED") {
return (
<Link
target="_blank"
className="text-blue-500 underline"
href={`/${row.getValue("chainId")}/${coreContract.address}`}
>
{row.getValue("network")}
</Link>
);
}
return row.getValue("network");
},
},
{
accessorKey: "chainId",
header: "Chain ID",
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
if (row.getValue("status") === "DEPLOYED") {
return <p>Deployed</p>;
}
return (
<Button
type="button"
onClick={() => deployContract(row.getValue("chainId"))}
>
Deploy
</Button>
);
},
},
];
const table = useReactTable({
data: mergedChainData,
columns,
getCoreRowModel: getCoreRowModel(),
});
const deployContract = async (chainId: number) => {
try {
if (!activeAccount) {
throw new Error("No active account");
}
// eslint-disable-next-line no-restricted-syntax
const chain = defineChain(chainId);
const client = getThirdwebClient();
const salt =
inputSalt || concatHex(["0x03", padHex("0x", { size: 31 })]).toString();
await switchChain(chain);
const steps: DeployModalStep[] = [
{
type: "deploy",
signatureCount: 1,
},
];
deployStatusModal.setViewContractLink("");
deployStatusModal.open(steps);
let crosschainContractAddress: string | undefined;
if (initCode && isDirectDeploy) {
const tx = prepareTransaction({
client,
chain,
to: "0x4e59b44847b379578588920cA78FbF26c0B4956C",
data: initCode,
});
await sendAndConfirmTransaction({
transaction: tx,
account: activeAccount,
});
const code = await eth_getCode(
getRpcClient({
client,
chain,
}),
{
address: coreContract.address,
},
);
if (code && code.length > 2) {
crosschainContractAddress = coreContract.address;
}
} else {
crosschainContractAddress = await deployContractfromDeployMetadata({
account: activeAccount,
chain,
client,
deployMetadata: coreMetadata,
isCrosschain: true,
initializeData,
salt,
});
verifyContract({
address: crosschainContractAddress as `0x${string}`,
chain,
client,
});
if (modulesMetadata) {
for (const m of modulesMetadata) {
await getOrDeployInfraForPublishedContract({
chain,
client,
account: activeAccount,
contractId: m.name,
publisher: m.publisher,
});
}
}
}
deployStatusModal.nextStep();
deployStatusModal.setViewContractLink(
`/${chain.id}/${crosschainContractAddress}`,
);
deployStatusModal.close();
setCustomChainData((prevData) =>
prevData.map((row) =>
row.chainId === chainId ? { ...row, status: "DEPLOYED" } : row,
),
);
} catch (e) {
onError(e);
console.error("failed to deploy contract", e);
deployStatusModal.close();
}
};
return (
<Form {...form}>
<form>
<div
style={{
maxHeight: "500px",
overflowY: "auto",
}}
>
<TableContainer>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
<DeployStatusModal deployStatusModal={deployStatusModal} />
</TableContainer>
</div>
{addRowMutation.isPending && (
<div className="my-4 animate-pulse text-center text-blue-500">
Processing...
</div>
)}
{addRowMutation.isError && (
<div className="my-4 text-center text-red-500">
Error while adding selected chain!
</div>
)}
<div className="mt-4">
<SingleNetworkSelector
onAddRow={addRowMutation.mutate}
isAddingRow={addRowMutation.isPending}
className="w-full"
/>
</div>
</form>
</Form>
);
}
@@ -0,0 +1,17 @@
import Link from "next/link";
export function NoCrossChainPrompt() {
return (
<div>
Multi-chain deployments are not available for this contract (or chain).
<br />
Please ensure the chain is supported or deploy a new contract to enable
this functionality.
<br />
<br />
<Link href="/explore" target="_blank" className="text-blue-500 underline">
Explore contracts
</Link>
</div>
);
}
@@ -0,0 +1,282 @@
import { fetchPublishedContractsFromDeploy } from "components/contract-components/fetchPublishedContractsFromDeploy";
import { notFound } from "next/navigation";
import {
type ContractOptions,
eth_getTransactionByHash,
eth_getTransactionReceipt,
getContractEvents,
parseEventLogs,
prepareEvent,
} from "thirdweb";
import { defineChain, getChainMetadata } from "thirdweb/chains";
import {
type FetchDeployMetadataResult,
getContract,
getDeployedCloneFactoryContract,
resolveContractAbi,
} from "thirdweb/contract";
import { moduleInstalledEvent } from "thirdweb/modules";
import { eth_getCode, getRpcClient } from "thirdweb/rpc";
import type { TransactionReceipt } from "thirdweb/transaction";
import { type AbiFunction, decodeFunctionData } from "thirdweb/utils";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { DataTable } from "./data-table";
import { NoCrossChainPrompt } from "./no-crosschain-prompt";
export function getModuleInstallParams(mod: FetchDeployMetadataResult) {
return (
mod.abi
.filter((a) => a.type === "function")
.find((f) => f.name === "encodeBytesOnInstall")?.inputs || []
);
}
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
}) {
const ProxyDeployedEvent = prepareEvent({
signature:
"event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
});
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { contract } = info;
const isModularCore = (await getContractPageMetadata(contract)).isModularCore;
let twCloneFactoryContract: Readonly<
ContractOptions<[], `0x${string}`>
> | null = null;
try {
twCloneFactoryContract = await getDeployedCloneFactoryContract({
chain: contract.chain,
client: contract.client,
});
} catch {}
const originalCode = await eth_getCode(
getRpcClient({
client: contract.client,
chain: contract.chain,
}),
{
address: contract.address,
},
);
let initCode: `0x${string}` = "0x";
let creationTxReceipt: TransactionReceipt | undefined;
let isDirectDeploy = false;
try {
const res = await fetch(
`https://contract.thirdweb-dev.com/creation/${contract.chain.id}/${contract.address}`,
);
const creationData = await res.json();
if (creationData.status === "1" && creationData.result[0]?.txHash) {
const rpcClient = getRpcClient({
client: contract.client,
chain: contract.chain,
});
creationTxReceipt = await eth_getTransactionReceipt(rpcClient, {
hash: creationData.result[0]?.txHash,
});
const creationTx = await eth_getTransactionByHash(rpcClient, {
hash: creationData.result[0]?.txHash,
});
initCode = creationTx.input;
isDirectDeploy =
creationTx.to?.toLowerCase() ===
"0x4e59b44847b379578588920cA78FbF26c0B4956C".toLowerCase();
}
} catch (e) {
console.debug(e);
}
let initializeData: `0x${string}` | undefined;
let inputSalt: `0x${string}` | undefined;
let creationBlockNumber: bigint | undefined;
if (twCloneFactoryContract) {
const events = await getContractEvents({
contract: twCloneFactoryContract,
events: [ProxyDeployedEvent],
blockRange: 123456n,
});
const event = events.find(
(e) =>
e.args.proxy.toLowerCase() === params.contractAddress.toLowerCase(),
);
initializeData = event?.args.data;
inputSalt = event?.args.inputSalt;
creationBlockNumber = event?.blockNumber;
}
const defaultChains = [
1, 137, 8453, 10, 42161, 11155111, 84532, 11155420, 421614,
];
const chainsDeployedOn = (
await Promise.all(
defaultChains.map(async (c) => {
// eslint-disable-next-line no-restricted-syntax
const chain = defineChain(c);
try {
const chainMetadata = await getChainMetadata(chain);
const rpcRequest = getRpcClient({
client: contract.client,
chain,
});
const code = await eth_getCode(rpcRequest, {
address: params.contractAddress,
});
return {
id: chain.id,
network: chainMetadata.name,
chainId: chain.id,
status:
code === originalCode
? ("DEPLOYED" as const)
: ("NOT_DEPLOYED" as const),
};
} catch {
return {
id: chain.id,
network: "",
chainId: chain.id,
status: "NOT_DEPLOYED" as const,
};
}
}),
)
).filter((c) => c.status === "DEPLOYED");
let coreMetadata: FetchDeployMetadataResult | undefined;
try {
coreMetadata = (
await fetchPublishedContractsFromDeploy({
contract,
client: contract.client,
})
).at(-1) as FetchDeployMetadataResult;
} catch {}
if (!coreMetadata) {
return NoCrossChainPrompt();
}
let modulesMetadata: FetchDeployMetadataResult[] | undefined;
if (isModularCore) {
let modules: string[] | undefined;
const moduleEvent = moduleInstalledEvent();
// extract module address in ModuleInstalled events from transaction receipt
if (creationTxReceipt) {
const decodedEvent = parseEventLogs({
events: [moduleEvent],
logs: creationTxReceipt.logs,
});
modules = decodedEvent.map((e) => e.args.installedModule);
}
// fetch events from contract
if (!modules && creationBlockNumber) {
const events = await getContractEvents({
contract: contract,
events: [moduleEvent],
blockRange: 123456n,
});
const filteredEvents = events.filter(
(e) => e.blockNumber === creationBlockNumber,
);
modules = filteredEvents.map((e) => e.args.installedModule);
}
// if receipt not available, try extracting module address from initialize data
if (!modules && initializeData) {
// biome-ignore lint/suspicious/noExplicitAny: FIXME
const decodedData: any = await decodeFunctionData({
contract,
data: initializeData,
});
const abi = await resolveContractAbi(contract).catch(() => undefined);
if (abi) {
const initializeFunction = abi.find(
(i: AbiFunction) => i.type === "function" && i.name === "initialize",
) as unknown as AbiFunction;
const moduleIndex = initializeFunction.inputs.findIndex(
(i) => i.name === "_modules" || i.name === "modules",
);
modules = moduleIndex ? decodedData[moduleIndex] : undefined;
}
}
modulesMetadata = modules
? ((await Promise.all(
modules.map(async (m) =>
(
await fetchPublishedContractsFromDeploy({
contract: getContract({
chain: contract.chain,
client: contract.client,
address: m,
}),
client: contract.client,
})
).at(-1),
),
)) as FetchDeployMetadataResult[])
: undefined;
}
if (!isDirectDeploy && !initializeData) {
return NoCrossChainPrompt();
}
return (
<>
<div>
<h2 className="mb-1 font-bold text-2xl tracking-tight">
Deploy Cross-chain
</h2>
<p className="text-muted-foreground">
Deterministically deploy your contracts on multiple networks.
</p>
</div>
<div className="h-10" />
<DataTable
coreMetadata={coreMetadata}
modulesMetadata={modulesMetadata}
initializeData={initializeData}
inputSalt={inputSalt}
data={chainsDeployedOn}
coreContract={contract}
initCode={initCode}
isDirectDeploy={isDirectDeploy}
/>
</>
);
}
@@ -0,0 +1,90 @@
import { SelectWithSearch } from "@/components/blocks/select-with-search";
import { Badge } from "@/components/ui/badge";
import { ChainIcon } from "components/icons/ChainIcon";
import { useAllChainsData } from "hooks/chains/allChains";
import { useCallback, useMemo } from "react";
type Option = { label: string; value: string };
export function SingleNetworkSelector(props: {
onAddRow: (chain: { chainId: number; name: string }) => void;
isAddingRow: boolean;
className?: string;
}) {
const { allChains, idToChain } = useAllChainsData();
const options = useMemo(() => {
return allChains.map((chain) => ({
label: chain.name,
value: String(chain.chainId),
}));
}, [allChains]);
const searchFn = useCallback(
(option: Option, searchValue: string) => {
const chain = idToChain.get(Number(option.value));
if (!chain) {
return false;
}
if (Number.isInteger(Number.parseInt(searchValue))) {
return String(chain.chainId).startsWith(searchValue);
}
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
},
[idToChain],
);
const renderOption = useCallback(
(option: Option) => {
const chain = idToChain.get(Number(option.value));
if (!chain) {
return option.label;
}
return (
<div className="flex justify-between gap-4">
<span className="flex grow gap-2 truncate text-left">
<ChainIcon
className="size-5"
ipfsSrc={chain.icon?.url}
loading="lazy"
/>
{chain.name}
</span>
<Badge variant="outline" className="gap-2 max-sm:hidden">
<span className="text-muted-foreground">Chain ID</span>
{chain.chainId}
</Badge>
</div>
);
},
[idToChain],
);
const handleChange = async (chainId: string) => {
const chain = idToChain.get(Number(chainId));
if (chain) {
props.onAddRow({ chainId: chain.chainId, name: chain.name });
}
};
return (
<SelectWithSearch
searchPlaceholder="Search by Name or Chain ID"
value={undefined}
options={options}
onValueChange={handleChange}
placeholder={
allChains.length === 0
? "Loading chains..."
: "Select chain to deploy on"
}
overrideSearchFn={searchFn}
renderOption={renderOption}
className={props.className}
disabled={props.isAddingRow || allChains.length === 0}
closeOnSelect={true}
/>
);
}
@@ -1,8 +1,12 @@
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractMetadata } from "thirdweb/extensions/common";
import { isAddress, isContractDeployed } from "thirdweb/utils";
import type { MinimalTeamsAndProjects } from "../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
@@ -38,11 +42,16 @@ export default async function Layout(props: {
notFound();
}
const client = getThirdwebClient();
const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
if (contract.chain.id === localhost.id) {
return (
<ContractPageLayoutClient
chainMetadata={chainMetadata}
contract={contract}
teamsAndProjects={teamsAndProjects}
client={client}
>
{props.children}
</ContractPageLayoutClient>
@@ -73,12 +82,43 @@ export default async function Layout(props: {
sidebarLinks={sidebarLinks}
dashboardContractMetadata={contractMetadata}
externalLinks={externalLinks}
teamsAndProjects={teamsAndProjects}
client={client}
>
{props.children}
</ContractPageLayout>
);
}
async function getTeamsAndProjectsIfLoggedIn() {
try {
const teams = await getTeams();
if (!teams) {
return undefined;
}
const teamsAndProjects: MinimalTeamsAndProjects = await Promise.all(
teams.map(async (team) => ({
team: {
id: team.id,
name: team.name,
slug: team.slug,
image: team.image,
},
projects: (await getProjects(team.slug)).map((x) => ({
id: x.id,
name: x.name,
})),
})),
);
return teamsAndProjects;
} catch {
return undefined;
}
}
export async function generateMetadata(props: {
params: Promise<{
contractAddress: string;
@@ -1,6 +1,10 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
import {
fetchLatestPublishedContractVersion,
fetchPublishedContractVersions,
} from "components/contract-components/fetch-contracts-with-versions";
import { isAddress } from "thirdweb";
import type { FetchDeployMetadataResult } from "thirdweb/contract";
import { resolveAddress } from "thirdweb/extensions/ens";
import invariant from "tiny-invariant";
import type { ModuleMeta } from "./install-module-params";
@@ -14,16 +18,24 @@ export async function getModuleInstalledParams(ext: ModuleMeta) {
client: getThirdwebClient(),
name: ext.publisherAddress,
});
const allPublishedModules = await fetchPublishedContractVersions(
publisherAddress,
ext.moduleName,
);
// find the version we want
const publishedModule =
ext.moduleVersion === "latest"
? allPublishedModules[0]
: allPublishedModules.find((v) => v.version === ext.moduleVersion);
let publishedModule: FetchDeployMetadataResult | undefined = undefined;
if (ext.moduleVersion === "latest") {
publishedModule = await fetchLatestPublishedContractVersion(
publisherAddress,
ext.moduleName,
);
} else {
const allPublishedModules = await fetchPublishedContractVersions(
publisherAddress,
ext.moduleName,
);
publishedModule = allPublishedModules.find(
(v) => v.version === ext.moduleVersion,
);
}
invariant(
publishedModule,
@@ -135,7 +135,7 @@ function OverviewAnalytics(props: ChartProps) {
data={mergedData || []}
isPending={isPending}
showLegend
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
chartClassName="aspect-[1.5] lg:aspect-[3.5]"
/>
);
}
@@ -25,6 +25,7 @@ export function ContractSettingsPageClient(props: {
contract={props.contract}
functionSelectors={metadataQuery.data.functionSelectors}
twAccount={props.twAccount}
hasDefaultFeeConfig={true}
/>
);
}
@@ -79,8 +79,9 @@ export function ContractSettingsPage(props: {
contract: ThirdwebContract;
functionSelectors: string[];
twAccount: Account | undefined;
hasDefaultFeeConfig: boolean;
}) {
const { functionSelectors, contract, twAccount } = props;
const { functionSelectors, contract, twAccount, hasDefaultFeeConfig } = props;
return (
<ContractSettingsPageInner
contract={contract}
@@ -96,10 +97,13 @@ export function ContractSettingsPage(props: {
CommonExt.isGetDefaultRoyaltyInfoSupported(functionSelectors),
CommonExt.isSetDefaultRoyaltyInfoSupported(functionSelectors),
].every(Boolean)}
isPlatformFeesSupported={[
CommonExt.isGetPlatformFeeInfoSupported(functionSelectors),
CommonExt.isSetPlatformFeeInfoSupported(functionSelectors),
].every(Boolean)}
isPlatformFeesSupported={
!hasDefaultFeeConfig &&
[
CommonExt.isGetPlatformFeeInfoSupported(functionSelectors),
CommonExt.isSetPlatformFeeInfoSupported(functionSelectors),
].every(Boolean)
}
twAccount={twAccount}
/>
);
@@ -132,9 +132,8 @@ export const SettingsPlatformFees = ({
<SolidityInput
solidityType="address"
formContext={form}
variant="filled"
{...form.register("platform_fee_recipient")}
isDisabled={!address || sendAndConfirmTx.isPending}
disabled={!address || sendAndConfirmTx.isPending}
/>
<FormErrorMessage>
{
@@ -155,7 +154,6 @@ export const SettingsPlatformFees = ({
>
<FormLabel>Percentage</FormLabel>
<BasisPointsInput
variant="filled"
value={form.watch("platform_fee_basis_points")}
onChange={(value) =>
form.setValue("platform_fee_basis_points", value, {
@@ -163,7 +161,7 @@ export const SettingsPlatformFees = ({
shouldTouch: true,
})
}
isDisabled={sendAndConfirmTx.isPending}
disabled={sendAndConfirmTx.isPending}
/>
<FormErrorMessage>
{
@@ -128,10 +128,9 @@ export const SettingsPrimarySale = ({
>
<FormLabel>Recipient Address</FormLabel>
<SolidityInput
isDisabled={mutation.isPending || !address}
disabled={mutation.isPending || !address}
solidityType="address"
formContext={form}
variant="filled"
{...form.register("primary_sale_recipient")}
/>
<FormErrorMessage>
@@ -144,9 +144,8 @@ export const SettingsRoyalties = ({
<SolidityInput
solidityType="address"
formContext={form}
variant="filled"
{...form.register("fee_recipient")}
isDisabled={!address}
disabled={!address}
/>
<FormErrorMessage>
{
@@ -165,7 +164,6 @@ export const SettingsRoyalties = ({
>
<FormLabel>Percentage</FormLabel>
<BasisPointsInput
variant="filled"
value={form.watch("seller_fee_basis_points")}
onChange={(value) =>
form.setValue("seller_fee_basis_points", value, {
@@ -1,5 +1,7 @@
import { DEFAULT_FEE_RECIPIENT } from "constants/addresses";
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getPlatformFeeInfo } from "thirdweb/extensions/common";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
@@ -33,11 +35,19 @@ export default async function Page(props: {
getContractPageMetadata(info.contract),
]);
let hasDefaultFeeConfig = true;
try {
const feeInfo = await getPlatformFeeInfo({ contract });
hasDefaultFeeConfig =
feeInfo[0].toLowerCase() === DEFAULT_FEE_RECIPIENT.toLowerCase();
} catch {}
return (
<ContractSettingsPage
contract={info.contract}
functionSelectors={metadata.functionSelectors}
twAccount={account}
hasDefaultFeeConfig={hasDefaultFeeConfig}
/>
);
}
@@ -74,6 +74,7 @@ import zytronCTA from "./temp-assets/zytronCTA.jpg";
import { API_SERVER_URL } from "@/constants/env";
import type { ChainMetadata } from "thirdweb/chains";
import { getGasSponsoredChains } from "../../../@/api/chain";
import type {
ChainMetadataWithServices,
ChainService,
@@ -133,7 +134,6 @@ export async function getChain(
type ExtraChainMetadata = Partial<{
headerImgUrl: string;
about: string;
gasSponsored: boolean;
cta: ChainCTAProps;
}>;
@@ -167,34 +167,6 @@ const chainMetaRecord = {
headerImgUrl: baseBanner.src,
about:
"Base is a secure, low-cost, builder-friendly Ethereum L2 built to bring the next billion users onchain.",
gasSponsored: true,
cta: OP_CTA,
},
//celo Mainnet
42220: {
cta: OP_CTA,
gasSponsored: true,
},
// optimism
10: {
cta: OP_CTA,
gasSponsored: true,
},
// fraxtal
252: {
cta: OP_CTA,
gasSponsored: true,
},
// zora
7777777: {
cta: OP_CTA,
gasSponsored: true,
},
// mode
34443: {
cta: OP_CTA,
gasSponsored: true,
},
// cyber
7560: {
@@ -207,21 +179,13 @@ const chainMetaRecord = {
buttonLink: "https://cyber.co/",
buttonText: "Learn more",
},
gasSponsored: true,
},
// redstone
690: {
cta: OP_CTA,
gasSponsored: true,
},
// ancient8
888888888: {
about:
"Ancient8 is building an ETH gaming Layer 2 built with OP Stack, offering a suite of Web3 gaming infrastructure tools that serve as the distribution and marketing channel for games globally. With Space3 Game Publishing Platform, Ancient8 Gaming Guild, Reneverse Web3 Ads engine, A8ID, and Gosu Network, Ancient8 is dedicated to onboard millions of gamers to Web3 gaming, while providing unparalleled support to game developers looking to reach more players. Ancient8’s products have helped 100+ Web3 games and 200K+ users better navigate Web3.\n\nAncient8 has raised $10M in total financing from leading investors including Pantera, Dragonfly, Hashed, Makers Fund, Mechanism, Coinbase, IOSG, Jump and Animoca.",
// TODO: add CTA
headerImgUrl: ancient8Banner.src,
cta: OP_CTA,
gasSponsored: true,
},
// mantle
5000: {
@@ -233,7 +197,6 @@ const chainMetaRecord = {
buttonText: "Learn More",
buttonLink: "https://x.com/0xMantleDevs",
},
gasSponsored: true,
},
// Flare
14: {
@@ -252,18 +215,12 @@ const chainMetaRecord = {
buttonText: "Learn more",
},
},
//Superseed
5330: {
cta: OP_CTA,
gasSponsored: true,
},
// Ink
57073: {
headerImgUrl: inkBanner.src,
about:
"Ink is an Ethereum OP Stack layer 2 blockchain designed to be the house of DeFi for the Superchain, a powerful base layer for deploying innovative DeFi protocols.",
cta: OP_CTA,
gasSponsored: true,
},
// vanar
2040: {
@@ -341,11 +298,6 @@ const chainMetaRecord = {
buttonText: "Learn more",
},
},
//Arena Z Mainnet
7897: {
cta: OP_CTA,
gasSponsored: true,
},
//Appchain
466: {
headerImgUrl: appChainBanner.src,
@@ -399,8 +351,6 @@ const chainMetaRecord = {
headerImgUrl: thirdwebBanner.src,
about:
"Monetize your passion, your way. Discover the freedom to grow with our creator-focused platform.",
cta: OP_CTA,
gasSponsored: true,
},
//Etherlink Testnet
128123: {
@@ -554,33 +504,12 @@ const chainMetaRecord = {
headerImgUrl: liskBanner.src,
about:
"Lisk is focused on serving builders in high-growth markets like Africa and Southeast Asia. When you build on Lisk, you become part of the strongest Ethereum collective, alongside Coinbase, Sony, World, Kraken and Uniswap -- the OP Superchain.",
// cta: {
// backgroundImageUrl: liskCTA.src,
// title: "Ready to reshape the world?",
// buttonLink: "https://lisk.com",
// buttonText: "Learn more",
// },
cta: OP_CTA,
gasSponsored: true,
},
//Metal L2
1750: {
headerImgUrl: metalBanner.src,
about:
"Metal L2 is a banking-focused Layer 2 blockchain built on the Optimism Superchain, designed to enable direct on-chain fiat deposits through its connection to The Digital Banking Network—an open-source blockchain banking protocol developed by Metallicus",
// cta: {
// backgroundImageUrl: metalCTA.src,
// title: "Connect to The Banking Layer, Metal L2",
// buttonLink: "https://metall2.com/",
// buttonText: "Connect Here",
// },
cta: OP_CTA,
gasSponsored: true,
},
//Ozean Testnet
7849306: {
cta: OP_CTA,
gasSponsored: true,
},
//0G-Newton-Testnet
16600: {
@@ -678,11 +607,6 @@ const chainMetaRecord = {
buttonText: "Learn More",
},
},
//Unichain Sepolis Testnet
1301: {
cta: OP_CTA,
gasSponsored: true,
},
//viction Mainnet
88: {
headerImgUrl: victionBanner.src,
@@ -714,14 +638,6 @@ const chainMetaRecord = {
headerImgUrl: worldChainBanner.src,
about:
"World Chain is the blockchain for real humans, offering priority blockspace and gas-free transactions for World ID-verified users.",
// cta: {
// backgroundImageUrl: worldChainCTA.src,
// title: "Build for real humans with World Chain",
// buttonLink: "https://world.org/world-chain",
// buttonText: "Learn More",
// },
cta: OP_CTA,
gasSponsored: true,
},
//World Chain Testnet
4801: {
@@ -848,14 +764,12 @@ const chainMetaRecord = {
headerImgUrl: soneiumBanner.src,
about:
"Soneium, an Ethereum layer-2 developed by Sony Block Solutions Labs. This versatile chain is a general-purpose blockchain platform that aims to evoke emotion, empower creativity, and meet diverse needs to go mainstream. Soneium will be simplifying blockchain experiences while empowering developers, creators, and communities. Built on accessibility, scalability, and efficiency, it aims to solve real-world problems across industries globally. Soneium will change the way we interact with the internet, opening up a world of innovative applications and unlimited potential for users worldwide.",
cta: OP_CTA,
},
//Soneium Mainnet
1868: {
headerImgUrl: soneiumBanner.src,
about:
"Soneium, an Ethereum layer-2 developed by Sony Block Solutions Labs. This versatile chain is a general-purpose blockchain platform that aims to evoke emotion, empower creativity, and meet diverse needs to go mainstream. Soneium will be simplifying blockchain experiences while empowering developers, creators, and communities. Built on accessibility, scalability, and efficiency, it aims to solve real-world problems across industries globally. Soneium will change the way we interact with the internet, opening up a world of innovative applications and unlimited potential for users worldwide.",
cta: OP_CTA,
},
//Shido
9008: {
@@ -874,10 +788,23 @@ const chainMetaRecord = {
export async function getChainMetadata(
chainId: number,
): Promise<ExtraChainMetadata | null> {
): Promise<(ExtraChainMetadata & { gasSponsored?: true }) | null> {
const gasSponsoredChains = await getGasSponsoredChains();
const isGasSponsored = gasSponsoredChains.includes(chainId);
// TODO: fetch this from the API
if (chainId in chainMetaRecord) {
return chainMetaRecord[chainId as keyof typeof chainMetaRecord];
return {
...(isGasSponsored ? { cta: OP_CTA, gasSponsored: true } : {}),
// this will OVERRIDE the op CTA if there is a custom one configured
...chainMetaRecord[chainId as keyof typeof chainMetaRecord],
};
} else if (isGasSponsored) {
return {
cta: OP_CTA,
gasSponsored: true,
};
}
return null;
}
@@ -5,7 +5,7 @@ import { ContractPublishForm } from "components/contract-components/contract-pub
import { revalidatePath } from "next/cache";
import { notFound, redirect } from "next/navigation";
import { fetchDeployMetadata } from "thirdweb/contract";
import { getPublishedContractsWithPublisherMapping } from "../../../published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping";
import { getLatestPublishedContractsWithPublisherMapping } from "../../../published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping";
type DirectDeployPageProps = {
params: Promise<{
@@ -46,18 +46,12 @@ export default async function PublishContractPage(
// - get the publish metadata with name+publisher address
// - merge the two objects with publishMetadataFromUri taking higher precedence
if (!publishMetadataFromUri.version) {
const publishedContractVersions =
await getPublishedContractsWithPublisherMapping({
const publishedContract =
await getLatestPublishedContractsWithPublisherMapping({
publisher: address,
contract_id: publishMetadataFromUri.name,
});
if (!publishedContractVersions) {
notFound();
}
const publishedContract = publishedContractVersions[0];
if (publishedContract) {
publishMetadata = {
...publishedContract,
@@ -1,8 +1,6 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { fetchPublishedContracts } from "components/contract-components/fetchPublishedContracts";
import { DeployedContracts } from "components/contract-components/tables/deployed-contracts";
import { Suspense } from "react";
import { getSortedDeployedContracts } from "../../../account/contracts/_components/getSortedDeployedContracts";
import { ProfileHeader } from "./components/profile-header";
import { PublishedContracts } from "./components/published-contracts";
@@ -32,36 +30,10 @@ export function ProfileUI(props: {
</div>
<div className="h-12" />
<div>
<h2 className="font-semibold text-2xl tracking-tight">
Deployed contracts
</h2>
<p className="text-muted-foreground">
List of contracts deployed across all Mainnets
</p>
<div className="h-4" />
<Suspense fallback={<LoadingSection />}>
<AsyncDeployedContracts profileAddress={profileAddress} />
</Suspense>
</div>
</div>
);
}
async function AsyncDeployedContracts(props: {
profileAddress: string;
}) {
const contracts = await getSortedDeployedContracts({
address: props.profileAddress,
onlyMainnet: true,
});
return <DeployedContracts contractList={contracts} limit={50} />;
}
async function AsyncPublishedContracts(props: {
publisherAddress: string;
publisherEnsName: string | undefined;
@@ -1,6 +1,5 @@
import { notFound } from "next/navigation";
import { PublishedContractBreadcrumbs } from "./components/breadcrumbs.client";
import { getPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
import { getLatestPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
type Params = { publisher: string; contract_id: string };
@@ -20,16 +19,11 @@ export async function generateMetadata(props: { params: Promise<Params> }) {
const params = await props.params;
const { publisher, contract_id } = params;
const publishedContracts = await getPublishedContractsWithPublisherMapping({
publisher: publisher,
contract_id: contract_id,
});
if (!publishedContracts) {
notFound();
}
const publishedContract = publishedContracts[0];
const publishedContract =
await getLatestPublishedContractsWithPublisherMapping({
publisher: publisher,
contract_id: contract_id,
});
if (!publishedContract) {
return {
@@ -3,7 +3,7 @@ import { format } from "date-fns/format";
import { resolveEns } from "lib/ens";
import { correctAndUniqueLicenses } from "lib/licenses";
import { getSocialProfiles } from "thirdweb/social";
import { getPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
import { getLatestPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
import { publishedContractOGImageTemplate } from "./utils/publishedContractOGImageTemplate";
export const runtime = "edge";
@@ -22,8 +22,8 @@ export default async function Image(props: {
const client = getThirdwebClient();
const { publisher, contract_id } = props.params;
const [publishedContracts, socialProfiles] = await Promise.all([
getPublishedContractsWithPublisherMapping({
const [publishedContract, socialProfiles] = await Promise.all([
getLatestPublishedContractsWithPublisherMapping({
publisher: publisher,
contract_id: contract_id,
}),
@@ -42,12 +42,6 @@ export default async function Image(props: {
};
})();
if (!publishedContracts) {
return null;
}
const publishedContract = publishedContracts[0];
if (!publishedContract) {
return null;
}
@@ -1,5 +1,8 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
import {
fetchLatestPublishedContractVersion,
fetchPublishedContractVersions,
} from "components/contract-components/fetch-contracts-with-versions";
import { isAddress } from "thirdweb";
import { resolveAddress } from "thirdweb/extensions/ens";
@@ -36,3 +39,24 @@ export async function getPublishedContractsWithPublisherMapping(options: {
return undefined;
}
}
export async function getLatestPublishedContractsWithPublisherMapping(options: {
publisher: string;
contract_id: string;
}) {
const { publisher, contract_id } = options;
try {
// resolve ENS
const publisherAddress = isAddress(publisher)
? publisher
: await resolveAddress({
client: getThirdwebClient(),
name: mapThirdwebPublisher(publisher),
});
return fetchLatestPublishedContractVersion(publisherAddress, contract_id);
} catch {
return undefined;
}
}
@@ -1,41 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { shareLink } from "@/lib/shareLink";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
export function DeployActions(props: {
publisher: string;
contract_id: string;
version?: string;
displayName: string;
}) {
const searchparams = useSearchParams();
const stringifiedSearchParams = searchparams?.toString();
return (
<div className="flex gap-3">
<Button asChild variant="outline">
<Link
href={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}${stringifiedSearchParams ? `?${stringifiedSearchParams}` : ""}`}
target="_blank"
>
Contract
</Link>
</Button>
<Button
variant="outline"
onClick={() => {
shareLink({
title: `Deploy ${props.displayName}`,
});
}}
>
Share
</Button>
</div>
);
}
@@ -19,6 +19,7 @@ export function PublishedActions(props: {
<div className="flex gap-3">
<Button
variant="outline"
className="bg-card"
onClick={() => {
shareLink({
title: `Deploy ${props.displayName}`,
@@ -27,7 +28,7 @@ export function PublishedActions(props: {
>
Share
</Button>
<Button asChild variant="primary" className="gap-2">
<Button asChild className="gap-2">
<Link
href={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}/deploy${stringifiedSearchParams ? `?${stringifiedSearchParams}` : ""}`}
>
@@ -18,7 +18,7 @@ export function DeployContractInfo(props: {
<div className="flex flex-col gap-2">
<div className="flex flex-1 items-center gap-4">
{contractImageLink && (
<div className="hidden shrink-0 items-center justify-center rounded-xl border border-border p-2 md:flex">
<div className="hidden shrink-0 items-center justify-center rounded-xl border border-border bg-card p-2 md:flex">
{/*eslint-disable-next-line @next/next/no-img-element*/}
<img className="size-12" alt={props.name} src={contractImageLink} />
</div>
@@ -1,4 +1,3 @@
import { Separator } from "@/components/ui/separator";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import {
fetchPublishedContractVersion,
@@ -7,7 +6,6 @@ import {
import { isAddress } from "thirdweb";
import { fetchDeployMetadata } from "thirdweb/contract";
import { resolveAddress } from "thirdweb/extensions/ens";
import { DeployActions } from "./contract-actions-deploy.client";
import { DeployContractHeader } from "./contract-header";
import { DeployFormForUri } from "./uri-based-deploy";
@@ -73,23 +71,17 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
]);
return (
<>
<div className="mx-auto flex w-full max-w-[1000px] flex-col gap-8 pb-20">
<DeployContractHeader
{...props}
allVersions={publishedContractVersions}
activeVersion={publishedContract}
>
<DeployActions
{...props}
displayName={publishedContract.displayName || publishedContract.name}
/>
</DeployContractHeader>
<Separator />
/>
<DeployFormForUri
contractMetadata={contractMetadata}
modules={fetchedModules.filter((m) => m !== null)}
pathname={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}/deploy`}
/>
</>
</div>
);
}
@@ -1,10 +1,10 @@
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { CustomContractForm } from "components/contract-components/contract-deploy-form/custom-contract";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getAddress } from "thirdweb";
import type { FetchDeployMetadataResult } from "thirdweb/contract";
import { getAuthToken } from "../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../login/loginRedirect";
type DeployFormForUriProps = {
contractMetadata: FetchDeployMetadataResult | null;
@@ -19,24 +19,35 @@ export async function DeployFormForUri(props: DeployFormForUriProps) {
return <div>Could not fetch metadata</div>;
}
const cookieStore = await cookies();
const address = cookieStore.get(COOKIE_ACTIVE_ACCOUNT)?.value;
if (!address) {
redirect(`/login?next=${encodeURIComponent(pathname)}`);
}
const authCookieName = COOKIE_PREFIX_TOKEN + getAddress(address);
const token = cookieStore.get(authCookieName)?.value;
if (!token) {
redirect(`/login?next=${encodeURIComponent(pathname)}`);
const [authToken, teams] = await Promise.all([getAuthToken(), getTeams()]);
if (!teams || !authToken) {
loginRedirect(pathname);
}
const teamsAndProjects = await Promise.all(
teams.map(async (team) => ({
team: {
id: team.id,
name: team.name,
slug: team.slug,
image: team.image,
},
projects: (await getProjects(team.slug)).map((x) => ({
id: x.id,
name: x.name,
})),
})),
);
// TODO: remove the `ChakraProviderSetup` wrapper once the form is updated to no longer use chakra
return (
<ChakraProviderSetup>
<CustomContractForm
metadata={contractMetadata}
modules={modules?.filter((m) => m !== null)}
jwt={token}
jwt={authToken}
teamsAndProjects={teamsAndProjects}
/>
</ChakraProviderSetup>
);
@@ -56,7 +56,7 @@ export function DeployContractVersionSelector({
}
}}
>
<SelectTrigger className="min-w-[180px] bg-transparent hover:bg-accent">
<SelectTrigger className="min-w-[180px] bg-card hover:bg-accent">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -1,10 +1,10 @@
"use server";
import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getTeamById } from "@/api/team";
import { getRawAccount } from "../../../../account/settings/getAccount";
import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../../login/loginRedirect";
type State = {
success: boolean;
@@ -71,33 +71,37 @@ export async function createTicketAction(
_previousState: State,
formData: FormData,
) {
const cookieManager = await cookies();
const activeAccount = cookieManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const token = activeAccount
? cookieManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
: null;
if (!activeAccount || !token) {
// user is not logged in, make them log in
redirect(`/login?next=${encodeURIComponent("/support")}`);
}
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (accountRes.status !== 200) {
// user is not logged in, make them log in
redirect(`/login?next=${encodeURIComponent("/support")}`);
const teamId = formData.get("teamId")?.toString();
if (!teamId) {
return {
success: false,
message: "teamId is required",
};
}
const account = (await accountRes.json()) as {
data: { name: string; email: string; plan: string; id: string };
};
const team = await getTeamById(teamId);
const customerId = isValidPlan(account.data.plan)
? planToCustomerId[account.data.plan]
: undefined;
if (!team) {
return {
success: false,
message: `Team with id "${teamId}" not found`,
};
}
const [walletAddress, account] = await Promise.all([
getAuthTokenWalletAddress(),
getRawAccount(),
]);
if (!walletAddress || !account) {
loginRedirect("/support");
}
const customerId = isValidPlan(team.supportPlan)
? planToCustomerId[team.supportPlan]
: // fallback to "free" tier
planToCustomerId.free;
const product = formData.get("product")?.toString() || "";
const problemArea = formData.get("extraInfo_Problem_Area")?.toString() || "";
@@ -105,8 +109,8 @@ export async function createTicketAction(
const title = prepareEmailTitle(
product,
problemArea,
account.data.email,
account.data.name,
account.email || "",
account.name || "",
);
const keyVal: Record<string, string> = {};
@@ -117,10 +121,10 @@ export async function createTicketAction(
const markdown = prepareEmailBody({
product,
markdownInput: keyVal.markdown || "",
email: account.data.email,
name: account.data.name,
email: account.email || "",
name: account.name || "",
extraInfoInput: keyVal,
walletAddress: activeAccount,
walletAddress: walletAddress,
});
const content = {
@@ -129,9 +133,9 @@ export async function createTicketAction(
markdown,
status: "open",
onBehalfOf: {
email: account.data.email,
name: account.data.name,
id: account.data.id,
email: account.email,
name: account.name,
id: account.id,
},
customerId,
emailInboxId: process.env.UNTHREAD_EMAIL_INBOX_ID,
@@ -3,6 +3,7 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { SupportForm_SelectInput } from "components/help/contact-forms/shared/SupportForm_SelectInput";
import dynamic from "next/dynamic";
import {
@@ -14,6 +15,7 @@ import {
} from "react";
import { useFormStatus } from "react-dom";
import { toast } from "sonner";
import { SupportForm_TeamSelection } from "../../../../../components/help/contact-forms/shared/SupportForm_TeamSelection";
import { createTicketAction } from "./create-ticket.action";
const ConnectSupportForm = dynamic(
@@ -75,14 +77,46 @@ const productOptions: { label: string; component: ReactElement }[] = [
},
];
export function CreateTicket() {
function ProductAreaSelection(props: {
productLabel: string;
setProductLabel: (val: string) => void;
}) {
const { productLabel, setProductLabel } = props;
return (
<div className="flex flex-col gap-6">
<SupportForm_SelectInput
formLabel="What do you need help with?"
name="product"
options={productOptions.map((o) => o.label)}
promptText="Select a product"
onValueChange={setProductLabel}
value={productLabel}
required={true}
/>
{productOptions.find((o) => o.label === productLabel)?.component}
</div>
);
}
export function CreateTicket(props: {
teams: {
name: string;
id: string;
}[];
}) {
const formRef = useRef<HTMLFormElement>(null);
const [selectedTeamId, setSelectedTeamId] = useState<string | undefined>(
props.teams[0]?.id,
);
const [productLabel, setProductLabel] = useState("");
const [state, formAction] = useActionState(createTicketAction, {
message: "",
success: false,
});
// needed here
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
@@ -112,16 +146,19 @@ export function CreateTicket() {
<div className="h-7" />
<div className="flex flex-col gap-6">
<SupportForm_SelectInput
formLabel="What do you need help with?"
name="product"
options={productOptions.map((o) => o.label)}
promptText="Select a product"
onValueChange={setProductLabel}
value={productLabel}
required={true}
{/* Don't conditionally render this - it has be rendered to submit the input values */}
<div className={cn(props.teams.length === 1 && "hidden")}>
<SupportForm_TeamSelection
selectedTeamId={selectedTeamId}
onChange={(teamId) => setSelectedTeamId(teamId)}
teams={props.teams}
/>
</div>
<ProductAreaSelection
productLabel={productLabel}
setProductLabel={setProductLabel}
/>
{productOptions.find((o) => o.label === productLabel)?.component}
</div>
</div>
@@ -1,3 +1,4 @@
import { getTeams } from "@/api/team";
import {
Breadcrumb,
BreadcrumbItem,
@@ -7,17 +8,16 @@ import {
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import Link from "next/link";
import { redirect } from "next/navigation";
import { getAuthToken } from "../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../login/loginRedirect";
import { CreateTicket } from "./components/create-ticket.client";
export default async function Page() {
const authToken = await getAuthToken();
const teams = await getTeams();
if (!authToken) {
return redirect(
`/login?next=${encodeURIComponent("/support/create-ticket")}`,
);
const pagePath = "/support/create-ticket";
if (!teams || teams.length === 0) {
loginRedirect(pagePath);
}
return (
@@ -36,7 +36,12 @@ export default async function Page() {
</BreadcrumbList>
</Breadcrumb>
<div className="container max-w-[750px] py-10">
<CreateTicket />
<CreateTicket
teams={teams.map((t) => ({
name: t.name,
id: t.id,
}))}
/>
</div>
</div>
);
@@ -86,6 +86,7 @@ function TailwindTheme(props: { children: React.ReactNode }) {
disableTransitionOnChange
enableSystem={false}
defaultTheme="dark"
enableColorScheme={true}
>
{props.children}
</ThemeProvider>

Some files were not shown because too many files have changed in this diff Show More