Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
89e6af6baf Version Packages (#7164)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-29 11:10:34 -07:00
catalyst17 b345cc115e docs: add docs on aggregates (#7201)
# [Portal] Feature: Add Aggregation Examples and Documentation to Insight

## Notes for the reviewer

This PR adds comprehensive documentation for the Insight aggregation functionality across multiple pages:

- Added an "Aggregation Examples" section to the LLMs page with code samples
- Added detailed aggregation documentation to the Agents and LLMs page
- Added aggregation examples to the Blueprints page for Events, Transactions, Wallet Transactions, and Blocks
- Added FAQs about aggregation queries
- Added "Using Aggregation Presets" section to the Get Started page
- Enhanced Use Cases page with example aggregations for relevant scenarios

## How to test

Review the documentation for clarity and accuracy. The examples should be consistent with the actual API functionality.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Documentation**
  - Added comprehensive examples and explanations for using aggregation functions in Insight API queries across multiple documentation sections, including blueprints, use cases, FAQs, and getting started guides.
  - Introduced new sections detailing aggregation presets, usage patterns, and code snippets for blockchain data analysis.
  - Enhanced clarity on performing statistical and summary queries, with guidance on leveraging the Insight Playground for interactive exploration.
  - Updated the Insight landing page with a new feature card highlighting powerful aggregation capabilities and improved layout for feature cards.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the documentation for the `Insight` tool by adding new aggregation features, examples, and updating existing content to clarify how to utilize these functionalities effectively.

### Detailed summary
- Added `FunctionSquareIcon` to the import statement in `page.mdx`.
- Updated grid layout class from `lg:grid-cols-2` to `lg:grid-cols-3` in `page.mdx`.
- Introduced a new `FeatureCard` titled "Powerful Aggregations" in `page.mdx`.
- Added sections on aggregation queries and examples in `faqs/page.mdx`, `get-started/page.mdx`, and `agents-and-llms/page.mdx`.
- Provided aggregation examples in `use-cases/page.mdx` and `blueprints/page.mdx`.
- Enhanced documentation for wallet transactions and blocks, including aggregation examples in `agents-and-llms/page.mdx`.

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

<!-- end pr-codex -->
2025-05-29 17:37:27 +00:00
gregandGitHub 3d3c6f3d45 Fix: Transfer purchase data (#7202) 2025-05-29 10:30:15 -07:00
Robby UitbeijerseandGitHub ff3a79c884 chore: annotate getBuyWithFiatQuote as deprecated (#7198) 2025-05-29 09:00:53 -07:00
kumaryash90 16482cfa6a Stylus ERC20 template for CLI (#7192)
<!--

## 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 `create.ts` and `builder.ts` files for the Stylus project by adding project type selection and improving contract name handling during ABI processing. It introduces prompts for user input and refines how contract names are extracted and utilized.

### Detailed summary
- In `create.ts`, added prompts for selecting project type (Default or ERC20).
- Updated project creation logic based on the selected type.
- In `builder.ts`, improved contract name extraction to handle multiple contracts.
- Added prompts for selecting the entrypoint if multiple contracts are found.
- Refined usage of selected contract names throughout the build process.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added support for handling multiple contract ABIs, allowing users to select a contract entrypoint when more than one is present.
  - Introduced a prompt to select a project template type ("default" or "erc20") during project creation.

- **Improvements**
  - Enhanced error handling and user guidance when no contract is selected or found.
  - Updated success messages and console output for improved clarity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 12:12:00 +00:00
jnsdls e8a25d70b9 [Dashboard] replace chakra stats in accounts (#7188)
## Summary
- swap chakra `SimpleGrid`, `Stat`, `Skeleton` for shadcn `StatCard`
- use Tailwind grid utility classes

## Checklist
- [x] `pnpm biome check --apply`
- [x] `pnpm test` *(fails: spawn anvil ENOENT)*

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `AccountsCount` component to replace the `Card` and `SimpleGrid` components from `@chakra-ui/react` with a new layout using `StatCard` from a different module, enhancing the presentation of the total accounts data.

### Detailed summary
- Removed imports for `SimpleGrid`, `Skeleton`, `Stat`, `StatLabel`, and `StatNumber`.
- Added import for `StatCard` from `../../overview/components/stat-card`.
- Changed the layout from `SimpleGrid` and `Card` to a `div` with a grid layout.
- Updated the rendering to use `StatCard` for displaying the total accounts.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Refactor**
  - Updated the accounts count display to use a new StatCard component with improved layout and styling for a more consistent and responsive user interface.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 00:55:53 +00:00
gregandGitHub 44a7460c83 [SDK] Feature: Remove Switch Network button (#7191) 2025-05-28 17:45:07 -07:00
gregandGitHub 0731831169 Fix: Webhook docs tags (#7189) 2025-05-28 17:11:21 -07:00
AmineAfia 91750eddc6 Temporarily remove webhooks sidebar link until scrolling issue is fixed (#7187)
## [Dashboard] Fix: Temporarily remove Webhooks from sidebar

## Notes for the reviewer

Commented out the Webhooks section in the project sidebar until we resolve a scrolling issue. Also removed the unused BellIcon import.

## How to test

Verify that the Webhooks option no longer appears in the project sidebar navigation.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
  - Temporarily removed the "Webhooks" link from the project sidebar to address a scrolling issue. The link and its badge are no longer visible.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-28 23:45:59 +00:00
iuwqyir 2fc083f1f2 token owners endpoint in insight playground (#7184)
## [Dashboard] Feature: Update Token and NFT API Endpoints

## Notes for the reviewer

This PR adds a new token endpoint and updates the name of an existing NFT endpoint:

1. Added a new endpoint for getting token owners by contract: `/v1/tokens/owners`
2. Renamed the "Get NFTs by owner" endpoint to simply "Get NFTs" to better reflect its functionality

## How to test

Verify the updated endpoint names appear correctly in the Insight section of the playground.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added a new API endpoint for retrieving token owners by contract.

- **Refactor**
  - Renamed the "Get NFTs by owner" endpoint to "Get NFTs" for improved clarity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-28 23:26:30 +00:00
AmineAfia 75ae536e61 Add scrollable overflow to webhook filter select dropdowns (#7186)
## [Dashboard] Fix: Add scrolling to webhook event and function signature dropdowns

## Notes for the reviewer

This PR adds scrolling functionality to the webhook event and function signature dropdown menus by adding `max-h-60 overflow-y-auto` classes. This ensures that when there are many options, users can still navigate through them effectively.

## How to test

1. Navigate to the webhooks section in a project
2. Open the event signature dropdown and verify it scrolls properly when there are many options
3. Similarly, test the function signature dropdown to ensure it maintains its max width while allowing vertical scrolling

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Improved dropdown menus for event and transaction signature selection by limiting their maximum height and enabling vertical scrolling, enhancing usability when many options are available.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `FilterDetailsStep` component by improving the usability of the dropdown content with better scrolling behavior.

### Detailed summary
- Updated the `className` of the first `<SelectContent>` from default to `max-h-60 overflow-y-auto` for better vertical scrolling.
- Updated the second `<SelectContent>` to include `overflow-y-auto` while retaining `max-w-[600px]`.

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

<!-- end pr-codex -->
2025-05-28 23:11:30 +00:00
gregandGitHub 41e59a9c20 [SDK] Feature: Adds explicit country param to onramps (#7185) 2025-05-28 16:11:09 -07:00
Joey BrightandGitHub fd97406fa2 chore: fix typos (#7180) 2025-05-28 16:10:17 -07:00
AmineAfia c336de3bfb Add webhook creation UI to dashboard (#7174)
<!--

## 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 `CreateWebhookModal` component and related functionalities in the webhook management system, including form handling, validation, and API integration for creating webhooks.

### Detailed summary
- Changed `createWebhook` function to `export async function createWebhook`.
- Updated `CreateWebhookModal` to accept `clientId` prop.
- Integrated form steps for creating a webhook with validation.
- Added utility functions for parsing addresses and building webhook payloads.
- Improved error handling and user feedback during webhook testing.
- Enhanced UI components for better user experience in the webhook creation process.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced a multi-step modal form for creating webhooks, including form validation, ABI integration, and live endpoint testing.
  - Added components for basic info entry, filter configuration, and review before submission.
  - Enabled dynamic ABI fetching and signature extraction for blockchain event and transaction filters.
  - Improved error handling and user feedback throughout the webhook creation flow.

- **Bug Fixes**
  - Enhanced validation and error messages for webhook URLs and date handling in tables.

- **Chores**
  - Added comprehensive utility modules for ABI processing and webhook payload construction.
  - Implemented robust type definitions and validation schemas for webhook forms.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-28 22:02:27 +00:00
AmineAfia c420099dc3 Add webhook testing functionality to dashboard (#7173)
<!--

## 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 webhook testing functionality in the dashboard application by exporting the `testWebhook` function, adding a testing feature in the `WebhooksTable`, and creating a custom hook `useTestWebhook` to manage the testing state and results.

### Detailed summary
- Changed `testWebhook` from a local function to an exported function in `webhooks.ts`.
- Imported `PlayIcon` in `WebhooksTable.tsx` and added it to the UI.
- Implemented `handleTestWebhook` function to test webhooks and display results.
- Created `useTestWebhook` hook to manage testing state and results.
- Added error handling and success notifications for webhook testing.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added the ability to test webhooks directly from the webhooks table with a "Test" button, providing immediate feedback and status indicators during testing.
- **User Interface**
  - Enhanced the actions column in the webhooks table for improved layout and usability, including real-time spinner icons and error notifications during webhook testing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-28 22:02:27 +00:00
AmineAfia dcb412fc27 Add webhooks page to project dashboard (#7172)
<!--

## 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 new `CreateWebhookModal` component and enhances the `Webhooks` functionality in the dashboard. It implements a `WebhooksTable` for displaying webhooks and their details, along with error handling and user notifications for actions like deleting webhooks.

### Detailed summary
- Added `CreateWebhookModal` component for creating new webhooks.
- Updated `ProjectSidebarLayout` to include a link to webhooks with a new badge.
- Changed `WebhookResponse` and `WebhookFilters` interfaces to `export`.
- Modified `WebhooksPage` to handle project and webhook loading with error management.
- Introduced `WebhooksTable` for displaying webhooks with actions.
- Implemented `RelativeTime` component to show formatted timestamps.
- Added deletion functionality for webhooks with user notifications.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a "Webhooks" section in the project sidebar, highlighted with a "New" badge.
  - Added a dedicated Webhooks page displaying a list of webhooks in a sortable table with options to copy URLs and delete entries.
  - Included a "New Webhook" button and modal for future webhook creation.
  - Implemented a component to display relative creation times for webhooks.
- **Bug Fixes**
  - Improved error handling and user feedback for webhook-related actions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-28 22:02:26 +00:00
catalyst17 4705a599a2 feat: redesign aggregate parameter input with improved UI (#7179)
# [Dashboard] Feature: Redesign Aggregate Parameter Input Component

## Notes for the reviewer

This PR completely redesigns the aggregate parameter input component with a more user-friendly interface. The new design uses a popover pattern instead of the previous MultiSelect approach, making it easier to view and select aggregation presets.

## How to test

1. Open any insight blueprint that uses aggregation
2. Click on the aggregate parameter input
3. Test searching for presets, selecting/deselecting options
4. Verify that manual text entry still works in the textarea
5. Check that the selected aggregations display correctly when the popover is closed
2025-05-28 21:48:48 +00:00
catalyst17 f7331f15a2 feat: enhance aggregate parameter input with multi-select presets (#7168)
## [Dashboard] Feature: Enhance Aggregate Parameter Input with Multi-Select Functionality

## Notes for the reviewer
This PR replaces the simple dropdown select with a more robust multi-select component for aggregate parameter inputs. The new implementation allows users to select multiple presets and displays them as badges.

## How to test
Test the new aggregate parameter input by:
1. Opening the insight blueprint page
2. Selecting multiple presets from the dropdown
3. Verifying that selected values appear as badges
4. Testing the search functionality within the preset selector
5. Confirming that manual input still works alongside preset selection

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced aggregate parameter input with combined manual entry and multi-select for presets.
  - Added search functionality within the multi-select component for easier preset selection.
  - Updated parameter descriptions and examples for "aggregate" to clarify multi-selection usage.
- **Refactor**
  - Made input options optional and improved synchronization between manual input and multi-select.
  - Adjusted tooltip positioning for better UI alignment on aggregate parameters.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `ParameterSection` and `AggregateParameterInput` components to provide dynamic descriptions and examples based on the parameter type. It also refines the handling of selected values and manual input for aggregations, replacing the `Select` component with a `MultiSelect`.

### Detailed summary
- Updated `description` and `exampleToShow` in `ParameterSection` based on `param.name`.
- Introduced `MultiSelect` in `AggregateParameterInput` for selecting aggregation presets.
- Added state management for manual input and selected values.
- Removed the `Select` component in favor of a more flexible input system.

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

<!-- end pr-codex -->
2025-05-28 21:48:47 +00:00
AmineAfia a5d6e769db Add webhook API functions for insight dashboard (#7171)
<!--

## 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 functionality for managing webhooks in the application. It includes a temporary `WebhooksPage` for displaying webhook data and implements several API functions for creating, retrieving, deleting, and testing webhooks.

### Detailed summary
- Added `WebhooksPage` component in `page.tsx` to display webhook data.
- Implemented `getWebhooks` function to fetch webhooks from the API.
- Created interfaces for webhook responses and payloads.
- Added `createWebhook`, `deleteWebhook`, and `testWebhook` functions for managing webhooks.
- Handled API errors with appropriate responses.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced the ability to create, view, delete, and test webhooks through the dashboard interface, allowing users to manage webhook integrations more easily.
  - Added a new dashboard page displaying webhook data for better visibility and management.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-28 21:20:24 +00:00
jnsdls 97b8926024 [Dashboard] migrate external links form (#7156)
## Summary
- refactor contract publish resources inputs
- use shadcn/ui Button, Input, and Separator
- apply tailwind classes

## Testing
- `pnpm biome check --apply apps/dashboard/src/components/contract-components/contract-publish-form/external-links-fieldset.tsx apps/dashboard/src/components/contract-components/contract-publish-form/external-links-input.tsx`
- `pnpm test` *(fails: spawn anvil ENOENT)*

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the UI components in the `ExternalLinksFieldset` and `ExternalLinksInput` files to improve the styling and structure using custom components instead of those from `tw-components` and `@chakra-ui/react`.

### Detailed summary
- Replaced `Heading` and `Text` with custom HTML elements in `ExternalLinksFieldset`.
- Updated button styles in `ExternalLinksFieldset` to use `variant="outline"` and added a `className`.
- Refactored `ExternalLinksInput` to use custom components (`Button`, `Input`, `Label`, `Separator`) instead of Chakra UI components.
- Changed layout structure in `ExternalLinksInput` to use `div` elements with flexbox for better styling.
- Added error message handling below input fields in `ExternalLinksInput`.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Updated form and button components to use new UI primitives and Tailwind CSS classes for a refreshed appearance.
  - Improved accessibility by linking input labels and error messages with appropriate HTML attributes.

- **Refactor**
  - Replaced Chakra UI components with custom or alternative UI components, maintaining existing functionality and validation logic.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-28 15:47:22 +00:00
Joaquim VergesandGitHub 655df037dd [SDK] fix hiddenWallets prop (#7183) 2025-05-28 01:46:03 -07:00
Joaquim VergesandGitHub cbde32e974 [Dashboard] add wallet user search (#7182) 2025-05-28 01:14:14 -07:00
Joaquim VergesandGitHub 8ed5ef3f81 [Dashboard] Change "Network" label to "Select Testnet" in analytics test transaction form (#7178) 2025-05-28 00:52:08 -07:00
Joe-Thirdweb e14a1349e1 [Dashboard] Add token request route to dashboard (#7112)
TOOL-4443
TOOL-4559

How to Test
1. Log into dashboard
2. Select a project
3. Click on Universal Bridge on left hand side
4. Click on Settings
5. Scroll down to add token section

-  Failure input
Chain: Ethereum
Token Address: 0x7613c48e0cd50e42dd9bf0f6c235063145f6f8db

-Success input
Chain: Ethereum
Token Address:0x7613c48e0cd50e42dd9bf0f6c235063145f6f8dc

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new validation schema for route discovery, enhances the UI for token submission, and integrates a new `RouteDiscovery` component to facilitate token discovery in the dashboard.

### Detailed summary
- Added `routeDiscoveryValidationSchema` in `validations.ts` for validating token discovery inputs.
- Created `RouteDiscovery` component to handle token submission.
- Updated `page.tsx` to include `RouteDiscovery` below `PayConfig`.
- Implemented `addUniversalBridgeTokenRoute` function in `tokens.ts` for API token submission.
- Added `RouteDiscoveryCard` component for better UI presentation of token discovery.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a Route Discovery workflow, allowing users to submit tokens for route discovery on selected blockchain networks.
  - Added a new card component for displaying route discovery information and actions.
  - Integrated Route Discovery into the Universal Bridge settings page for easier access.

- **Enhancements**
  - Added validation for token address and chain selection to ensure proper input.
  - Provided user feedback through success and error messages, including toast notifications.

- **Style**
  - Minor cleanup of extra blank lines in the Project General Settings page for improved readability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 22:14:03 +00:00
gregGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
acd8b9e535 Feat: tokenAddress and amount bridge params (#7176)
Signed-off-by: greg <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-05-27 13:28:36 -07:00
Robby UitbeijerseandGitHub 52d451ff70 [SDK] ensure that the default spinner stroke color gets inherited from the parentnode (#7157) 2025-05-27 12:28:10 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c03616666b chore(deps): bump @radix-ui/react-select from 2.2.2 to 2.2.5 (#7159)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 12:21:22 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bb7a8988ef chore(deps): bump @radix-ui/react-separator from 1.1.4 to 1.1.7 (#7161)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 11:51:20 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8d0ffedd41 chore(deps): bump papaparse from 5.5.2 to 5.5.3 (#7162)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 11:51:04 -07:00
Jonas DanielsandGitHub 5edfb81ec5 [Dashboard] consolidate solution page rewrites with dynamic slug (#7175) 2025-05-27 11:50:23 -07:00
iuwqyir f115b4d2db new tokens route (#7170)
## [Dashboard] Feature: Add new tokens endpoint and deprecate ERC-20 balances endpoint

## Notes for the reviewer

This PR adds a new endpoint for getting tokens at `/v1/tokens` and marks the existing ERC-20 balances endpoint (`/v1/tokens/erc20/{ownerAddress}`) as deprecated.

## How to test

Verify that the new endpoint is accessible and that the deprecated flag is correctly set for both endpoints in the insight blueprints.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced a new "Get tokens" endpoint for accessing token data.

- **Bug Fixes**
  - Marked the older ERC-20 balances endpoint as deprecated to guide users toward the updated endpoint.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 17:02:25 +00:00
Joaquim VergesandGitHub 0e4390b2c7 [Docs] Add Onramp extension to sidebar and typedoc (#7167) 2025-05-27 01:18:31 -07:00
Joaquim VergesandGitHub aec8d88af2 [Wallet] Move 7702 batch transaction test to integration tests (#7166) 2025-05-26 20:53:10 -07:00
gregandGitHub 80207ae240 [Dashboard] Refactor: Bridge UI Cleanup and Fixes (#7165) 2025-05-26 19:12:23 -07:00
Joaquim VergesandGitHub ec7bc2bb1e [Insight] Make clientId optional and improve error handling (#7158) 2025-05-26 14:34:12 -07:00
MananTank 12475027d1 Add Contract layout in team/project (#7152)
<!--

## 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 enhancements to the handling of contract pages, adding new types for parameters and project metadata, while refactoring existing components to incorporate team and project-specific data.

### Detailed summary
- Added `PublicContractPageParams` and `ProjectContractPageParams` types.
- Introduced `ProjectMeta` type for project metadata.
- Refactored components to accept `teamSlug`, `projectSlug`, and `projectMeta`.
- Updated routing and links to include project-specific paths.
- Enhanced handling of contract page data retrieval and UI rendering.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/AccountPage.client.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/TokenIdPage.client.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account-permissions/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/ContractNFTPage.client.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/contract-page-layout.client.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/explorer/shared-explorer-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/explorer/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/ContractPermissionsPage.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/shared-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/ContractChecklist.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/shared-permissions-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/accounts/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account-permissions/shared-account-permissions-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/shared-split-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/accounts/shared-accounts-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/contract-page-layout.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/shared-modules-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/shared-proposals-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/shared-account-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/shared-english-auctions-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/shared-direct-listings-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/shared-nfts-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/data-table.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/claim-conditions/shared-claim-conditions-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/claim-conditions/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/shared-settings-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-overview-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/analytics/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/token-id.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/analytics/shared-analytics-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/shared-nfts-token-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/ContractOverviewPage.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/events/events-feed.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/layout.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/MarketplaceDetails.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/shared-cross-chain-page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/cross-chain/page.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx`

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced routing to include project and team slugs, enabling project-aware navigation for contract, NFT, marketplace, analytics, permissions, and proposal pages.
  - Passed project metadata (team and project info) throughout various pages and components for contextual consistency.
  - Introduced shared layout and page components for contract overview, NFTs, analytics, permissions, and settings, streamlining the user experience.

- **Refactor**
  - Removed internal data fetching, validation, and conditional rendering from pages, delegating logic to shared components.
  - Updated navigation links and breadcrumbs to dynamically incorporate project and team slugs when available.
  - Centralized metadata and layout generation into utility functions to reduce duplication.

- **Bug Fixes**
  - Improved handling of missing or invalid project/team data by triggering notFound responses appropriately.

- **Chores**
  - Extended component props and interfaces to accept projectMeta objects.
  - Simplified page components to directly render shared components with minimal logic.
  - Updated URL construction to utilize utility functions for project-aware paths.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-26 20:59:12 +00:00
catalyst17 9479d252b4 feat: add aggregate parameter input with presets (#7146)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces the `AggregateParameterInput` component to handle aggregate parameters in the `BlueprintPlayground`. It adds new presets for various data types and modifies the `ParameterSection` to utilize this new component, enhancing the functionality of the parameter input system.

### Detailed summary
- Added `AggregateParameterInput` component to handle aggregate parameters.
- Introduced multiple preset options for aggregates, transactions, events, and blocks.
- Updated `ParameterSection` to include a check for the `aggregate` parameter and render the new component.
- Modified placeholder logic in `ParameterSection` to use `param.description` or `param.name`.
- Passed `endpointPath` prop to `AggregateParameterInput` for context on preset selection.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a specialized input with preset selection for aggregate query parameters, enhancing usability when configuring aggregate values.
  - The playground UI now displays this new input component for aggregate parameters, providing relevant preset options based on the selected endpoint.
- **Improvements**
  - Placeholders for parameter inputs now more accurately reflect the parameter’s description or name for better clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-25 19:14:20 +00:00
Joaquim VergesandGitHub 5139857db9 [Portal] fix portal build (#7154) 2025-05-23 22:56:39 -07:00
Joaquim VergesandGitHub ad4341a7ad [Portal] Add AI chat functionality with markdown support (#7139) 2025-05-23 20:55:13 -07:00
Joaquim VergesandGitHub ebd58f49b6 [Portal] Redesign header with improved mobile menu layout (#7138) 2025-05-23 20:12:18 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
3862bc7a94 Version Packages (#7150)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-23 20:03:03 -07:00
gregfromstl 5277eddb70 [SDK] Fix: Webhook Schema (#7151)
<!--

## 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 updating the `Webhook` schema in the `thirdweb` package to enhance the structure of `originToken` and `destinationToken` by replacing simple address strings with detailed objects containing additional properties.

### Detailed summary
- Updated `originToken` and `destinationToken` to be objects with properties:
  - `chainId`
  - `address`
  - `name`
  - `symbol`
  - `decimals`
  - `priceUsd`
  - `iconUri`
- Adjusted the test file to reflect these changes with appropriate object structures.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
  - Enhanced webhook support to include detailed token information such as chain ID, name, symbol, decimals, price, and icon for origin and destination tokens.
- **Tests**
  - Updated test data to match the new detailed token structure in webhook payloads.
- **Chores**
  - Added a changeset to document this patch update.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 23:10:14 +00:00
jnsdls 6c180b5c8a [Dashboard] migrate solidity inputs to shadcn (#7148)
## Summary
- migrate solidity bool, int, bytes, and string inputs to shadcn/ui and tailwind

## Testing
- `pnpm biome check --apply apps/dashboard/src/contract-ui/components/solidity-inputs/bool-input.tsx apps/dashboard/src/contract-ui/components/solidity-inputs/bytes-input.tsx apps/dashboard/src/contract-ui/components/solidity-inputs/int-input.tsx apps/dashboard/src/contract-ui/components/solidity-inputs/string-input.tsx`
- `pnpm test` *(fails: spawn anvil ENOENT)*

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the UI components in the `solidity-inputs` directory to use a consistent styling approach and improve the layout by replacing certain components with more flexible alternatives.

### Detailed summary
- Updated imports from `tw-components` to a local `@/components/ui` for `Button` and `Input`.
- Replaced `ButtonGroup` with a `div` for better styling control in `SolidityBoolInput`.
- Adjusted button variants from `solid` to `default` for consistency.
- Introduced a `showConversionButton` flag in `SolidityIntInput` for conditional rendering.
- Changed `InputGroup` to `div` in `SolidityIntInput` and `SolidityStringInput` for layout flexibility.
- Used `cn` utility for conditional class names in `Input` components.
- Updated button and input placements to enhance responsiveness and styling.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Updated input components to use custom UI elements and Tailwind CSS for consistent styling.
  - Replaced Chakra UI and third-party components with locally defined UI components and native HTML elements.
  - Improved button and input layouts for better visual consistency across boolean, integer, byte, and string input fields.

- **New Features**
  - Enhanced integer input with conditional display of a conversion button based on input value format.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 22:57:37 +00:00
Joaquim VergesandGitHub 3489ecec98 [SDK] Improve quote error handling (#7147) 2025-05-23 15:43:52 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f9b34ff2f1 Version Packages (#7149)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-23 14:04:03 -07:00
Jonas DanielsandGitHub f3abea3276 Add webhook verification functionality for secure payload validation (#7103) 2025-05-23 13:59:46 -07:00
245 changed files with 23310 additions and 13548 deletions
+1 -4
View File
@@ -27,10 +27,7 @@ module.exports = [
// -- engine
"/engine",
// -- solutions pages --
"/solutions/gaming",
"/solutions/consumer-apps",
"/solutions/defi",
"/solutions/ecosystem",
"/solutions/:solution_slug",
// -- campaigns --
// "/unlimited-wallets", -- OFF for now
// -- TPP --
+5 -5
View File
@@ -40,8 +40,8 @@
"@radix-ui/react-popover": "^1.1.10",
"@radix-ui/react-progress": "^1.1.4",
"@radix-ui/react-radio-group": "^1.3.3",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-switch": "^1.2.2",
"@radix-ui/react-tooltip": "1.2.3",
@@ -77,7 +77,7 @@
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
"p-limit": "^6.2.0",
"papaparse": "^5.5.2",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.67.1",
"prettier": "3.5.3",
@@ -89,7 +89,7 @@
"react-dropzone": "^14.3.8",
"react-error-boundary": "^5.0.0",
"react-hook-form": "7.55.0",
"react-markdown": "^9.0.1",
"react-markdown": "10.1.0",
"react-table": "^7.8.0",
"recharts": "2.15.3",
"remark-gfm": "4.0.1",
@@ -105,7 +105,7 @@
"thirdweb": "workspace:*",
"tiny-invariant": "^1.3.3",
"use-debounce": "^10.0.4",
"zod": "3.24.3"
"zod": "3.25.24"
},
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
@@ -0,0 +1,201 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
"use server";
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
import { THIRDWEB_INSIGHT_API_DOMAIN } from "constants/urls";
export interface WebhookResponse {
id: string;
name: string;
team_id: string;
project_id: string;
webhook_url: string;
webhook_secret: string;
filters: WebhookFilters;
suspended_at: string | null;
suspended_reason: string | null;
disabled: boolean;
created_at: string;
updated_at: string | null;
}
export interface WebhookFilters {
"v1.events"?: {
chain_ids?: string[];
addresses?: string[];
signatures?: Array<{
sig_hash: string;
abi?: string;
params?: Record<string, unknown>;
}>;
};
"v1.transactions"?: {
chain_ids?: string[];
from_addresses?: string[];
to_addresses?: string[];
signatures?: Array<{
sig_hash: string;
abi?: string;
params?: Record<string, unknown>;
}>;
};
}
interface CreateWebhookPayload {
name: string;
webhook_url: string;
filters: WebhookFilters;
}
interface WebhooksListResponse {
data: WebhookResponse[];
error?: string;
}
interface WebhookSingleResponse {
data: WebhookResponse | null;
error?: string;
}
interface TestWebhookPayload {
webhook_url: string;
type?: "event" | "transaction";
}
interface TestWebhookResponse {
success: boolean;
error?: string;
}
export async function createWebhook(
payload: CreateWebhookPayload,
clientId: string,
): Promise<WebhookSingleResponse> {
try {
const authToken = await getAuthToken();
const response = await fetch(`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
return {
data: null,
error: `Failed to create webhook: ${errorText}`,
};
}
return (await response.json()) as WebhookSingleResponse;
} catch (error) {
return {
data: null,
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}
export async function getWebhooks(
clientId: string,
): Promise<WebhooksListResponse> {
try {
const authToken = await getAuthToken();
const response = await fetch(`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, {
method: "GET",
headers: {
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
return {
data: [],
error: `Failed to get webhooks: ${errorText}`,
};
}
return (await response.json()) as WebhooksListResponse;
} catch (error) {
return {
data: [],
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}
export async function deleteWebhook(
webhookId: string,
clientId: string,
): Promise<WebhookSingleResponse> {
try {
const authToken = await getAuthToken();
const response = await fetch(
`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/${encodeURIComponent(webhookId)}`,
{
method: "DELETE",
headers: {
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
},
},
);
if (!response.ok) {
const errorText = await response.text();
return {
data: null,
error: `Failed to delete webhook: ${errorText}`,
};
}
return (await response.json()) as WebhookSingleResponse;
} catch (error) {
return {
data: null,
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}
export async function testWebhook(
payload: TestWebhookPayload,
clientId: string,
): Promise<TestWebhookResponse> {
try {
const authToken = await getAuthToken();
const response = await fetch(
`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/test`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(payload),
},
);
if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: `Failed to test webhook: ${errorText}`,
};
}
return (await response.json()) as TestWebhookResponse;
} catch (error) {
return {
success: false,
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}
@@ -1,5 +1,7 @@
"use server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
import { UB_BASE_URL } from "./constants";
export type TokenMetadata = {
@@ -37,3 +39,33 @@ export async function getUniversalBridgeTokens(props: {
const json = await res.json();
return json.data as Array<TokenMetadata>;
}
export async function addUniversalBridgeTokenRoute(props: {
chainId: number;
tokenAddress: string;
project: ProjectResponse;
}) {
const authToken = await getAuthToken();
const url = new URL(`${UB_BASE_URL}/v1/tokens`);
const res = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
"x-client-id": props.project.publishableKey,
} as Record<string, string>,
body: JSON.stringify({
chainId: props.chainId,
tokenAddress: props.tokenAddress,
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(text);
}
const json = await res.json();
return json.data as Array<TokenMetadata>;
}
@@ -0,0 +1,84 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type React from "react";
export function RouteDiscoveryCard(
props: React.PropsWithChildren<{
bottomText: React.ReactNode;
header?: {
description: string | undefined;
title: string;
};
errorText: string | undefined;
noPermissionText: string | undefined;
saveButton?: {
onClick?: () => void;
disabled: boolean;
isPending: boolean;
type?: "submit";
variant?:
| "ghost"
| "default"
| "primary"
| "destructive"
| "outline"
| "secondary";
className?: string;
label?: string;
};
}>,
) {
return (
<div className="relative rounded-lg border border-border bg-card">
<div
className={cn(
"relative border-border border-b px-4 py-6 lg:px-6",
props.noPermissionText && "cursor-not-allowed",
)}
>
{props.header && (
<>
<h3 className="font-semibold text-xl tracking-tight">
{props.header.title}
</h3>
{props.header.description && (
<p className="mt-1.5 mb-4 text-foreground text-sm">
{props.header.description}
</p>
)}
</>
)}
{props.children}
</div>
<div className="flex min-h-[60px] items-center justify-between gap-2 px-4 py-3 lg:px-6">
{props.noPermissionText ? (
<p className="text-muted-foreground text-sm">
{props.noPermissionText}
</p>
) : props.errorText ? (
<p className="text-destructive-text text-sm">{props.errorText}</p>
) : (
<p className="text-muted-foreground text-sm">{props.bottomText}</p>
)}
{props.saveButton && !props.noPermissionText && (
<Button
size="sm"
className={cn("gap-2", props.saveButton.className)}
onClick={props.saveButton.onClick}
disabled={props.saveButton.disabled || props.saveButton.isPending}
variant={props.saveButton.variant || "outline"}
type={props.saveButton.type}
>
{props.saveButton.isPending && <Spinner className="size-3" />}
{props.saveButton.label ||
(props.saveButton.isPending ? "Submit" : "Submit Token")}
</Button>
)}
</div>
</div>
);
}
@@ -1,29 +0,0 @@
"use client";
import { getSDKTheme } from "app/(app)/components/sdk-component-theme";
import { useV5DashboardChain } from "lib/v5-adapter";
import { useTheme } from "next-themes";
import type { ThirdwebClient } from "thirdweb";
import { PayEmbed } from "thirdweb/react";
export function UniversalBridgeEmbed({
chainId,
client,
}: { chainId?: number; client: ThirdwebClient }) {
const { theme } = useTheme();
const chain = useV5DashboardChain(chainId || 1);
return (
<PayEmbed
client={client}
payOptions={{
mode: "fund_wallet",
prefillBuy: {
chain,
amount: "0.01",
},
}}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
/>
);
}
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
@@ -9,6 +10,7 @@ import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
export function ContractDirectListingsPageClient(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -21,7 +23,12 @@ export function ContractDirectListingsPageClient(props: {
}
if (!metadataQuery.data.isDirectListingSupported) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return (
@@ -1,45 +1,18 @@
import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
import { ContractDirectListingsPageClient } from "./ContractDirectListingsPage.client";
import type { PublicContractPageParams } from "../../types";
import { SharedDirectListingsPage } from "./shared-direct-listings-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const account = await getRawAccount();
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
if (info.isLocalhostChain) {
return (
<ContractDirectListingsPageClient
contract={info.clientContract}
isLoggedIn={!!account}
/>
);
}
const { isDirectListingSupported, isInsightSupported } =
await getContractPageMetadata(info.serverContract);
if (!isDirectListingSupported) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<ContractDirectListingsPage
contract={info.clientContract}
<SharedDirectListingsPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
isInsightSupported={isInsightSupported}
/>
);
}
@@ -0,0 +1,53 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
import { ContractDirectListingsPageClient } from "./ContractDirectListingsPage.client";
export async function SharedDirectListingsPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
if (info.isLocalhostChain) {
return (
<ContractDirectListingsPageClient
contract={info.clientContract}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
const { isDirectListingSupported, isInsightSupported } =
await getContractPageMetadata(info.serverContract);
if (!isDirectListingSupported) {
redirectToContractLandingPage({
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
projectMeta: props.projectMeta,
});
}
return (
<ContractDirectListingsPage
contract={info.clientContract}
isLoggedIn={props.isLoggedIn}
isInsightSupported={isInsightSupported}
/>
);
}
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
@@ -9,6 +10,7 @@ import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
export function ContractEnglishAuctionsPageClient(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -21,7 +23,12 @@ export function ContractEnglishAuctionsPageClient(props: {
}
if (!metadataQuery.data.isEnglishAuctionSupported) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return (
@@ -1,46 +1,18 @@
import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
import { ContractEnglishAuctionsPageClient } from "./ContractEnglishAuctionsPage.client";
import type { PublicContractPageParams } from "../../types";
import { SharedEnglishAuctionsPage } from "./shared-english-auctions-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const twAccount = await getRawAccount();
if (info.isLocalhostChain) {
return (
<ContractEnglishAuctionsPageClient
contract={info.clientContract}
isLoggedIn={!!twAccount}
/>
);
}
const { isEnglishAuctionSupported, isInsightSupported } =
await getContractPageMetadata(info.serverContract);
if (!isEnglishAuctionSupported) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<ContractEnglishAuctionsPage
contract={info.clientContract}
isLoggedIn={!!twAccount}
isInsightSupported={isInsightSupported}
<SharedEnglishAuctionsPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
/>
);
}
@@ -0,0 +1,52 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
import { ContractEnglishAuctionsPageClient } from "./ContractEnglishAuctionsPage.client";
export async function SharedEnglishAuctionsPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
if (info.isLocalhostChain) {
return (
<ContractEnglishAuctionsPageClient
contract={info.clientContract}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
const metadata = await getContractPageMetadata(info.serverContract);
if (!metadata.isEnglishAuctionSupported) {
redirectToContractLandingPage({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
projectMeta: props.projectMeta,
});
}
return (
<ContractEnglishAuctionsPage
contract={info.clientContract}
isLoggedIn={props.isLoggedIn}
isInsightSupported={metadata.isInsightSupported}
/>
);
}
@@ -3,6 +3,8 @@ import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { useMemo } from "react";
import { type NFT, ZERO_ADDRESS } from "thirdweb";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../_utils/contract-page-path";
type NFTWithContract = NFT & { contractAddress: string; chainId: number };
@@ -29,6 +31,7 @@ interface NFTCardsProps {
trackingCategory: string;
isPending: boolean;
allNfts?: boolean;
projectMeta: ProjectMeta | undefined;
}
export const NFTCards: React.FC<NFTCardsProps> = ({
@@ -36,6 +39,7 @@ export const NFTCards: React.FC<NFTCardsProps> = ({
trackingCategory,
isPending,
allNfts,
projectMeta,
}) => {
const dummyData = useMemo(() => {
return Array.from({
@@ -87,7 +91,12 @@ export const NFTCards: React.FC<NFTCardsProps> = ({
<TrackedLinkTW
category={trackingCategory}
label="view_nft"
href={`/${token.chainId}/${token.contractAddress}/nfts/${tokenId}`}
href={buildContractPagePath({
projectMeta,
chainIdOrSlug: token.chainId.toString(),
contractAddress: token.contractAddress,
subpath: `/nfts/${tokenId}`,
})}
className="before:absolute before:inset-0"
>
{v.name}
@@ -3,10 +3,13 @@
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useEffect, useRef } from "react";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../_utils/contract-page-path";
import { LoadingPage } from "./page-skeletons";
export function RedirectToContractOverview(props: {
contract: ThirdwebContract;
projectMeta: ProjectMeta | undefined;
}) {
const router = useDashboardRouter();
const redirected = useRef(false);
@@ -17,8 +20,14 @@ export function RedirectToContractOverview(props: {
return;
}
redirected.current = true;
router.replace(`/${props.contract.chain.id}/${props.contract.address}`);
}, [router, props.contract]);
const landingPage = buildContractPagePath({
projectMeta: props.projectMeta,
chainIdOrSlug: props.contract.chain.id.toString(),
contractAddress: props.contract.address,
});
router.replace(landingPage);
}, [router, props]);
return <LoadingPage />;
}
@@ -2,8 +2,9 @@
import { useQuery } from "@tanstack/react-query";
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
@@ -15,7 +16,7 @@ export function ContractPageLayoutClient(props: {
contract: ThirdwebContract;
children: React.ReactNode;
teamsAndProjects: MinimalTeamsAndProjects | undefined;
client: ThirdwebClient;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
const headerMetadataQuery = useQuery({
@@ -39,6 +40,7 @@ export function ContractPageLayoutClient(props: {
chainSlug: props.chainMetadata.slug,
contractAddress: props.contract.address,
metadata: metadataQuery.data,
projectMeta: props.projectMeta,
});
return (
@@ -4,8 +4,9 @@ import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ContractMetadata } from "./contract-metadata";
import { PrimaryDashboardButton } from "./primary-dashboard-button";
@@ -15,8 +16,8 @@ export function ContractPageLayout(props: {
children: React.ReactNode;
sidebarLinks: SidebarLink[];
dashboardContractMetadata: DashboardContractMetadata | undefined;
client: ThirdwebClient;
teamsAndProjects: MinimalTeamsAndProjects | undefined;
projectMeta: ProjectMeta | undefined;
externalLinks:
| {
name: string;
@@ -31,7 +32,7 @@ export function ContractPageLayout(props: {
dashboardContractMetadata,
externalLinks,
teamsAndProjects,
client,
projectMeta,
} = props;
return (
@@ -46,6 +47,7 @@ export function ContractPageLayout(props: {
externalLinks={externalLinks}
/>
<PrimaryDashboardButton
projectMeta={projectMeta}
contractAddress={contract.address}
chain={contract.chain}
contractInfo={{
@@ -54,7 +56,7 @@ export function ContractPageLayout(props: {
contractAddress: contract.address,
}}
teamsAndProjects={teamsAndProjects}
client={client}
client={contract.client}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
@@ -26,6 +26,8 @@ import { useState } from "react";
import { toast } from "sonner";
import type { Chain, ThirdwebClient } from "thirdweb";
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/(sidebar)/hooks/project-contracts";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../_utils/contract-page-path";
const TRACKING_CATEGORY = "add_to_dashboard_upsell";
@@ -36,6 +38,7 @@ type AddToDashboardCardProps = {
hideCodePageLink?: boolean;
teamsAndProjects: MinimalTeamsAndProjects | undefined;
client: ThirdwebClient;
projectMeta: ProjectMeta | undefined;
};
export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
@@ -45,9 +48,17 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
hideCodePageLink,
teamsAndProjects,
client,
projectMeta,
}) => {
const pathname = usePathname();
const codePagePath = buildContractPagePath({
projectMeta,
chainIdOrSlug: contractInfo.chainSlug,
contractAddress: contractAddress,
subpath: "/code",
});
// if user is not logged in
if (!teamsAndProjects) {
if (hideCodePageLink) {
@@ -57,9 +68,7 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
if (!pathname?.endsWith("/code")) {
return (
<Button variant="outline" asChild className="gap-2">
<Link
href={`/${contractInfo.chainSlug}/${contractInfo.contractAddress}/code`}
>
<Link href={codePagePath}>
<CodeIcon className="size-4" />
Code Snippets
</Link>
@@ -70,6 +79,11 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
return null;
}
// if user is on a project page
if (projectMeta) {
return null;
}
return (
<AddToProjectButton
contractAddress={contractAddress}
@@ -0,0 +1,15 @@
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
export function buildContractPagePath(params: {
projectMeta: ProjectMeta | undefined;
chainIdOrSlug: string;
contractAddress: string;
subpath?: `/${string}`;
}) {
const { projectMeta, chainIdOrSlug, contractAddress, subpath } = params;
if (projectMeta) {
return `/team/${projectMeta.teamSlug}/${projectMeta.projectSlug}/contract/${chainIdOrSlug}/${contractAddress}${subpath || ""}`;
}
return `/${chainIdOrSlug}/${contractAddress}${subpath || ""}`;
}
@@ -7,10 +7,11 @@ import { getUserThirdwebClient } from "../../../../../api/lib/getAuthToken";
export async function getContractPageParamsInfo(params: {
contractAddress: string;
chain_id: string;
chainIdOrSlug: string;
teamId: string | undefined;
}) {
const contractAddress = getCheckSummedAddress(params.contractAddress);
const chainSlugOrId = params.chain_id;
const chainSlugOrId = params.chainIdOrSlug;
const chainMetadata = await fetchChainWithLocalOverrides(chainSlugOrId);
if (!chainMetadata) {
@@ -31,7 +32,9 @@ export async function getContractPageParamsInfo(params: {
// eslint-disable-next-line no-restricted-syntax
chain: mapV4ChainToV5Chain(chainMetadata),
// if we have the auth token pass it into the client
client: await getUserThirdwebClient(),
client: await getUserThirdwebClient({
teamId: params.teamId,
}),
});
return {
@@ -1,4 +1,6 @@
import type { SidebarBaseLink, SidebarLink } from "@/components/blocks/Sidebar";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "./contract-page-path";
import type { ContractPageMetadata } from "./getContractPageMetadata";
type SidebarBaseLinkWithHide = SidebarBaseLink & {
@@ -9,8 +11,13 @@ export function getContractPageSidebarLinks(data: {
metadata: ContractPageMetadata;
contractAddress: string;
chainSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const layoutPrefix = `/${data.chainSlug}/${data.contractAddress}`;
const layoutPrefix = buildContractPagePath({
projectMeta: data.projectMeta,
chainIdOrSlug: data.chainSlug,
contractAddress: data.contractAddress,
});
const generalLinks: SidebarBaseLinkWithHide[] = [
{
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
@@ -8,6 +9,7 @@ import { AccountSigners } from "./components/account-signers";
export function AccountSignersClient(props: {
contract: ThirdwebContract;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -20,7 +22,12 @@ export function AccountSignersClient(props: {
}
if (!metadataQuery.data.isAccountPermissionsSupported) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return <AccountSigners contract={props.contract} />;
@@ -1,34 +1,15 @@
import { notFound, redirect } from "next/navigation";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountSignersClient } from "./AccountSigners.client";
import { AccountSigners } from "./components/account-signers";
import type { PublicContractPageParams } from "../types";
import { SharedContractAccountPermissionsPage } from "./shared-account-permissions-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return <AccountSignersClient contract={clientContract} />;
}
const { isAccountPermissionsSupported } =
await getContractPageMetadata(serverContract);
if (!isAccountPermissionsSupported) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
return <AccountSigners contract={clientContract} />;
return (
<SharedContractAccountPermissionsPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
/>
);
}
@@ -0,0 +1,47 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountSignersClient } from "./AccountSigners.client";
import { AccountSigners } from "./components/account-signers";
export async function SharedContractAccountPermissionsPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<AccountSignersClient
contract={clientContract}
projectMeta={props.projectMeta}
/>
);
}
const { isAccountPermissionsSupported } =
await getContractPageMetadata(serverContract);
if (!isAccountPermissionsSupported) {
redirectToContractLandingPage({
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
projectMeta: props.projectMeta,
});
}
return <AccountSigners contract={clientContract} />;
}
@@ -2,6 +2,7 @@
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
@@ -11,6 +12,7 @@ export function AccountPageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -23,7 +25,12 @@ export function AccountPageClient(props: {
}
if (!metadataQuery.data.isAccount) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return (
@@ -32,6 +39,7 @@ export function AccountPageClient(props: {
chainMetadata={props.chainMetadata}
isLoggedIn={props.isLoggedIn}
isInsightSupported={metadataQuery.data.isInsightSupported}
projectMeta={props.projectMeta}
/>
);
}
@@ -3,6 +3,7 @@
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { Heading } from "tw-components";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { AccountBalance } from "./components/account-balance";
import { DepositNative } from "./components/deposit-native";
import { NftsOwned } from "./components/nfts-owned";
@@ -12,6 +13,7 @@ interface AccountPageProps {
chainMetadata: ChainMetadata;
isLoggedIn: boolean;
isInsightSupported: boolean;
projectMeta: ProjectMeta | undefined;
}
export const AccountPage: React.FC<AccountPageProps> = ({
@@ -19,6 +21,7 @@ export const AccountPage: React.FC<AccountPageProps> = ({
chainMetadata,
isLoggedIn,
isInsightSupported,
projectMeta,
}) => {
const symbol = chainMetadata.nativeCurrency.symbol || "Native Token";
@@ -44,7 +47,11 @@ export const AccountPage: React.FC<AccountPageProps> = ({
<div className="flex flex-row items-center justify-between">
<Heading size="title.sm">NFTs owned</Heading>
</div>
<NftsOwned contract={contract} isInsightSupported={isInsightSupported} />
<NftsOwned
contract={contract}
isInsightSupported={isInsightSupported}
projectMeta={projectMeta}
/>
</div>
);
};
@@ -2,16 +2,19 @@
import { useWalletNFTs } from "@3rdweb-sdk/react";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { NFTCards } from "../../_components/NFTCards";
interface NftsOwnedProps {
contract: ThirdwebContract;
isInsightSupported: boolean;
projectMeta: ProjectMeta | undefined;
}
export const NftsOwned: React.FC<NftsOwnedProps> = ({
contract,
isInsightSupported,
projectMeta,
}) => {
const { data: walletNFTs, isPending: isWalletNFTsLoading } = useWalletNFTs({
chainId: contract.chain.id,
@@ -24,6 +27,7 @@ export const NftsOwned: React.FC<NftsOwnedProps> = ({
return nfts.length !== 0 ? (
<NFTCards
projectMeta={projectMeta}
nfts={nfts.map((nft) => ({
id: BigInt(nft.id),
supply: BigInt(nft.supply),
@@ -1,51 +1,17 @@
import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountPage } from "./AccountPage";
import { AccountPageClient } from "./AccountPage.client";
import type { PublicContractPageParams } from "../types";
import { SharedContractAccountPage } from "./shared-account-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
info;
const account = await getRawAccount();
if (isLocalhostChain) {
return (
<AccountPageClient
contract={clientContract}
chainMetadata={chainMetadata}
isLoggedIn={!!account}
/>
);
}
const { isAccount, isInsightSupported } =
await getContractPageMetadata(serverContract);
if (!isAccount) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<AccountPage
contract={clientContract}
chainMetadata={chainMetadata}
<SharedContractAccountPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
isInsightSupported={isInsightSupported}
/>
);
}
@@ -0,0 +1,57 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountPage } from "./AccountPage";
import { AccountPageClient } from "./AccountPage.client";
export async function SharedContractAccountPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
if (info.isLocalhostChain) {
return (
<AccountPageClient
contract={info.clientContract}
chainMetadata={info.chainMetadata}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
const { isAccount, isInsightSupported } = await getContractPageMetadata(
info.serverContract,
);
if (!isAccount) {
redirectToContractLandingPage({
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
projectMeta: props.projectMeta,
});
}
return (
<AccountPage
contract={info.clientContract}
chainMetadata={info.chainMetadata}
isLoggedIn={props.isLoggedIn}
isInsightSupported={isInsightSupported}
projectMeta={props.projectMeta}
/>
);
}
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
@@ -9,6 +10,7 @@ import { AccountsPage } from "./AccountsPage";
export function AccountsPageClient(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -21,10 +23,19 @@ export function AccountsPageClient(props: {
}
if (!metadataQuery.data.isAccountFactory) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return (
<AccountsPage contract={props.contract} isLoggedIn={props.isLoggedIn} />
<AccountsPage
contract={props.contract}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
@@ -3,6 +3,7 @@
import { ButtonGroup, Flex } from "@chakra-ui/react";
import type { ThirdwebContract } from "thirdweb";
import { Heading, TrackedLinkButton } from "tw-components";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { AccountsCount } from "./components/accounts-count";
import { AccountsTable } from "./components/accounts-table";
import { CreateAccountButton } from "./components/create-account-button";
@@ -10,11 +11,13 @@ import { CreateAccountButton } from "./components/create-account-button";
interface AccountsPageProps {
contract: ThirdwebContract;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}
export const AccountsPage: React.FC<AccountsPageProps> = ({
contract,
isLoggedIn,
projectMeta,
}) => {
return (
<Flex direction="column" gap={6}>
@@ -43,7 +46,7 @@ export const AccountsPage: React.FC<AccountsPageProps> = ({
</ButtonGroup>
</Flex>
<AccountsCount contract={contract} />
<AccountsTable contract={contract} />
<AccountsTable contract={contract} projectMeta={projectMeta} />
</Flex>
);
};
@@ -1,16 +1,9 @@
"use client";
import {
SimpleGrid,
Skeleton,
Stat,
StatLabel,
StatNumber,
} from "@chakra-ui/react";
import type { ThirdwebContract } from "thirdweb";
import { totalAccounts } from "thirdweb/extensions/erc4337";
import { useReadContract } from "thirdweb/react";
import { Card } from "tw-components";
import { StatCard } from "../../overview/components/stat-card";
type AccountsCountProps = {
contract: ThirdwebContract;
@@ -19,13 +12,12 @@ type AccountsCountProps = {
export const AccountsCount: React.FC<AccountsCountProps> = ({ contract }) => {
const totalAccountsQuery = useReadContract(totalAccounts, { contract });
return (
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={{ base: 3, md: 6 }}>
<Card as={Stat}>
<StatLabel mb={{ base: 1, md: 0 }}>Total Accounts</StatLabel>
<Skeleton isLoaded={totalAccountsQuery.isSuccess}>
<StatNumber>{totalAccountsQuery.data?.toString()}</StatNumber>
</Skeleton>
</Card>
</SimpleGrid>
<div className="grid grid-cols-1 gap-3 md:grid-cols-3 md:gap-6">
<StatCard
label="Total Accounts"
value={totalAccountsQuery.data?.toString() || "0"}
isPending={totalAccountsQuery.isPending}
/>
</div>
);
};
@@ -16,6 +16,8 @@ import type { ThirdwebContract } from "thirdweb";
import { getAccounts, totalAccounts } from "thirdweb/extensions/erc4337";
import { useReadContract } from "thirdweb/react";
import { Text, TrackedCopyButton } from "tw-components";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../../_utils/contract-page-path";
const columnHelper = createColumnHelper<{ account: string }>();
@@ -38,9 +40,13 @@ const columns = [
type AccountsTableProps = {
contract: ThirdwebContract;
projectMeta: ProjectMeta | undefined;
};
export const AccountsTable: React.FC<AccountsTableProps> = ({ contract }) => {
export const AccountsTable: React.FC<AccountsTableProps> = ({
contract,
projectMeta,
}) => {
const router = useDashboardRouter();
const chainSlug = useChainSlug(contract.chain.id);
@@ -92,7 +98,13 @@ export const AccountsTable: React.FC<AccountsTableProps> = ({ contract }) => {
isPending={accountsQuery.isPending}
isFetched={accountsQuery.isFetched}
onRowClick={(row) => {
router.push(`/${chainSlug}/${row.account}`);
const accountContractPagePath = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug.toString(),
contractAddress: row.account,
});
router.push(accountContractPagePath);
}}
/>
{/* pagination */}
@@ -1,41 +1,17 @@
import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountsPage } from "./AccountsPage";
import { AccountsPageClient } from "./AccountsPage.client";
import type { PublicContractPageParams } from "../types";
import { SharedAccountsPage } from "./shared-accounts-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
const [account, { isAccountFactory }] = await Promise.all([
getRawAccount(),
isLocalhostChain
? { isAccountFactory: undefined }
: getContractPageMetadata(serverContract),
]);
if (isLocalhostChain) {
return (
<AccountsPageClient contract={clientContract} isLoggedIn={!!account} />
);
}
if (!isAccountFactory) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
return <AccountsPage contract={clientContract} isLoggedIn={!!account} />;
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<SharedAccountsPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
/>
);
}
@@ -0,0 +1,54 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountsPage } from "./AccountsPage";
import { AccountsPageClient } from "./AccountsPage.client";
export async function SharedAccountsPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<AccountsPageClient
contract={clientContract}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
const { isAccountFactory } = await getContractPageMetadata(serverContract);
if (!isAccountFactory) {
redirectToContractLandingPage({
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
projectMeta: props.projectMeta,
});
}
return (
<AccountsPage
contract={clientContract}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
@@ -1,71 +1,15 @@
import { notFound, redirect } from "next/navigation";
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
import { type Abi, toEventSelector, toFunctionSelector } from "thirdweb/utils";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
import type { PublicContractPageParams } from "../types";
import { SharedAnalyticsPage } from "./shared-analytics-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const [
{ eventSelectorToName, writeFnSelectorToName },
{ isInsightSupported },
] = await Promise.all([
getSelectors(info.serverContract),
getContractPageMetadata(info.serverContract),
]);
if (!isInsightSupported) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
return (
<ContractAnalyticsPage
contract={info.clientContract}
writeFnSelectorToNameRecord={writeFnSelectorToName}
eventSelectorToNameRecord={eventSelectorToName}
<SharedAnalyticsPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
/>
);
}
async function getSelectors(contract: ThirdwebContract) {
try {
const abi = await resolveContractAbi<Abi>(contract);
const writeFnSelectorToName: Record<string, string> = {};
const eventSelectorToName: Record<string, string> = {};
for (const item of abi) {
if (item.type === "event") {
eventSelectorToName[toEventSelector(item)] = item.name;
} else if (
// if write function
item.type === "function" &&
item.stateMutability !== "view" &&
item.stateMutability !== "pure"
) {
writeFnSelectorToName[toFunctionSelector(item)] = item.name;
}
}
return {
writeFnSelectorToName,
eventSelectorToName,
};
} catch {
return {
writeFnSelectorToName: {},
eventSelectorToName: {},
};
}
}
@@ -0,0 +1,79 @@
import { notFound } from "next/navigation";
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
import { type Abi, toEventSelector, toFunctionSelector } from "thirdweb/utils";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
export async function SharedAnalyticsPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const [
{ eventSelectorToName, writeFnSelectorToName },
{ isInsightSupported },
] = await Promise.all([
getSelectors(info.serverContract),
getContractPageMetadata(info.serverContract),
]);
if (!isInsightSupported) {
redirectToContractLandingPage({
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
projectMeta: props.projectMeta,
});
}
return (
<ContractAnalyticsPage
contract={info.clientContract}
writeFnSelectorToNameRecord={writeFnSelectorToName}
eventSelectorToNameRecord={eventSelectorToName}
/>
);
}
async function getSelectors(contract: ThirdwebContract) {
try {
const abi = await resolveContractAbi<Abi>(contract);
const writeFnSelectorToName: Record<string, string> = {};
const eventSelectorToName: Record<string, string> = {};
for (const item of abi) {
if (item.type === "event") {
eventSelectorToName[toEventSelector(item)] = item.name;
} else if (
// if write function
item.type === "function" &&
item.stateMutability !== "view" &&
item.stateMutability !== "pure"
) {
writeFnSelectorToName[toFunctionSelector(item)] = item.name;
}
}
return {
writeFnSelectorToName,
eventSelectorToName,
};
} catch {
return {
writeFnSelectorToName: {},
eventSelectorToName: {},
};
}
}
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
@@ -9,6 +10,7 @@ import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
export function ClaimConditionsClient(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -27,7 +29,12 @@ export function ClaimConditionsClient(props: {
} = metadataQuery.data;
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return (
@@ -1,58 +1,17 @@
import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ClaimConditionsClient } from "./ClaimConditions.client";
import type { PublicContractPageParams } from "../types";
import { SharedClaimConditionsPage } from "./shared-claim-conditions-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
const [
account,
{
isERC20ClaimConditionsSupported,
isERC721ClaimConditionsSupported,
supportedERCs,
},
] = await Promise.all([
getRawAccount(),
isLocalhostChain
? {
isERC20ClaimConditionsSupported: undefined,
isERC721ClaimConditionsSupported: undefined,
supportedERCs: undefined,
}
: getContractPageMetadata(serverContract),
]);
if (isLocalhostChain) {
return (
<ClaimConditionsClient contract={clientContract} isLoggedIn={!!account} />
);
}
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<ClaimConditions
contract={clientContract}
isERC20={supportedERCs.isERC20}
<SharedClaimConditionsPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
isMultiphase={true}
/>
);
}
@@ -0,0 +1,59 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ClaimConditionsClient } from "./ClaimConditions.client";
export async function SharedClaimConditionsPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<ClaimConditionsClient
contract={clientContract}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
const {
isERC20ClaimConditionsSupported,
isERC721ClaimConditionsSupported,
supportedERCs,
} = await getContractPageMetadata(serverContract);
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
redirectToContractLandingPage({
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
projectMeta: props.projectMeta,
});
}
return (
<ClaimConditions
contract={clientContract}
isERC20={supportedERCs.isERC20}
isLoggedIn={props.isLoggedIn}
isMultiphase={true}
/>
);
}
@@ -1,41 +1,15 @@
import { notFound } from "next/navigation";
import { resolveContractAbi } from "thirdweb/contract";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { ContractCodePage } from "./contract-code-page";
import { ContractCodePageClient } from "./contract-code-page.client";
import type { PublicContractPageParams } from "../types";
import { SharedCodePage } from "./shared-code-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
info;
if (isLocalhostChain) {
return (
<ContractCodePageClient
contract={clientContract}
chainMetadata={chainMetadata}
/>
);
}
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
return (
<ContractCodePage
abi={abi}
contract={clientContract}
chainMetadata={chainMetadata}
<SharedCodePage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
/>
);
}
@@ -0,0 +1,44 @@
import { notFound } from "next/navigation";
import { resolveContractAbi } from "thirdweb/contract";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { ContractCodePage } from "./contract-code-page";
import { ContractCodePageClient } from "./contract-code-page.client";
export async function SharedCodePage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
info;
if (isLocalhostChain) {
return (
<ContractCodePageClient
contract={clientContract}
chainMetadata={chainMetadata}
/>
);
}
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
return (
<ContractCodePage
abi={abi}
contract={clientContract}
chainMetadata={chainMetadata}
/>
);
}
@@ -58,6 +58,8 @@ import {
import { useActiveAccount, useSwitchActiveWalletChain } from "thirdweb/react";
import { concatHex, padHex } from "thirdweb/utils";
import { z } from "zod";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../_utils/contract-page-path";
import { SingleNetworkSelector } from "./single-network-selector";
type CrossChain = {
@@ -91,6 +93,7 @@ export function DataTable({
inputSalt,
initCode,
isDirectDeploy,
projectMeta,
}: {
data: CrossChain[];
coreMetadata: FetchDeployMetadataResult;
@@ -100,6 +103,7 @@ export function DataTable({
inputSalt?: `0x${string}`;
initCode?: `0x${string}`;
isDirectDeploy: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const activeAccount = useActiveAccount();
const switchChain = useSwitchActiveWalletChain();
@@ -235,11 +239,17 @@ export function DataTable({
header: "Network",
cell: ({ row }) => {
if (row.getValue("status") === "DEPLOYED") {
const href = buildContractPagePath({
projectMeta,
chainIdOrSlug: `${row.getValue("chainId")}`,
contractAddress: coreContract.address,
});
return (
<Link
target="_blank"
className="text-blue-500 underline"
href={`/${row.getValue("chainId")}/${coreContract.address}`}
href={href}
>
{row.getValue("network")}
</Link>
@@ -405,9 +415,17 @@ export function DataTable({
});
}
deployStatusModal.nextStep();
deployStatusModal.setViewContractLink(
`/${chain.id}/${crosschainContractAddress}`,
);
if (crosschainContractAddress) {
const contractLink = buildContractPagePath({
projectMeta,
chainIdOrSlug: `${chain.id}`,
contractAddress: crosschainContractAddress,
});
deployStatusModal.setViewContractLink(contractLink);
}
deployStatusModal.close();
setCustomChainData((prevData) =>
@@ -1,287 +1,15 @@
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 || []
);
}
import type { PublicContractPageParams } from "../types";
import { SharedCrossChainPage } from "./shared-cross-chain-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
const ProxyDeployedEvent = prepareEvent({
signature:
"event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
filters: {
proxy: params.contractAddress.toLowerCase(),
},
});
if (!info) {
notFound();
}
const { clientContract, serverContract } = info;
const isModularCore = (await getContractPageMetadata(serverContract))
.isModularCore;
let twCloneFactoryContract: Readonly<
ContractOptions<[], `0x${string}`>
> | null = null;
try {
twCloneFactoryContract = await getDeployedCloneFactoryContract({
chain: serverContract.chain,
client: serverContract.client,
});
} catch {}
const originalCode = await eth_getCode(
getRpcClient({
client: serverContract.client,
chain: serverContract.chain,
}),
{
address: serverContract.address,
},
);
let initCode: `0x${string}` = "0x";
let creationTxReceipt: TransactionReceipt | undefined;
let isDirectDeploy = false;
try {
const res = await fetch(
`https://contract.thirdweb-dev.com/creation/${serverContract.chain.id}/${serverContract.address}`,
);
const creationData = await res.json();
if (creationData.status === "1" && creationData.result[0]?.txHash) {
const rpcClient = getRpcClient({
client: serverContract.client,
chain: serverContract.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],
});
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: serverContract.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.chainId !== serverContract.chain.id);
let coreMetadata: FetchDeployMetadataResult | undefined;
try {
coreMetadata = (
await fetchPublishedContractsFromDeploy({
contract: serverContract,
client: serverContract.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: serverContract,
events: [moduleEvent],
});
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: serverContract,
data: initializeData,
});
const abi = await resolveContractAbi(serverContract).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: serverContract.chain,
client: serverContract.client,
address: m,
}),
client: serverContract.client,
})
).at(-1),
),
)) as FetchDeployMetadataResult[])
: undefined;
}
if (!isDirectDeploy && !initializeData) {
return NoCrossChainPrompt();
}
return (
<>
<div>
<h2 className="mb-1 font-bold text-2xl tracking-tight">
Cross-chain contracts
</h2>
<p className="text-muted-foreground">
Deterministically deploy and interact with your contracts on multiple
networks.
</p>
</div>
<div className="h-10" />
<DataTable
coreMetadata={coreMetadata}
modulesMetadata={modulesMetadata}
initializeData={initializeData}
inputSalt={inputSalt}
data={chainsDeployedOn}
coreContract={clientContract}
initCode={initCode}
isDirectDeploy={isDirectDeploy}
/>
</>
<SharedCrossChainPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
/>
);
}
@@ -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 type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { DataTable } from "./data-table";
import { NoCrossChainPrompt } from "./no-crosschain-prompt";
export async function SharedCrossChainPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
const ProxyDeployedEvent = prepareEvent({
signature:
"event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
filters: {
proxy: props.contractAddress.toLowerCase(),
},
});
if (!info) {
notFound();
}
const { clientContract, serverContract } = info;
const isModularCore = (await getContractPageMetadata(serverContract))
.isModularCore;
let twCloneFactoryContract: Readonly<
ContractOptions<[], `0x${string}`>
> | null = null;
try {
twCloneFactoryContract = await getDeployedCloneFactoryContract({
chain: serverContract.chain,
client: serverContract.client,
});
} catch {}
const originalCode = await eth_getCode(
getRpcClient({
client: serverContract.client,
chain: serverContract.chain,
}),
{
address: serverContract.address,
},
);
let initCode: `0x${string}` = "0x";
let creationTxReceipt: TransactionReceipt | undefined;
let isDirectDeploy = false;
try {
const res = await fetch(
`https://contract.thirdweb-dev.com/creation/${serverContract.chain.id}/${serverContract.address}`,
);
const creationData = await res.json();
if (creationData.status === "1" && creationData.result[0]?.txHash) {
const rpcClient = getRpcClient({
client: serverContract.client,
chain: serverContract.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],
});
const event = events.find(
(e) => e.args.proxy.toLowerCase() === props.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: serverContract.client,
chain,
});
const code = await eth_getCode(rpcRequest, {
address: props.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.chainId !== serverContract.chain.id);
let coreMetadata: FetchDeployMetadataResult | undefined;
try {
coreMetadata = (
await fetchPublishedContractsFromDeploy({
contract: serverContract,
client: serverContract.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: serverContract,
events: [moduleEvent],
});
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: serverContract,
data: initializeData,
});
const abi = await resolveContractAbi(serverContract).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: serverContract.chain,
client: serverContract.client,
address: m,
}),
client: serverContract.client,
})
).at(-1),
),
)) as FetchDeployMetadataResult[])
: undefined;
}
if (!isDirectDeploy && !initializeData) {
return NoCrossChainPrompt();
}
return (
<>
<div>
<h2 className="mb-1 font-bold text-2xl tracking-tight">
Cross-chain contracts
</h2>
<p className="text-muted-foreground">
Deterministically deploy and interact with your contracts on multiple
networks.
</p>
</div>
<div className="h-10" />
<DataTable
coreMetadata={coreMetadata}
modulesMetadata={modulesMetadata}
initializeData={initializeData}
inputSalt={inputSalt}
data={chainsDeployedOn}
coreContract={clientContract}
initCode={initCode}
isDirectDeploy={isDirectDeploy}
projectMeta={props.projectMeta}
/>
</>
);
}
@@ -33,12 +33,18 @@ import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { stringify } from "thirdweb/utils";
import { Button, Card, FormLabel, Heading, Text } from "tw-components";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../_utils/contract-page-path";
interface EventsFeedProps {
contract: ThirdwebContract;
projectMeta: ProjectMeta | undefined;
}
export const EventsFeed: React.FC<EventsFeedProps> = ({ contract }) => {
export const EventsFeed: React.FC<EventsFeedProps> = ({
contract,
projectMeta,
}) => {
const [autoUpdate, setAutoUpdate] = useState(true);
const allEvents = useActivity(contract, autoUpdate);
const searchParams = useSearchParams();
@@ -81,11 +87,18 @@ export const EventsFeed: React.FC<EventsFeedProps> = ({ contract }) => {
value={selectedEvent}
onChange={(e) => {
const val = e.target.value;
if (eventTypes.includes(val)) {
const path =
e.target.value === "all"
? `/${chainSlug}/${contract.address}/events`
: `/${chainSlug}/${contract.address}/events?event=${val}`;
const path = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug.toString(),
contractAddress: contract.address,
subpath:
e.target.value === "all"
? "/events"
: `/events?event=${val}`,
});
router.push(path);
setSelectedEvent(val);
}
@@ -154,6 +167,7 @@ export const EventsFeed: React.FC<EventsFeedProps> = ({ contract }) => {
setSelectedEvent={setSelectedEvent}
contractAddress={contract.address}
chainSlug={chainSlug}
projectMeta={projectMeta}
/>
))}
</Accordion>
@@ -168,6 +182,7 @@ interface EventsFeedItemProps {
setSelectedEvent: React.Dispatch<React.SetStateAction<string>>;
contractAddress: string;
chainSlug: string | number;
projectMeta: ProjectMeta | undefined;
}
const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
@@ -175,9 +190,9 @@ const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
setSelectedEvent,
contractAddress,
chainSlug,
projectMeta,
}) => {
const { onCopy } = useClipboard(transaction.transactionHash);
const router = useDashboardRouter();
return (
@@ -246,9 +261,14 @@ const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
key={idx}
onClick={(ev) => {
ev.stopPropagation();
router.push(
`/${chainSlug}/${contractAddress}/events?event=${e.eventName}`,
);
const path = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug.toString(),
contractAddress: contractAddress,
subpath: `/events?event=${e.eventName}`,
});
router.push(path);
setSelectedEvent(e.eventName);
}}
>
@@ -1,19 +1,15 @@
import { notFound } from "next/navigation";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { EventsFeed } from "./events-feed";
import type { PublicContractPageParams } from "../types";
import { SharedEventsPage } from "./shared-events-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
return <EventsFeed contract={info.clientContract} />;
return (
<SharedEventsPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
/>
);
}
@@ -0,0 +1,27 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { EventsFeed } from "./events-feed";
export async function SharedEventsPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
return (
<EventsFeed
contract={info.clientContract}
projectMeta={props.projectMeta}
/>
);
}
@@ -1,45 +1,17 @@
import { notFound } from "next/navigation";
import { resolveContractAbi } from "thirdweb/contract";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { ContractExplorerPage } from "./ContractExplorerPage";
import { ContractExplorerPageClient } from "./ContractExplorerPage.client";
import type { PublicContractPageParams } from "../types";
import { SharedExplorerPage } from "./shared-explorer-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
info;
const account = await getRawAccount();
if (isLocalhostChain) {
return (
<ContractExplorerPageClient
contract={clientContract}
chainMetadata={chainMetadata}
isLoggedIn={!!account}
/>
);
}
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
return (
<ContractExplorerPage
contract={clientContract}
abi={abi}
chainMetadata={chainMetadata}
<SharedExplorerPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
/>
);
@@ -0,0 +1,47 @@
import { notFound } from "next/navigation";
import { resolveContractAbi } from "thirdweb/contract";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { ContractExplorerPage } from "./ContractExplorerPage";
import { ContractExplorerPageClient } from "./ContractExplorerPage.client";
export async function SharedExplorerPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
info;
if (isLocalhostChain) {
return (
<ContractExplorerPageClient
contract={clientContract}
chainMetadata={chainMetadata}
isLoggedIn={props.isLoggedIn}
/>
);
}
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
return (
<ContractExplorerPage
contract={clientContract}
abi={abi}
chainMetadata={chainMetadata}
isLoggedIn={props.isLoggedIn}
/>
);
}
@@ -1,26 +1,8 @@
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
import { resolveFunctionSelectors } from "lib/selectors";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getContractMetadata } from "thirdweb/extensions/common";
import { isAddress, isContractDeployed } from "thirdweb/utils";
import { shortenIfAddress } from "utils/usedapp-external";
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
import { examplePrompts } from "../../../../../nebula-app/(app)/data/examplePrompts";
import {
getAuthTokenWalletAddress,
getUserThirdwebClient,
} from "../../../../api/lib/getAuthToken";
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
import { ContractPageLayout } from "./_layout/contract-page-layout";
import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
SharedContractLayout,
generateContractLayoutMetadata,
} from "./shared-layout";
export default async function Layout(props: {
params: Promise<{
@@ -30,130 +12,17 @@ export default async function Layout(props: {
children: React.ReactNode;
}) {
const params = await props.params;
if (!isAddress(params.contractAddress)) {
return notFound();
}
const info = await getContractPageParamsInfo(params);
if (!info) {
return <ConfigureCustomChain chainSlug={params.chain_id} />;
}
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
info;
if (chainMetadata.status === "deprecated") {
notFound();
}
const accountAddress = await getAuthTokenWalletAddress();
const client = await getUserThirdwebClient();
const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
if (isLocalhostChain) {
return (
<ContractPageLayoutClient
chainMetadata={chainMetadata}
contract={clientContract}
teamsAndProjects={teamsAndProjects}
client={client}
>
{props.children}
</ContractPageLayoutClient>
);
}
// check if the contract exists
const isValidContract = await isContractDeployed(serverContract).catch(
() => false,
);
if (!isValidContract) {
// TODO - replace 404 with a better page to upsell deploy or other thirdweb products
notFound();
}
const contractPageMetadata = await getContractPageMetadata(serverContract);
const sidebarLinks = getContractPageSidebarLinks({
chainSlug: chainMetadata.slug,
contractAddress: serverContract.address,
metadata: contractPageMetadata,
});
const { contractMetadata, externalLinks } =
await getContractMetadataHeaderData(serverContract);
const contractAddress = serverContract.address;
const chainName = chainMetadata.name;
const chainId = chainMetadata.chainId;
const contractPromptPrefix = `A user is viewing the contract address ${contractAddress} on ${chainName} (Chain ID: ${chainId}). Provide a concise summary of this contract's functionalities, such as token minting, staking, or governance mechanisms. Focus on what the contract enables users to do, avoiding transaction execution details unless requested.
Users may be interested in how to interact with the contract. Outline common interaction patterns, such as claiming rewards, participating in governance, or transferring assets. Emphasize the contract's capabilities without guiding through transaction processes unless asked.
Provide insights into how the contract is being used. Share information on user engagement, transaction volumes, or integration with other dApps, focusing on the contract's role within the broader ecosystem.
Users may be considering integrating the contract into their applications. Discuss how this contract's functionalities can be leveraged within different types of dApps, highlighting potential use cases and benefits.
The following is the user's message:`;
return (
<ContractPageLayout
chainMetadata={chainMetadata}
contract={clientContract}
sidebarLinks={sidebarLinks}
dashboardContractMetadata={contractMetadata}
externalLinks={externalLinks}
teamsAndProjects={teamsAndProjects}
client={client}
<SharedContractLayout
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
>
<NebulaChatButton
isLoggedIn={!!accountAddress}
networks={info.chainMetadata.testnet ? "testnet" : "mainnet"}
isFloating={true}
pageType="contract"
label="Ask AI about this contract"
client={client}
nebulaParams={{
messagePrefix: contractPromptPrefix,
chainIds: [chainId],
wallet: accountAddress ?? undefined,
}}
examplePrompts={examplePrompts}
/>
{props.children}
</ContractPageLayout>
</SharedContractLayout>
);
}
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,
image: x.image,
})),
})),
);
return teamsAndProjects;
} catch {
return undefined;
}
}
export async function generateMetadata(props: {
params: Promise<{
contractAddress: string;
@@ -161,54 +30,8 @@ export async function generateMetadata(props: {
}>;
}): Promise<Metadata> {
const params = await props.params;
try {
const info = await getContractPageParamsInfo(params);
if (!info) {
throw new Error();
}
const [functionSelectors, contractMetadata] = await Promise.all([
resolveFunctionSelectors(info.serverContract),
getContractMetadata({
contract: info.serverContract,
}),
]);
if (contractMetadata.name === null) {
throw new Error();
}
const { isERC1155, isERC20, isERC721 } = supportedERCs(functionSelectors);
const contractDisplayName = `${contractMetadata.name}${
contractMetadata.symbol ? ` (${contractMetadata.symbol})` : ""
}`;
const cleanedChainName = info?.chainMetadata?.name
.replace("Mainnet", "")
.replace("Testnet", "")
.trim();
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
let description = "";
if (isERC721 || isERC1155) {
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
} else if (isERC20) {
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
} else {
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
}
return {
title: title,
description: description,
};
} catch {
return {
title: `${shortenIfAddress(params.contractAddress)} | ${params.chain_id}`,
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chain_id}`,
};
}
return generateContractLayoutMetadata({
contractAddress: params.contractAddress,
chainIdOrSlug: params.chain_id,
});
}
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
@@ -9,6 +10,7 @@ import { ContractEditModulesPage } from "./ContractEditModulesPage";
export function ContractEditModulesPageClient(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -21,7 +23,12 @@ export function ContractEditModulesPageClient(props: {
}
if (!metadataQuery.data.isModularCore) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return (
@@ -1,43 +1,17 @@
import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractEditModulesPage } from "./ContractEditModulesPage";
import { ContractEditModulesPageClient } from "./ContractEditModulesPage.client";
import type { PublicContractPageParams } from "../types";
import { SharedModulesPage } from "./shared-modules-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
const account = await getRawAccount();
if (isLocalhostChain) {
return (
<ContractEditModulesPageClient
contract={clientContract}
isLoggedIn={!!account}
/>
);
}
const { isModularCore } = await getContractPageMetadata(serverContract);
if (!isModularCore) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<ContractEditModulesPage contract={clientContract} isLoggedIn={!!account} />
<SharedModulesPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
/>
);
}
@@ -0,0 +1,53 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractEditModulesPage } from "./ContractEditModulesPage";
import { ContractEditModulesPageClient } from "./ContractEditModulesPage.client";
export async function SharedModulesPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<ContractEditModulesPageClient
contract={clientContract}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
const { isModularCore } = await getContractPageMetadata(serverContract);
if (!isModularCore) {
redirectToContractLandingPage({
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
projectMeta: props.projectMeta,
});
}
return (
<ContractEditModulesPage
contract={clientContract}
isLoggedIn={props.isLoggedIn}
/>
);
}
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
@@ -9,6 +10,7 @@ import { ContractNFTPage } from "./ContractNFTPage";
export function ContractNFTPageClient(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -23,7 +25,12 @@ export function ContractNFTPageClient(props: {
const { supportedERCs, functionSelectors } = metadataQuery.data;
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return (
@@ -32,6 +39,7 @@ export function ContractNFTPageClient(props: {
isErc721={supportedERCs.isERC721}
functionSelectors={functionSelectors}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
@@ -1,6 +1,7 @@
import type { ThirdwebContract } from "thirdweb";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { BatchLazyMintButton } from "./components/batch-lazy-mint-button";
import { NFTClaimButton } from "./components/claim-button";
import { NFTLazyMintButton } from "./components/lazy-mint-button";
@@ -16,6 +17,7 @@ interface NftOverviewPageProps {
tokenId?: string;
functionSelectors: string[];
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}
export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
@@ -23,6 +25,7 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
isErc721,
functionSelectors,
isLoggedIn,
projectMeta,
}) => {
const isERC721ClaimToSupported =
ERC721Ext.isClaimToSupported(functionSelectors);
@@ -107,6 +110,7 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
contract={contract}
isErc721={isErc721}
tokenByIndex={isTokenByIndexSupported}
projectMeta={projectMeta}
/>
)}
</div>
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
@@ -11,6 +12,7 @@ export function TokenIdPageClient(props: {
tokenId: string;
isLoggedIn: boolean;
accountAddress: string | undefined;
projectMeta: ProjectMeta | undefined;
}) {
const { contract } = props;
const metadataQuery = useContractPageMetadata(props.contract);
@@ -26,7 +28,12 @@ export function TokenIdPageClient(props: {
const { supportedERCs } = metadataQuery.data;
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return (
@@ -36,6 +43,7 @@ export function TokenIdPageClient(props: {
tokenId={props.tokenId}
isLoggedIn={props.isLoggedIn}
accountAddress={props.accountAddress}
projectMeta={props.projectMeta}
/>
);
}
@@ -1,63 +1,23 @@
import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../../account/settings/getAccount";
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { TokenIdPageClient } from "./TokenIdPage.client";
import { TokenIdPage } from "./token-id";
import type { PublicContractPageParams } from "../../types";
import { SharedNFTTokenPage } from "./shared-nfts-token-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
tokenId: string;
}>;
params: Promise<
PublicContractPageParams & {
tokenId: string;
}
>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
if (!isOnlyNumbers(params.tokenId)) {
// redirect to nfts index page
redirect(`/${params.chain_id}/${params.contractAddress}/nfts`);
}
const [accountAddress, account] = await Promise.all([
getAuthTokenWalletAddress(),
getRawAccount(),
]);
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<TokenIdPageClient
contract={clientContract}
tokenId={params.tokenId}
isLoggedIn={!!account}
accountAddress={accountAddress || undefined}
/>
);
}
const { supportedERCs } = await getContractPageMetadata(serverContract);
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<TokenIdPage
contract={clientContract}
isErc721={supportedERCs.isERC721}
<SharedNFTTokenPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
tokenId={params.tokenId}
projectMeta={undefined}
isLoggedIn={!!account}
accountAddress={accountAddress || undefined}
/>
);
}
function isOnlyNumbers(str: string) {
return /^\d+$/.test(str);
}
@@ -0,0 +1,80 @@
import { notFound, redirect } from "next/navigation";
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { buildContractPagePath } from "../../_utils/contract-page-path";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { TokenIdPageClient } from "./TokenIdPage.client";
import { TokenIdPage } from "./token-id";
export async function SharedNFTTokenPage(props: {
contractAddress: string;
chainIdOrSlug: string;
tokenId: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const [info, accountAddress] = await Promise.all([
getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
}),
getAuthTokenWalletAddress(),
]);
if (!info) {
notFound();
}
// redirect to nfts index page
if (!isOnlyNumbers(props.tokenId)) {
redirect(
buildContractPagePath({
projectMeta: props.projectMeta,
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
subpath: "/nfts",
}),
);
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<TokenIdPageClient
contract={clientContract}
tokenId={props.tokenId}
isLoggedIn={props.isLoggedIn}
accountAddress={accountAddress || undefined}
projectMeta={props.projectMeta}
/>
);
}
const { supportedERCs } = await getContractPageMetadata(serverContract);
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
redirectToContractLandingPage({
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
projectMeta: props.projectMeta,
});
}
return (
<TokenIdPage
contract={clientContract}
isErc721={supportedERCs.isERC721}
tokenId={props.tokenId}
isLoggedIn={props.isLoggedIn}
accountAddress={accountAddress || undefined}
projectMeta={props.projectMeta}
/>
);
}
function isOnlyNumbers(str: string) {
return /^\d+$/.test(str);
}
@@ -17,7 +17,6 @@ import { CodeClient } from "@/components/ui/code/code.client";
import { TabButtons } from "@/components/ui/tabs";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import { useChainSlug } from "hooks/chains/chainSlug";
import { ExternalLinkIcon } from "lucide-react";
@@ -28,6 +27,8 @@ import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
import { useReadContract } from "thirdweb/react";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../../_utils/contract-page-path";
import { NftProperty } from "../components/nft-property";
import { useNFTDrawerTabs } from "./useNftDrawerTabs";
@@ -53,6 +54,7 @@ interface TokenIdPageProps {
isErc721: boolean;
isLoggedIn: boolean;
accountAddress: string | undefined;
projectMeta: ProjectMeta | undefined;
}
// TODO: verify the entire nft object with zod schema and display an error message
@@ -63,9 +65,9 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
isErc721,
isLoggedIn,
accountAddress,
projectMeta,
}) => {
const [tab, setTab] = useState("Details");
const router = useDashboardRouter();
const chainId = contract.chain.id;
const chainSlug = useChainSlug(chainId || 1);
@@ -113,6 +115,13 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
: undefined,
};
const nftsPagePath = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug.toString(),
contractAddress: contract.address,
subpath: "/nfts",
});
return (
<div>
<div className="flex flex-col gap-6 lg:flex-row">
@@ -129,15 +138,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
<Breadcrumb className="mb-3">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
href={`/${chainSlug}/${contract.address}/nfts`}
onClick={(e) => {
e.preventDefault();
router.push(`/${chainSlug}/${contract.address}/nfts`);
}}
>
NFTs
</BreadcrumbLink>
<BreadcrumbLink href={nftsPagePath}>NFTs</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
@@ -41,16 +41,20 @@ import type { NFT, ThirdwebContract } from "thirdweb";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import { useReadContract } from "thirdweb/react";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../../_utils/contract-page-path";
interface ContractOverviewNFTGetAllProps {
contract: ThirdwebContract;
isErc721: boolean;
tokenByIndex: boolean;
projectMeta: ProjectMeta | undefined;
}
export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
contract,
isErc721,
tokenByIndex,
projectMeta,
}) => {
// if it's not erc721, it's erc1155
const isErc1155 = !isErc721;
@@ -297,12 +301,17 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
if (!tokenId && tokenId !== 0n) {
return;
}
router.push(
`/${chainSlug}/${contract.address}/nfts/${tokenId.toString()}`,
{
scroll: true,
},
);
const path = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug.toString(),
contractAddress: contract.address,
subpath: `/nfts/${tokenId.toString()}`,
});
router.push(path, {
scroll: true,
});
}}
borderBottomWidth={1}
_last={{ borderBottomWidth: 0 }}
@@ -1,44 +1,17 @@
import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractNFTPage } from "./ContractNFTPage";
import { ContractNFTPageClient } from "./ContractNFTPage.client";
import type { PublicContractPageParams } from "../types";
import { SharedNFTPage } from "./shared-nfts-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const account = await getRawAccount();
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<ContractNFTPageClient contract={clientContract} isLoggedIn={!!account} />
);
}
const { supportedERCs, functionSelectors } =
await getContractPageMetadata(serverContract);
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<ContractNFTPage
contract={clientContract}
isErc721={supportedERCs.isERC721}
functionSelectors={functionSelectors}
<SharedNFTPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
/>
);
@@ -0,0 +1,56 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractNFTPage } from "./ContractNFTPage";
import { ContractNFTPageClient } from "./ContractNFTPage.client";
export async function SharedNFTPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<ContractNFTPageClient
contract={clientContract}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
const { supportedERCs, functionSelectors } =
await getContractPageMetadata(serverContract);
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
redirectToContractLandingPage({
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
projectMeta: props.projectMeta,
});
}
return (
<ContractNFTPage
contract={clientContract}
isErc721={supportedERCs.isERC721}
functionSelectors={functionSelectors}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
@@ -21,7 +21,11 @@ export default async function Image({
};
}) {
try {
const info = await getContractPageParamsInfo(params);
const info = await getContractPageParamsInfo({
contractAddress: params.contractAddress,
chainIdOrSlug: params.chain_id,
teamId: undefined,
});
if (!info) {
throw new Error();
@@ -1,4 +1,5 @@
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { TokenDetailsCard } from "../tokens/components/supply";
import { ContractAnalyticsOverviewCard } from "./components/Analytics";
import { BuildYourApp } from "./components/BuildYourApp";
@@ -20,6 +21,7 @@ interface ContractOverviewPageProps {
isAnalyticsSupported: boolean;
functionSelectors: string[];
publishedBy: React.ReactNode;
projectMeta: ProjectMeta | undefined;
}
const TRACKING_CATEGORY = "contract_overview";
@@ -36,6 +38,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
isAnalyticsSupported,
functionSelectors,
publishedBy,
projectMeta,
}) => {
return (
<div className="flex flex-col gap-10 lg:flex-row lg:gap-8">
@@ -47,6 +50,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
contract={contract}
chainSlug={chainSlug}
functionSelectors={functionSelectors}
projectMeta={projectMeta}
/>
{isAnalyticsSupported && (
@@ -55,12 +59,14 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
chainId={contract.chain.id}
trackingCategory={TRACKING_CATEGORY}
chainSlug={chainSlug}
projectMeta={projectMeta}
/>
)}
{(hasEnglishAuctions || hasDirectListings) && (
<MarketplaceDetails
contract={contract}
projectMeta={projectMeta}
trackingCategory={TRACKING_CATEGORY}
hasEnglishAuctions={hasEnglishAuctions}
hasDirectListings={hasDirectListings}
@@ -74,6 +80,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
trackingCategory={TRACKING_CATEGORY}
isErc721={isErc721}
chainSlug={chainSlug}
projectMeta={projectMeta}
/>
)}
@@ -83,6 +90,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
contract={contract}
trackingCategory={TRACKING_CATEGORY}
chainSlug={chainSlug}
projectMeta={projectMeta}
/>
{isPermissionsEnumerable && (
@@ -90,6 +98,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
contract={contract}
trackingCategory={TRACKING_CATEGORY}
chainSlug={chainSlug}
projectMeta={projectMeta}
/>
)}
@@ -97,6 +106,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
trackingCategory={TRACKING_CATEGORY}
chainSlug={chainSlug}
contractAddress={contract.address}
projectMeta={projectMeta}
/>
</div>
<div className="shrink-0 lg:w-[300px]">{publishedBy}</div>
@@ -11,6 +11,8 @@ import { useTrack } from "hooks/analytics/useTrack";
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../../_utils/contract-page-path";
function getDayKey(date: Date) {
return date.toISOString().split("T")[0];
@@ -21,6 +23,7 @@ export function ContractAnalyticsOverviewCard(props: {
chainId: number;
trackingCategory: string;
chainSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const trackEvent = useTrack();
const [startDate] = useState(
@@ -85,6 +88,13 @@ export function ContractAnalyticsOverviewCard(props: {
});
}, [wallets.data, transactions.data, events.data, isPending]);
const analyticsPath = buildContractPagePath({
projectMeta: props.projectMeta,
chainIdOrSlug: props.chainSlug,
contractAddress: props.contractAddress,
subpath: "/analytics",
});
return (
<ThirdwebAreaChart
config={{
@@ -121,9 +131,7 @@ export function ContractAnalyticsOverviewCard(props: {
});
}}
>
<Link
href={`/${props.chainSlug}/${props.contractAddress}/analytics`}
>
<Link href={analyticsPath}>
<span>View All</span>
<ArrowRightIcon className="size-4" />
</Link>
@@ -1,12 +1,22 @@
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../../_utils/contract-page-path";
export function BuildYourApp(props: {
trackingCategory: string;
contractAddress: string;
chainSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const codePath = buildContractPagePath({
projectMeta: props.projectMeta,
chainIdOrSlug: props.chainSlug,
contractAddress: props.contractAddress,
subpath: "/code",
});
return (
<div className="relative flex flex-col justify-between gap-4 rounded-lg border bg-card p-6 hover:border-active-border lg:flex-row">
{/* left */}
@@ -17,7 +27,7 @@ export function BuildYourApp(props: {
<TrackedLinkTW
category={props.trackingCategory}
label="build_your_app"
href={`/${props.chainSlug}/${props.contractAddress}/code`}
href={codePath}
className="block text-balance text-muted-foreground text-sm before:absolute before:inset-0"
>
Learn more about how you can use thirdweb tools to build apps on top
@@ -12,6 +12,8 @@ import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import * as ERC4337Ext from "thirdweb/extensions/erc4337";
import { getAccounts } from "thirdweb/extensions/erc4337";
import { useReadContract } from "thirdweb/react";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../../_utils/contract-page-path";
interface ContractChecklistProps {
contract: ThirdwebContract;
@@ -20,6 +22,7 @@ interface ContractChecklistProps {
isErc20: boolean;
chainSlug: string;
functionSelectors: string[];
projectMeta: ProjectMeta | undefined;
}
type Step = {
@@ -44,13 +47,20 @@ function Inner({
isErc721,
functionSelectors,
chainSlug,
projectMeta,
}: ContractChecklistProps & {
functionSelectors: string[];
}) {
const nftHref = `/${chainSlug}/${contract.address}/nfts`;
const tokenHref = `/${chainSlug}/${contract.address}/tokens`;
const accountsHref = `/${chainSlug}/${contract.address}/accounts`;
const claimConditionsHref = `/${chainSlug}/${contract.address}/claim-conditions`;
const contractLayout = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug,
contractAddress: contract.address,
});
const nftHref = `${contractLayout}/nfts`;
const tokenHref = `${contractLayout}/tokens`;
const accountsHref = `${contractLayout}/accounts`;
const claimConditionsHref = `${contractLayout}/claim-conditions`;
const erc721Claimed = useReadContract(ERC721Ext.getTotalClaimedSupply, {
contract: contract,
@@ -22,20 +22,30 @@ import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import type { ThirdwebContract } from "thirdweb";
import { shortenString } from "utils/usedapp-external";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../../_utils/contract-page-path";
export function LatestEvents(props: {
trackingCategory: string;
contract: ThirdwebContract;
chainSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const autoUpdate = true;
const allEvents = useActivity(props.contract, autoUpdate);
const eventsHref = buildContractPagePath({
projectMeta: props.projectMeta,
chainIdOrSlug: props.chainSlug,
contractAddress: props.contract.address,
subpath: "/events",
});
return (
<LatestEventsUI
allEvents={allEvents}
autoUpdate={autoUpdate}
eventsHref={`/${props.chainSlug}/${props.contract.address}/events`}
eventsHref={eventsHref}
trackingCategory={props.trackingCategory}
/>
);
@@ -19,6 +19,8 @@ import {
import { useReadContract } from "thirdweb/react";
import { min } from "thirdweb/utils";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../../_utils/contract-page-path";
import { ListingStatsV3 } from "./listing-stats";
type ListingData =
@@ -41,14 +43,22 @@ type ListingCardsSectionProps = {
contract: ThirdwebContract;
trackingCategory: string;
chainSlug: string;
projectMeta: ProjectMeta | undefined;
};
const DirectListingCards: React.FC<ListingCardsSectionProps> = ({
trackingCategory,
contract,
chainSlug,
projectMeta,
}) => {
const directListingsHref = `/${chainSlug}/${contract.address}/direct-listings`;
const directListingsHref = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug,
contractAddress: contract.address,
subpath: "/direct-listings",
});
const countQuery = useReadContract(totalListings, { contract });
const listingsQuery = useReadContract(getAllListings, {
contract,
@@ -109,6 +119,7 @@ const DirectListingCards: React.FC<ListingCardsSectionProps> = ({
trackingCategory={trackingCategory}
chainSlug={chainSlug}
contractAddress={contract.address}
projectMeta={projectMeta}
/>
</div>
</div>
@@ -119,8 +130,15 @@ const EnglishAuctionCards: React.FC<ListingCardsSectionProps> = ({
trackingCategory,
contract,
chainSlug,
projectMeta,
}) => {
const englishAuctionsHref = `/${chainSlug}/${contract.address}/english-auctions`;
const englishAuctionsHref = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug,
contractAddress: contract.address,
subpath: "/english-auctions",
});
const countQuery = useReadContract(totalAuctions, { contract });
const auctionsQuery = useReadContract(getAllAuctions, {
contract,
@@ -180,6 +198,7 @@ const EnglishAuctionCards: React.FC<ListingCardsSectionProps> = ({
trackingCategory={trackingCategory}
chainSlug={chainSlug}
contractAddress={contract.address}
projectMeta={projectMeta}
/>
</div>
</div>
@@ -192,6 +211,7 @@ interface MarketplaceDetailsVersionProps {
hasEnglishAuctions: boolean;
hasDirectListings: boolean;
chainSlug: string;
projectMeta: ProjectMeta | undefined;
}
export const MarketplaceDetails: React.FC<MarketplaceDetailsVersionProps> = ({
@@ -200,6 +220,7 @@ export const MarketplaceDetails: React.FC<MarketplaceDetailsVersionProps> = ({
hasDirectListings,
hasEnglishAuctions,
chainSlug,
projectMeta,
}) => {
return (
<div className="flex flex-col gap-10">
@@ -214,6 +235,7 @@ export const MarketplaceDetails: React.FC<MarketplaceDetailsVersionProps> = ({
contract={contract}
trackingCategory={trackingCategory}
chainSlug={chainSlug}
projectMeta={projectMeta}
/>
)}
@@ -222,6 +244,7 @@ export const MarketplaceDetails: React.FC<MarketplaceDetailsVersionProps> = ({
contract={contract}
trackingCategory={trackingCategory}
chainSlug={chainSlug}
projectMeta={projectMeta}
/>
)}
</div>
@@ -269,6 +292,7 @@ interface ListingCardsProps {
isMarketplaceV1?: boolean;
chainSlug: string;
contractAddress: string;
projectMeta: ProjectMeta | undefined;
}
const ListingCards: React.FC<ListingCardsProps> = ({
listings,
@@ -277,13 +301,20 @@ const ListingCards: React.FC<ListingCardsProps> = ({
trackingCategory,
chainSlug,
contractAddress,
projectMeta,
}) => {
const contractLayout = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug,
contractAddress,
});
listings = isPending
? Array.from({ length: 3 }).map((_, idx) => dummyMetadata(idx))
: listings.slice(0, 3);
const directListingsHref = `/${chainSlug}/${contractAddress}/direct-listings`;
const englishAuctionsHref = `/${chainSlug}/${contractAddress}/english-auctions`;
const directListingsHref = `${contractLayout}/direct-listings`;
const englishAuctionsHref = `${contractLayout}/english-auctions`;
return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 ">
@@ -7,13 +7,16 @@ import type { ThirdwebContract } from "thirdweb";
import * as ERC721 from "thirdweb/extensions/erc721";
import * as ERC1155 from "thirdweb/extensions/erc1155";
import { useReadContract } from "thirdweb/react";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { NFTCards } from "../../_components/NFTCards";
import { buildContractPagePath } from "../../_utils/contract-page-path";
type NFTDetailsProps = {
contract: ThirdwebContract;
trackingCategory: string;
isErc721: boolean;
chainSlug: string;
projectMeta: ProjectMeta | undefined;
};
export function NFTDetails({
@@ -21,8 +24,14 @@ export function NFTDetails({
trackingCategory,
isErc721,
chainSlug,
projectMeta,
}: NFTDetailsProps) {
const nftsHref = `/${chainSlug}/${contract.address}/nfts`;
const nftsHref = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug,
contractAddress: contract.address,
subpath: "/nfts",
});
const nftQuery = useReadContract(
isErc721 ? ERC721.getNFTs : ERC1155.getNFTs,
@@ -58,6 +67,7 @@ export function NFTDetails({
{/* cards */}
<div className="p-6">
<NFTCards
projectMeta={projectMeta}
nfts={displayableNFTs.map((t) => ({
...t,
contractAddress: contract.address,
@@ -19,12 +19,22 @@ import { ArrowRightIcon } from "lucide-react";
import { useMemo } from "react";
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
import { useReadContract } from "thirdweb/react";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../../_utils/contract-page-path";
export function PermissionsTable(props: {
contract: ThirdwebContract;
trackingCategory: string;
chainSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const permissionsHref = buildContractPagePath({
projectMeta: props.projectMeta,
chainIdOrSlug: props.chainSlug,
contractAddress: props.contract.address,
subpath: "/permissions",
});
const allRoleMembers = useReadContract(getAllRoleMembers, {
contract: props.contract,
});
@@ -53,7 +63,7 @@ export function PermissionsTable(props: {
<PermissionsTableUI
members={members}
isPending={allRoleMembers.isPending}
viewMoreLink={`/${props.chainSlug}/${props.contract.address}/permissions`}
viewMoreLink={permissionsHref}
trackingCategory={props.trackingCategory}
/>
);
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractOverviewPage } from "./ContractOverviewPage";
@@ -8,8 +9,9 @@ import { ContractOverviewPage } from "./ContractOverviewPage";
export function ContractOverviewPageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
projectMeta: ProjectMeta | undefined;
}) {
const { contract, chainMetadata } = props;
const { contract, chainMetadata, projectMeta } = props;
const metadataQuery = useContractPageMetadata(contract);
if (metadataQuery.isPending) {
@@ -25,6 +27,7 @@ export function ContractOverviewPageClient(props: {
return (
<ContractOverviewPage
contract={contract}
projectMeta={projectMeta}
hasDirectListings={contractPageMetadata.isDirectListingSupported}
hasEnglishAuctions={contractPageMetadata.isEnglishAuctionSupported}
isErc1155={contractPageMetadata.supportedERCs.isERC1155}
@@ -1,56 +1,15 @@
import { notFound } from "next/navigation";
import { ErrorBoundary } from "react-error-boundary";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
import { ContractOverviewPage } from "./overview/ContractOverviewPage";
import { PublishedBy } from "./overview/components/published-by.server";
import { ContractOverviewPageClient } from "./overview/contract-overview-page.client";
import { SharedContractOverviewPage } from "./shared-overview-page";
import type { PublicContractPageParams } from "./types";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
info;
if (isLocalhostChain) {
return (
<ContractOverviewPageClient
chainMetadata={chainMetadata}
contract={clientContract}
/>
);
}
const contractPageMetadata = await getContractPageMetadata(serverContract);
return (
<ContractOverviewPage
contract={clientContract}
hasDirectListings={contractPageMetadata.isDirectListingSupported}
hasEnglishAuctions={contractPageMetadata.isEnglishAuctionSupported}
isErc1155={contractPageMetadata.supportedERCs.isERC1155}
isErc20={contractPageMetadata.supportedERCs.isERC20}
isErc721={contractPageMetadata.supportedERCs.isERC721}
isPermissionsEnumerable={
contractPageMetadata.isPermissionsEnumerableSupported
}
chainSlug={chainMetadata.slug}
isAnalyticsSupported={contractPageMetadata.isInsightSupported}
functionSelectors={contractPageMetadata.functionSelectors}
publishedBy={
<ErrorBoundary fallback={null}>
<PublishedBy contract={serverContract} />
</ErrorBoundary>
}
<SharedContractOverviewPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
/>
);
}
@@ -2,6 +2,7 @@
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractPermissionsPage } from "./ContractPermissionsPage";
@@ -10,6 +11,7 @@ export function ContractPermissionsPageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -23,6 +25,7 @@ export function ContractPermissionsPageClient(props: {
return (
<ContractPermissionsPage
projectMeta={props.projectMeta}
contract={props.contract}
chainSlug={props.chainMetadata.slug}
isLoggedIn={props.isLoggedIn}
@@ -1,10 +1,11 @@
"use client";
import { InlineCode } from "@/components/ui/inline-code";
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
import { ButtonGroup, Divider, Flex } from "@chakra-ui/react";
import type { ThirdwebContract } from "thirdweb";
import { Card, Heading, Link, LinkButton, Text } from "tw-components";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { buildContractPagePath } from "../_utils/contract-page-path";
import { Permissions } from "./components";
interface ContractPermissionsPageProps {
@@ -12,16 +13,24 @@ interface ContractPermissionsPageProps {
detectedPermissionEnumerable: boolean;
chainSlug: string;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}
export const ContractPermissionsPage: React.FC<
ContractPermissionsPageProps
> = ({ contract, detectedPermissionEnumerable, chainSlug, isLoggedIn }) => {
useIsomorphicLayoutEffect(() => {
window?.scrollTo({ top: 0, behavior: "smooth" });
}, []);
const explorerHref = `/${chainSlug}/${contract.address}/explorer`;
> = ({
contract,
detectedPermissionEnumerable,
chainSlug,
isLoggedIn,
projectMeta,
}) => {
const explorerHref = buildContractPagePath({
projectMeta,
chainIdOrSlug: chainSlug,
contractAddress: contract.address,
subpath: "/explorer",
});
if (!detectedPermissionEnumerable) {
return (
@@ -1,44 +1,16 @@
import { notFound } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractPermissionsPage } from "./ContractPermissionsPage";
import { ContractPermissionsPageClient } from "./ContractPermissionsPage.client";
import type { PublicContractPageParams } from "../types";
import { SharedPermissionsPage } from "./shared-permissions-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const account = await getRawAccount();
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<ContractPermissionsPageClient
contract={clientContract}
chainMetadata={info.chainMetadata}
isLoggedIn={!!account}
/>
);
}
const { isPermissionsEnumerableSupported } =
await getContractPageMetadata(serverContract);
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<ContractPermissionsPage
contract={clientContract}
chainSlug={info.chainMetadata.slug}
detectedPermissionEnumerable={isPermissionsEnumerableSupported}
<SharedPermissionsPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
/>
);
@@ -0,0 +1,48 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractPermissionsPage } from "./ContractPermissionsPage";
import { ContractPermissionsPageClient } from "./ContractPermissionsPage.client";
export async function SharedPermissionsPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<ContractPermissionsPageClient
contract={clientContract}
chainMetadata={info.chainMetadata}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
const { isPermissionsEnumerableSupported } =
await getContractPageMetadata(serverContract);
return (
<ContractPermissionsPage
projectMeta={props.projectMeta}
contract={clientContract}
chainSlug={info.chainMetadata.slug}
detectedPermissionEnumerable={isPermissionsEnumerableSupported}
isLoggedIn={props.isLoggedIn}
/>
);
}
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
@@ -9,6 +10,7 @@ import { ContractProposalsPage } from "./ContractProposalsPage";
export function ContractProposalsPageClient(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -21,7 +23,12 @@ export function ContractProposalsPageClient(props: {
}
if (!metadataQuery.data.isVoteContract) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return (
@@ -1,41 +1,18 @@
import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractProposalsPage } from "./ContractProposalsPage";
import { ContractProposalsPageClient } from "./ContractProposalsPage.client";
import type { PublicContractPageParams } from "../types";
import { SharedContractProposalsPage } from "./shared-proposals-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const account = await getRawAccount();
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<ContractProposalsPageClient
contract={clientContract}
isLoggedIn={!!account}
/>
);
}
const { isVoteContract } = await getContractPageMetadata(serverContract);
if (!isVoteContract) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<ContractProposalsPage contract={clientContract} isLoggedIn={!!account} />
<SharedContractProposalsPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
/>
);
}
@@ -0,0 +1,52 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractProposalsPage } from "./ContractProposalsPage";
import { ContractProposalsPageClient } from "./ContractProposalsPage.client";
export async function SharedContractProposalsPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<ContractProposalsPageClient
contract={clientContract}
isLoggedIn={props.isLoggedIn}
projectMeta={props.projectMeta}
/>
);
}
const { isVoteContract } = await getContractPageMetadata(serverContract);
if (!isVoteContract) {
redirectToContractLandingPage({
projectMeta: props.projectMeta,
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
});
}
return (
<ContractProposalsPage
contract={clientContract}
isLoggedIn={props.isLoggedIn}
/>
);
}
@@ -1,55 +1,18 @@
import { DEFAULT_FEE_RECIPIENT } from "constants/addresses";
import { notFound } from "next/navigation";
import { getPlatformFeeInfo } from "thirdweb/extensions/common";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractSettingsPage } from "./ContractSettingsPage";
import { ContractSettingsPageClient } from "./ContractSettingsPage.client";
import type { PublicContractPageParams } from "../types";
import { SharedContractSettingsPage } from "./shared-settings-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
const account = await getRawAccount();
return (
<ContractSettingsPageClient
contract={clientContract}
isLoggedIn={!!account}
/>
);
}
const [account, metadata] = await Promise.all([
getRawAccount(),
getContractPageMetadata(serverContract),
]);
let hasDefaultFeeConfig = true;
try {
const feeInfo = await getPlatformFeeInfo({ contract: serverContract });
hasDefaultFeeConfig =
feeInfo[0].toLowerCase() === DEFAULT_FEE_RECIPIENT.toLowerCase();
} catch {}
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<ContractSettingsPage
contract={clientContract}
functionSelectors={metadata.functionSelectors}
<SharedContractSettingsPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
hasDefaultFeeConfig={hasDefaultFeeConfig}
/>
);
}
@@ -0,0 +1,62 @@
import { DEFAULT_FEE_RECIPIENT } from "constants/addresses";
import { notFound } from "next/navigation";
import type { ThirdwebContract } from "thirdweb";
import { getPlatformFeeInfo } from "thirdweb/extensions/common";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractSettingsPage } from "./ContractSettingsPage";
import { ContractSettingsPageClient } from "./ContractSettingsPage.client";
export async function SharedContractSettingsPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<ContractSettingsPageClient
contract={clientContract}
isLoggedIn={props.isLoggedIn}
/>
);
}
const [metadata, hasDefaultFeeConfig] = await Promise.all([
getContractPageMetadata(serverContract),
checkDefaultFeeConfig(serverContract),
]);
return (
<ContractSettingsPage
contract={clientContract}
functionSelectors={metadata.functionSelectors}
isLoggedIn={props.isLoggedIn}
hasDefaultFeeConfig={hasDefaultFeeConfig}
/>
);
}
async function checkDefaultFeeConfig(contract: ThirdwebContract) {
let hasDefaultFeeConfig = true;
try {
const feeInfo = await getPlatformFeeInfo({ contract });
hasDefaultFeeConfig =
feeInfo[0].toLowerCase() === DEFAULT_FEE_RECIPIENT.toLowerCase();
} catch {}
return hasDefaultFeeConfig;
}
@@ -0,0 +1,218 @@
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
import { resolveFunctionSelectors } from "lib/selectors";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getContractMetadata } from "thirdweb/extensions/common";
import { isAddress, isContractDeployed } from "thirdweb/utils";
import { shortenIfAddress } from "utils/usedapp-external";
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
import { examplePrompts } from "../../../../../nebula-app/(app)/data/examplePrompts";
import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
import type { ProjectMeta } from "../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
import { ContractPageLayout } from "./_layout/contract-page-layout";
import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
export async function SharedContractLayout(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
children: React.ReactNode;
}) {
if (!isAddress(props.contractAddress)) {
return notFound();
}
const [info, accountAddress, teamsAndProjects] = await Promise.all([
getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
}),
getAuthTokenWalletAddress(),
getTeamsAndProjectsIfLoggedIn(),
]);
if (!info) {
return <ConfigureCustomChain chainSlug={props.chainIdOrSlug} />;
}
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
info;
if (chainMetadata.status === "deprecated") {
notFound();
}
if (isLocalhostChain) {
return (
<ContractPageLayoutClient
chainMetadata={chainMetadata}
contract={clientContract}
teamsAndProjects={teamsAndProjects}
projectMeta={props.projectMeta}
>
{props.children}
</ContractPageLayoutClient>
);
}
const [
isValidContract,
contractPageMetadata,
{ contractMetadata, externalLinks },
] = await Promise.all([
isContractDeployed(serverContract).catch(() => false),
getContractPageMetadata(serverContract),
getContractMetadataHeaderData(serverContract),
]);
// check if the contract exists
if (!isValidContract) {
// TODO - replace 404 with a better page to upsell deploy or other thirdweb products
notFound();
}
const sidebarLinks = getContractPageSidebarLinks({
chainSlug: chainMetadata.slug,
contractAddress: serverContract.address,
metadata: contractPageMetadata,
projectMeta: props.projectMeta,
});
const contractAddress = serverContract.address;
const chainName = chainMetadata.name;
const chainId = chainMetadata.chainId;
const contractPromptPrefix = `A user is viewing the contract address ${contractAddress} on ${chainName} (Chain ID: ${chainId}). Provide a concise summary of this contract's functionalities, such as token minting, staking, or governance mechanisms. Focus on what the contract enables users to do, avoiding transaction execution details unless requested.
Users may be interested in how to interact with the contract. Outline common interaction patterns, such as claiming rewards, participating in governance, or transferring assets. Emphasize the contract's capabilities without guiding through transaction processes unless asked.
Provide insights into how the contract is being used. Share information on user engagement, transaction volumes, or integration with other dApps, focusing on the contract's role within the broader ecosystem.
Users may be considering integrating the contract into their applications. Discuss how this contract's functionalities can be leveraged within different types of dApps, highlighting potential use cases and benefits.
The following is the user's message:`;
return (
<ContractPageLayout
chainMetadata={chainMetadata}
contract={clientContract}
sidebarLinks={sidebarLinks}
dashboardContractMetadata={contractMetadata}
externalLinks={externalLinks}
teamsAndProjects={teamsAndProjects}
projectMeta={props.projectMeta}
>
<NebulaChatButton
isLoggedIn={!!accountAddress}
networks={info.chainMetadata.testnet ? "testnet" : "mainnet"}
isFloating={true}
pageType="contract"
label="Ask AI about this contract"
client={clientContract.client}
nebulaParams={{
messagePrefix: contractPromptPrefix,
chainIds: [chainId],
wallet: accountAddress ?? undefined,
}}
examplePrompts={examplePrompts}
/>
{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,
image: x.image,
})),
})),
);
return teamsAndProjects;
} catch {
return undefined;
}
}
export async function generateContractLayoutMetadata(params: {
contractAddress: string;
chainIdOrSlug: string;
}): Promise<Metadata> {
try {
const info = await getContractPageParamsInfo({
contractAddress: params.contractAddress,
chainIdOrSlug: params.chainIdOrSlug,
teamId: undefined,
});
if (!info) {
throw new Error();
}
const [functionSelectors, contractMetadata] = await Promise.all([
resolveFunctionSelectors(info.serverContract),
getContractMetadata({
contract: info.serverContract,
}),
]);
if (contractMetadata.name === null) {
throw new Error();
}
const { isERC1155, isERC20, isERC721 } = supportedERCs(functionSelectors);
const contractDisplayName = `${contractMetadata.name}${
contractMetadata.symbol ? ` (${contractMetadata.symbol})` : ""
}`;
const cleanedChainName = info?.chainMetadata?.name
.replace("Mainnet", "")
.replace("Testnet", "")
.trim();
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
let description = "";
if (isERC721 || isERC1155) {
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
} else if (isERC20) {
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
} else {
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
}
return {
title: title,
description: description,
};
} catch {
return {
title: `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`,
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`,
};
}
}
@@ -0,0 +1,61 @@
import { notFound } from "next/navigation";
import { ErrorBoundary } from "react-error-boundary";
import type { ProjectMeta } from "../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
import { ContractOverviewPage } from "./overview/ContractOverviewPage";
import { PublishedBy } from "./overview/components/published-by.server";
import { ContractOverviewPageClient } from "./overview/contract-overview-page.client";
export async function SharedContractOverviewPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
info;
if (isLocalhostChain) {
return (
<ContractOverviewPageClient
projectMeta={props.projectMeta}
chainMetadata={chainMetadata}
contract={clientContract}
/>
);
}
const contractPageMetadata = await getContractPageMetadata(serverContract);
return (
<ContractOverviewPage
contract={clientContract}
projectMeta={props.projectMeta}
hasDirectListings={contractPageMetadata.isDirectListingSupported}
hasEnglishAuctions={contractPageMetadata.isEnglishAuctionSupported}
isErc1155={contractPageMetadata.supportedERCs.isERC1155}
isErc20={contractPageMetadata.supportedERCs.isERC20}
isErc721={contractPageMetadata.supportedERCs.isERC721}
isPermissionsEnumerable={
contractPageMetadata.isPermissionsEnumerableSupported
}
chainSlug={chainMetadata.slug}
isAnalyticsSupported={contractPageMetadata.isInsightSupported}
functionSelectors={contractPageMetadata.functionSelectors}
publishedBy={
<ErrorBoundary fallback={null}>
<PublishedBy contract={serverContract} />
</ErrorBoundary>
}
/>
);
}
@@ -1,19 +1,15 @@
import { notFound } from "next/navigation";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { ContractSourcesPage } from "./ContractSourcesPage";
import type { PublicContractPageParams } from "../types";
import { SharedContractSourcesPage } from "./shared-sources-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
return <ContractSourcesPage contract={info.clientContract} />;
return (
<SharedContractSourcesPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
/>
);
}
@@ -0,0 +1,22 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { ContractSourcesPage } from "./ContractSourcesPage";
export async function SharedContractSourcesPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
return <ContractSourcesPage contract={info.clientContract} />;
}
@@ -1,6 +1,7 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
@@ -9,6 +10,7 @@ import { ContractSplitPage } from "./ContractSplitPage";
export function ContractSplitPageClient(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
projectMeta: ProjectMeta | undefined;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
@@ -21,7 +23,12 @@ export function ContractSplitPageClient(props: {
}
if (!metadataQuery.data.isSplitSupported) {
return <RedirectToContractOverview contract={props.contract} />;
return (
<RedirectToContractOverview
contract={props.contract}
projectMeta={props.projectMeta}
/>
);
}
return (
@@ -1,42 +1,18 @@
import { notFound, redirect } from "next/navigation";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractSplitPage } from "./ContractSplitPage";
import { ContractSplitPageClient } from "./ContractSplitPage.client";
import type { PublicContractPageParams } from "../types";
import { SharedContractSplitPage } from "./shared-split-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
const twAccount = await getRawAccount();
if (isLocalhostChain) {
return (
<ContractSplitPageClient
contract={clientContract}
isLoggedIn={!!twAccount}
/>
);
}
const { isSplitSupported } = await getContractPageMetadata(serverContract);
if (!isSplitSupported) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<ContractSplitPage contract={clientContract} isLoggedIn={!!twAccount} />
<SharedContractSplitPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
/>
);
}
@@ -0,0 +1,52 @@
import { notFound } from "next/navigation";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractSplitPage } from "./ContractSplitPage";
import { ContractSplitPageClient } from "./ContractSplitPage.client";
export async function SharedContractSplitPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
const { clientContract, serverContract, isLocalhostChain } = info;
if (isLocalhostChain) {
return (
<ContractSplitPageClient
projectMeta={props.projectMeta}
contract={clientContract}
isLoggedIn={props.isLoggedIn}
/>
);
}
const { isSplitSupported } = await getContractPageMetadata(serverContract);
if (!isSplitSupported) {
redirectToContractLandingPage({
chainIdOrSlug: props.chainIdOrSlug,
contractAddress: props.contractAddress,
projectMeta: props.projectMeta,
});
}
return (
<ContractSplitPage
contract={clientContract}
isLoggedIn={props.isLoggedIn}
/>
);
}
@@ -1,48 +1,17 @@
import { notFound } from "next/navigation";
import {
isClaimToSupported,
isMintToSupported,
} from "thirdweb/extensions/erc20";
import { getRawAccount } from "../../../../../account/settings/getAccount";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractTokensPage } from "./ContractTokensPage";
import { ContractTokensPageClient } from "./ContractTokensPage.client";
import type { PublicContractPageParams } from "../types";
import { SharedContractTokensPage } from "./shared-page";
export default async function Page(props: {
params: Promise<{
contractAddress: string;
chain_id: string;
}>;
params: Promise<PublicContractPageParams>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
const account = await getRawAccount();
if (info.isLocalhostChain) {
return (
<ContractTokensPageClient
contract={info.clientContract}
isLoggedIn={!!account}
/>
);
}
const { supportedERCs, functionSelectors } = await getContractPageMetadata(
info.serverContract,
);
const [params, account] = await Promise.all([props.params, getRawAccount()]);
return (
<ContractTokensPage
contract={info.clientContract}
isERC20={supportedERCs.isERC20}
isMintToSupported={isMintToSupported(functionSelectors)}
isClaimToSupported={isClaimToSupported(functionSelectors)}
<SharedContractTokensPage
contractAddress={params.contractAddress}
chainIdOrSlug={params.chain_id}
projectMeta={undefined}
isLoggedIn={!!account}
/>
);
@@ -0,0 +1,50 @@
import { notFound } from "next/navigation";
import {
isClaimToSupported,
isMintToSupported,
} from "thirdweb/extensions/erc20";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractTokensPage } from "./ContractTokensPage";
import { ContractTokensPageClient } from "./ContractTokensPage.client";
export async function SharedContractTokensPage(props: {
contractAddress: string;
chainIdOrSlug: string;
projectMeta: ProjectMeta | undefined;
isLoggedIn: boolean;
}) {
const info = await getContractPageParamsInfo({
contractAddress: props.contractAddress,
chainIdOrSlug: props.chainIdOrSlug,
teamId: props.projectMeta?.teamId,
});
if (!info) {
notFound();
}
if (info.isLocalhostChain) {
return (
<ContractTokensPageClient
contract={info.clientContract}
isLoggedIn={props.isLoggedIn}
/>
);
}
const { supportedERCs, functionSelectors } = await getContractPageMetadata(
info.serverContract,
);
return (
<ContractTokensPage
contract={info.clientContract}
isERC20={supportedERCs.isERC20}
isMintToSupported={isMintToSupported(functionSelectors)}
isClaimToSupported={isClaimToSupported(functionSelectors)}
isLoggedIn={props.isLoggedIn}
/>
);
}
@@ -0,0 +1,4 @@
export type PublicContractPageParams = {
contractAddress: string;
chain_id: string;
};
@@ -12,6 +12,8 @@ export function DeployedContractsPage(props: {
projectId: string;
authToken: string;
client: ThirdwebClient;
teamSlug: string;
projectSlug: string;
}) {
return (
<div className="flex grow flex-col">
@@ -39,6 +41,8 @@ async function DeployedContractsPageAsync(props: {
projectId: string;
authToken: string;
client: ThirdwebClient;
teamSlug: string;
projectSlug: string;
}) {
const deployedContracts = await getSortedDeployedContracts({
teamId: props.teamId,
@@ -56,6 +60,8 @@ async function DeployedContractsPageAsync(props: {
teamId={props.teamId}
projectId={props.projectId}
client={props.client}
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
/>
</ClientOnly>
);
@@ -29,8 +29,18 @@ export async function getAuthTokenWalletAddress() {
return null;
}
export async function getUserThirdwebClient() {
export async function getUserThirdwebClient(params: {
teamId: string | undefined;
}) {
const authToken = await getAuthToken();
if (params.teamId) {
return getClientThirdwebClient({
jwt: authToken,
teamId: params.teamId,
});
}
const cookiesManager = await cookies();
const lastUsedTeamId = cookiesManager.get(LAST_USED_TEAM_ID)?.value;
@@ -65,6 +65,8 @@ export default async function Page(props: {
projectId={project.id}
authToken={authToken}
client={client}
teamSlug={params.team_slug}
projectSlug={params.project_slug}
/>
</Suspense>
</div>
@@ -92,6 +94,8 @@ async function AssetsPageAsync(props: {
projectId: string;
authToken: string;
client: ThirdwebClient;
teamSlug: string;
projectSlug: string;
}) {
const deployedContracts = await getSortedDeployedContracts({
teamId: props.teamId,
@@ -109,6 +113,8 @@ async function AssetsPageAsync(props: {
teamId={props.teamId}
projectId={props.projectId}
client={props.client}
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
/>
</ClientOnly>
);
@@ -94,6 +94,17 @@ export function ProjectSidebarLayout(props: {
icon: NebulaIcon,
tracking: tracking("nebula"),
},
// Commented until we solve the scrolling issue
// {
// href: `${layoutPath}/webhooks`,
// label: (
// <span className="flex items-center gap-2">
// Webhooks <Badge>New</Badge>
// </span>
// ),
// icon: BellIcon,
// tracking: tracking("webhooks"),
// },
]}
footerSidebarLinks={[
{
@@ -50,6 +50,8 @@ export default async function Page(props: {
<YourFactoriesSection
teamId={team.id}
projectId={project.id}
teamSlug={team.slug}
projectSlug={project.slug}
authToken={authToken}
/>
</div>
@@ -60,6 +62,8 @@ function YourFactoriesSection(props: {
teamId: string;
projectId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
return (
<div className="flex flex-col gap-4">
@@ -98,6 +102,8 @@ function YourFactoriesSection(props: {
teamId={props.teamId}
projectId={props.projectId}
authToken={props.authToken}
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
/>
</Suspense>
</div>
@@ -108,6 +114,8 @@ async function AsyncYourFactories(props: {
teamId: string;
projectId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
const deployedContracts = await getSortedDeployedContracts({
teamId: props.teamId,
@@ -140,6 +148,8 @@ async function AsyncYourFactories(props: {
<FactoryContracts
contracts={factories}
isPending={false}
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
isFetched={true}
/>
</ClientOnly>
@@ -2,6 +2,8 @@ import { getProject } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
import { getFees } from "@/api/universal-bridge/developer";
import { PayConfig } from "components/pay/PayConfig";
import { RouteDiscovery } from "components/pay/RouteDiscovery";
import { redirect } from "next/navigation";
export default async function Page(props: {
@@ -47,11 +49,17 @@ export default async function Page(props: {
}
return (
<PayConfig
project={project}
teamId={team.id}
teamSlug={team_slug}
fees={fees}
/>
<div className="flex flex-col p-5">
<PayConfig
project={project}
teamId={team.id}
teamSlug={team_slug}
fees={fees}
/>
<div className="flex pt-5">
<RouteDiscovery project={project} />
</div>
</div>
);
}
@@ -111,8 +111,8 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
<TableHead>Label</TableHead>
<TableHead>Url</TableHead>
<TableHead>Created</TableHead>
<TableHead>Version</TableHead>
<TableHead>Delete</TableHead>
<TableHead className="text-center">Version</TableHead>
<TableHead className="text-center">Delete</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -123,8 +123,10 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
<TableCell>
{formatDistanceToNow(webhook.createdAt, { addSuffix: true })}
</TableCell>
<TableCell>{webhook.version || "v1"}</TableCell>
<TableCell className="text-right">
<TableCell className="text-center">
{webhook.version || "1"}
</TableCell>
<TableCell className="text-center">
<DeleteWebhookButton
clientId={props.clientId}
teamId={props.teamId}
@@ -42,6 +42,8 @@ export default async function Page(props: {
projectId={project.id}
authToken={authToken}
client={client}
teamSlug={params.team_slug}
projectSlug={params.project_slug}
/>
<div className="h-20" />
<ContractsFooter />

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