Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0299034fc1 Version Packages (#7207)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-29 13:30:44 -07:00
Joaquim VergesandGitHub 861e623a1b [SDK] Engine search transactions and batch transaction support (#7190) 2025-05-29 13:19:26 -07:00
MananTank 6a335bfa77 Fix requests to thirdwebstorage-dev.com not set with secret-key (#7205)
<!--

## 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 `fetch.ts` file in the `thirdweb` package to include a new URL for the thirdweb storage service.

### Detailed summary
- Added `".thirdwebstorage-dev.com"` to the list of URLs.

> ✨ 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

- **Chores**
  - Updated the list of recognized thirdweb service domains to include ".thirdwebstorage-dev.com".

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 20:09:50 +00:00
AmineAfia cb994dcb8f Re-enable webhooks sidebar link in project dashboard (#7196)
## [Dashboard] Feature: Add Webhooks to Project Sidebar

## Notes for the reviewer

This PR adds the Webhooks option to the project sidebar navigation. The option was previously commented out due to a scrolling issue, which has now been resolved.

## How to test

Navigate to a project page and verify that the Webhooks option appears in the sidebar with a "New" badge.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refining the handling of webhook payloads and enhancing the `ProjectSidebarLayout` component by reintroducing the webhooks link. It also updates the API fetch calls to ensure they consistently use HTTPS.

### Detailed summary
- Changed `params` type in `webhookPayloadUtils.ts` from `Record<string, unknown> | unknown[]` to `Record<string, unknown>`.
- Updated `params` in the `buildTransactionWebhookPayload` function to use an empty object instead of an empty array.
- Reintroduced the webhooks link in `ProjectSidebarLayout.tsx` with appropriate properties.
- Updated fetch calls in `webhooks.ts` to use HTTPS for the API domain in multiple functions (`getWebhooks`, `deleteWebhook`, `testWebhook`).

> ✨ 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**
  - Re-enabled the "Webhooks" link in the project sidebar, now visible with a "New" badge.

- **Bug Fixes**
  - Improved API reliability by ensuring all webhook-related network requests use the correct HTTPS scheme.

- **Refactor**
  - Updated internal data structures for webhook payloads to improve consistency and type safety.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 19:30:57 +00:00
Joaquim VergesandGitHub 4d38a7e8fe Update page.mdx (#7204)
Signed-off-by: Joaquim Verges <[email protected]>
2025-05-29 11:32:36 -07:00
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
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
adc738e536 Version Packages (#7116)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-23 12:44:58 -07:00
Robby UitbeijerseandGitHub dd2fb1b13d [SDK] Feature: Allow for supported providers to be passed to PayEmbed (#7108) 2025-05-23 12:01:27 -07:00
Joaquim VergesandGitHub 69fdef07f5 [thirdweb] Ensure bigints are stringified before usage with server wallets (#7145) 2025-05-23 11:57:43 -07:00
MananTank 362c086bf6 Dashboard: Move project pages inside (sidebar) layout (#7144)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring the codebase to organize components and hooks into a new `(sidebar)` directory structure, enhancing modularity and maintainability. It also updates import paths to reflect these changes.

### Detailed summary
- Moved various components and hooks to a new `(sidebar)` directory.
- Updated import paths across multiple files.
- Adjusted layout files to accommodate the new directory structure.
- Ensured proper functionality of components post-migration.

> ✨ 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

- **Chores**
  - Updated and standardized import paths across the dashboard and component files for improved code organization and maintainability.
  - No changes to end-user functionality or visible features.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 18:12:01 +00:00
jnsdls f9b609e7a2 [Dashboard] replace Chakra fieldset with shadcn (#7143)
Migrates the NetworksFieldset component from Chakra UI to shadcn/ui and Tailwind.

- Drop Chakra Flex, FormControl and Select
- Use shadcn Select primitives with tailwind classes
- Adjust layout with Tailwind via `cn`

Fixes #0

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `NetworksFieldset` component in `networks-fieldset.tsx` to use a new `Select` component from the UI library, improving styling and functionality while maintaining the same overall behavior.

### Detailed summary
- Replaced `Flex` and `FormControl` components with a new `div` structure and `Select` component.
- Updated the `Select` component to use `SelectTrigger`, `SelectContent`, and `SelectItem`.
- Changed event handlers from `onChange` to `onValueChange` for better state management.
- Maintained layout and functionality with updated class names for 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

- **Refactor**
  - Updated the network selection field to use a custom select component and Tailwind-based layout for a more consistent user interface.
- **Style**
  - Improved the visual appearance and spacing of the network selection field.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 18:00:04 +00:00
MananTank 916ecf494c [TOOL-4591] Fix resolveImplementation jsdoc code example in jsdoc (#7141)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the import path for the `resolveImplementation` function to reflect its new location within the `thirdweb/utils` directory.

### Detailed summary
- Changed import statement for `resolveImplementation` from `thirdweb` to `thirdweb/utils`.

> ✨ 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

- **Documentation**
  - Updated example import path in documentation for improved clarity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 17:11:46 +00:00
MananTank d1ca9af2f4 Remove SIWA feedback buttons from nebula (#7142)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring the `Chats` component and its related functionalities. It removes unused code and simplifies the props being passed, particularly the `teamId`. Additionally, it eliminates the feedback buttons and associated functionality.

### Detailed summary
- Removed `teamId` prop from various instances of the `Chats` component.
- Deleted the `FeedbackButtons` component and its related logic.
- Simplified imports in the `Chats` component by removing unused imports.
- Cleaned up the `RenderMessage` function by omitting `teamId` from props.

> ✨ 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**
	- None.

- **Bug Fixes**
	- None.

- **Refactor**
	- Removed the feedback feature, including thumbs up/down buttons and related tracking, from chat components.
	- Simplified chat components by removing the unused or redundant team selection prop.

- **Chores**
	- Cleaned up code by deleting unnecessary feedback-related imports and props.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 16:02:33 +00:00
jnsdls 317211a48e [Dashboard] migrate CurrencySelector to shadcn (#7137)
## Summary
- migrate shared CurrencySelector from Chakra UI to shadcn/ui
- use tailwindcss classes and Radix-based Select

## Checklist
- [x] `pnpm biome check apps/dashboard/src/components/shared/CurrencySelector.tsx --apply`
- [ ] `pnpm test` *(fails: spawn anvil ENOENT)*

<!-- start pr-codex -->

---

## PR-Codex overview
This PR refactors the `CurrencySelector` component to improve its structure and styling. It replaces certain UI elements with a new `Select` component, enhances props for better functionality, and updates the layout for a more modern appearance.

### Detailed summary
- Changed `CurrencySelectorProps` to include new props: `onChange`, `className`, `isDisabled`.
- Refactored the component to use a `Select` from the UI library instead of native HTML elements.
- Updated event handling to utilize `onValueChange` for the `Select`.
- Improved styling and layout for better user experience.
- Replaced `Flex` components with `div` and applied conditional class names for 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

- **Refactor**
  - Updated the CurrencySelector component to use custom UI components and Tailwind CSS for styling, replacing Chakra UI elements.
  - Simplified the component's props and adjusted event handling for the new select interface.
  - Maintained existing functionality and behavior while improving consistency with the app's design system.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 04:52:16 +00:00
gregfromstl 6cd64e7368 [Dashboard] Fix: Routes page pagination beyond 1, add og image (#7136)
<!--

## 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 titles for better clarity and modifying the pagination logic in the routes table component to directly use `routesToRender`, enhancing the user experience in the dashboard.

### Detailed summary
- Updated `title` in `page.tsx` files for clarity.
- Changed the condition from `paginatedRoutes.length` to `routesToRender.length` in the routes table component.
- Removed pagination logic variables (`activePage`, `pageSize`, `startIndex`, `endIndex`, `paginatedRoutes`).
- Updated pagination component to use `Number(props.searchParams.page) || 1`.

> ✨ 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**
  - Updated page titles and metadata for improved clarity and consistency, including changes from "On-Ramp" to "Onramp" and a new title for the routes page.
- **Refactor**
  - Simplified the routes list rendering by removing local pagination logic, now displaying data directly as provided without client-side slicing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 04:43:14 +00:00
jnsdls 005acce714 Update PostHog import to use React hook (#7135)
# Update PostHog import to use React hook

This PR updates the PostHog implementation in the `useTrack` hook to use the React-specific hook from PostHog instead of the direct import. The changes include:

- Replaced the direct import of `posthog` with `usePostHog` from `posthog-js/react`
- Added the PostHog instance as a dependency to the `useCallback` hook
- Added a null check before calling `posthog.capture()` to handle cases where PostHog might not be initialized

These changes ensure proper React integration with PostHog and prevent potential issues with the hook's dependency array.
2025-05-23 04:34:30 +00:00
jnsdls cde2dd941f Make free plan badge clickable to upgrade (#7134)
# Add Upgrade Paths for Free Plan Teams

This PR enhances the user experience for teams on the free plan by adding clear upgrade paths throughout the dashboard:

- Makes the free plan badge clickable, linking directly to the billing page with pricing modal open
- Adds analytics tracking to billing-related links for better conversion insights
- Implements a new `FreePlanUpsellBannerUI` component on the team dashboard page
- Ensures only one billing alert banner shows at a time (following priority order)
- Updates all `TeamPlanBadge` components to include the team slug parameter
- Improves billing alert banners with proper analytics tracking

These changes aim to increase conversions from free to paid plans by providing contextual upgrade opportunities throughout the product.

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

- **New Features**
  - Introduced a promotional upsell banner for teams on the free plan, encouraging upgrades with a direct link to view available plans.
  - Added a customizable promotional banner component with multiple accent color themes for enhanced marketing displays.

- **Enhancements**
  - Team plan badges now provide direct links to billing settings for teams on the free plan, with analytics tracking for user interactions.
  - Improved analytics tracking on billing-related banners and links throughout the dashboard.

- **Refactor**
  - Updated multiple components to pass team identifiers to plan badges for more contextual display and interaction.
  - Streamlined the display logic for billing-related banners to prioritize service status and plan type.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `TeamPlanBadge` component by adding a `teamSlug` prop across various files and implementing a new `FreePlanUpsellBannerUI` component to encourage free plan users to upgrade. It also updates links to use `TrackedLinkTW` for better tracking.

### Detailed summary
- Added `teamSlug` prop to `TeamPlanBadge` in multiple components.
- Introduced `FreePlanUpsellBannerUI` to promote upgrades for free plan users.
- Replaced `<Link>` with `<TrackedLinkTW>` for better tracking in several instances.
- Updated conditional rendering logic for billing alerts in the team layout.

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

<!-- end pr-codex -->
2025-05-23 04:21:27 +00:00
7130f9a929 [Dashboard] siwa chat widget (#7128)
Co-authored-by: Joaquim Verges <[email protected]>
2025-05-22 21:20:45 -07:00
1e0b1422f3 [SDK] Implement allowedSmsCountryCodes option (#7090)
Co-authored-by: Jonas Daniels <[email protected]>
2025-05-22 21:14:29 -07:00
jnsdls 70a519ada4 [Dashboard] migrate authorized wallets components to shadcn (#7131)
## Summary
- migrate authorized wallets revoke modal from Chakra to shadcn dialog
- replace Chakra button/text in AuthorizedWalletsTable with shadcn + tailwind
- drop Chakra provider wrapper from account devices page
- cleanup modal title/description and remove node cache

## Testing
- `pnpm biome check --apply apps/dashboard/src/components/settings/AuthorizedWallets/AuthorizedWalletRevokeModal.tsx .gitignore`
- `pnpm test` *(fails: spawn anvil ENOENT)*

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `AccountDevicesPage` and the `AuthorizedWalletRevokeModal` components to replace the `Chakra UI` modal with a new `Dialog` component. It also updates the `AuthorizedWalletsTable` to use local state for modal visibility and modifies UI elements for consistency.

### Detailed summary
- Removed `ChakraProviderSetup` from `AccountDevicesPage`.
- Replaced `Modal` components with `Dialog` components in `AuthorizedWalletRevokeModal`.
- Updated modal structure and styling in `AuthorizedWalletRevokeModal`.
- Changed state management for modal visibility in `AuthorizedWalletsTable`.
- Updated text rendering from `Text` to `span` for consistency in `AuthorizedWalletsTable`.
- Changed button styles in `AuthorizedWalletsTable` for revocation actions.

> ✨ 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**
  - Replaced Chakra UI components with custom UI library components in wallet authorization modals and tables.
  - Updated modal and button styling for a consistent look and feel.
  - Simplified modal state management in the authorized wallets table.

- **Chores**
  - Updated `.gitignore` to exclude dashboard node compilation cache.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 03:54:27 +00:00
gregandGitHub 678c00e322 Fix: Update routes page to use server-side pagination and filtering (#7133) 2025-05-22 17:26:36 -07:00
MananTank 5ab59ff400 Dashboard: Add token selector in asset creation wizard (#7132)
<!--

## 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 token sale functionality within the application. It introduces new properties, refines existing structures, and improves the token selection process to accommodate native tokens and better handle token sales.

### Detailed summary
- Added `saleTokenAddress` to the form validation schema.
- Updated `CreateTokenAssetPage` to handle native token address logic.
- Changed `TokenSelector` to support selected tokens and improved token address handling.
- Enhanced `TokenSaleSection` to include a currency field for token sales.
- Refined state management in `PaymentLinkForm` for better token handling.
- Modified `TokenInfoFieldset` to trigger updates on chain changes.

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

<!-- end pr-codex -->
2025-05-22 23:15:23 +00:00
MananTank 73629e9f0e [TOOL-4531] Dashboard: Add Token Asset creation wizard (#7081)
<!--

## 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 several enhancements and features related to asset management, including deployment tracking, UI updates, and new forms for creating and managing tokens. It also refines existing components and adds new functionalities for improved user experience.

### Detailed summary
- Added `deploymentType` and `contractType` properties in various components.
- Introduced `revalidatePathAction` for path revalidation.
- Updated UI components for better asset management.
- Enhanced forms for token creation with validation.
- Implemented multi-step status tracking for token launch processes.
- Improved error handling and user feedback in forms.
- Added new charts for token distribution visualization.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/launch/launch-token.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/distribution/token-airdrop.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**
  - Introduced a comprehensive asset management interface for creating, importing, and managing ERC-20 tokens within team and project dashboards.
  - Added a multi-step token creation flow with form validation, including token info, distribution, sale settings, airdrop CSV upload, and launch process with real-time status tracking.
  - Implemented visual token allocation charts and multi-step progress indicators for enhanced user experience.
  - Added a dedicated "Assets" section in the sidebar with a "New" badge.
  - Added a horizontal segmented distribution bar chart component for visualizing token allocations.
  - Introduced a multi-step status component to track asynchronous step executions with retry capabilities.
  - Added token airdrop CSV upload and validation feature with user-friendly UI and error handling.
  - Added token sale configuration section with price and allocation inputs.
  - Enhanced import modal to support both contract and asset imports with contextual UI and behavior.
  - Added new pages and components to support token asset creation and management workflows, including cards for asset actions.
  - Added server-side path revalidation support for pages and layouts.

- **Improvements**
  - Enhanced contract and asset import workflows with clearer UI, context-aware dialogs, and improved contract table variant handling.
  - Updated number input fields to remove browser-native spinners for a cleaner appearance.
  - Improved analytics event tracking for user interactions and deployment steps.
  - Refined UI components for consistent styling and accessibility.
  - Fixed sidebar badge placement and updated UI text for clarity.
  - Updated dialogs and payment embeds to reference transaction chain instead of active wallet chain and set default payment amounts.
  - Changed token supply label to "Circulating Supply" for clarity.
  - Replaced Chakra UI spinner and styled elements with local components for simplified UI.

- **Bug Fixes**
  - Corrected UI typos and improved analytics event tracking for deployment errors.

- **Documentation**
  - Added Storybook stories for new components to facilitate UI testing and documentation.

- **Chores**
  - Refactored and extended types and props to support new asset and contract features, ensuring future extensibility.
  - Added tracking utilities for token deployment and asset creation analytics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-22 23:15:23 +00:00
23029db9ef Feature: Add image support to payment links (#7129)
Co-authored-by: Claude <[email protected]>
2025-05-22 12:47:05 -07:00
jnsdls 526b6b54cf Add useAuthToken hook for in-app wallet authentication (#7120)
# Add `useAuthToken` hook for in-app wallet authentication

This PR adds a new React hook `useAuthToken()` that returns a JWT authentication token for the currently active in-app wallet. This token can be used to authorize API calls to backend servers.

Key changes:
- Added `useAuthToken()` hook that returns the auth token when an in-app wallet is active
- Modified the `InAppConnector` interface to expose the storage property
- Updated the wallet interface to include a `getAuthToken()` method for in-app wallets
- Implemented token retrieval in the in-app wallet core implementation

The hook ensures the token is only returned when the active wallet is an in-app wallet and matches the active account.

Example usage:
```tsx
function MyComponent() {
  const authToken = useAuthToken();

  const fetchData = async () => {
    const response = await fetch('https://api.example.com/data', {
      headers: {
        'Authorization': `Bearer ${authToken}`
      }
    });
    // ... handle response
  };
}
```

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

- **New Features**
	- Introduced a new hook to retrieve authentication tokens for in-app wallets, enabling secure authorization for API calls.
- **Documentation**
	- Added usage examples and clarifications regarding authentication token availability for wallets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the wallet functionality by introducing an authentication token feature and updating the visibility of certain properties. It adds a method for retrieving an authentication token and modifies the storage property to be public.

### Detailed summary
- Changed `storage` property from `private` to `public` in `native-connector.ts` and `web-connector.ts`.
- Added `getAuthToken` method to the wallet interface in `wallet.ts` with documentation.
- Included `storage` in the `InAppConnector` interface.
- Implemented `authToken` handling in `createInAppWallet` function.
- Created `useAuthToken` hook for retrieving the JWT of the active wallet.

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

<!-- end pr-codex -->
2025-05-22 18:01:48 +00:00
Jonas DanielsandGitHub f31116ea5a [SDK] fix avatar image detection on Node (#7123)
Signed-off-by: Jonas Daniels <[email protected]>
2025-05-22 11:00:48 -07:00
arcoraven c1f7aaadc1 chore: redirect /team to /team/~ to select a team (#7124)
## [Dashboard] Fix: Simplify team redirection logic

## Notes for the reviewer
This PR modifies the team redirection logic by:
1. Renaming `getLastVisitedTeamOrDefaultTeam()` to `getLastVisitedTeam()`
2. Removing the fallback to `getDefaultTeam()` when no last visited team is found
3. Updating the team root page to redirect to `/team/~` when no team is found instead of showing a 404

## How to test
Verify that users are properly redirected to their last visited team when available, and to `/team/~` when no team history exists.

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

## Summary by CodeRabbit

- **Bug Fixes**
	- Improved handling when no last visited team is found by redirecting users to a fallback page instead of showing a not found error.

- **Refactor**
	- Updated team selection logic to remove fallback to a default team and streamline user redirection.

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

<!-- start pr-codex -->

---

## PR-Codex overview
This PR refactors the `getLastVisitedTeamOrDefaultTeam` function to `getLastVisitedTeam`, changing its behavior to return `null` instead of a default team. It updates the `TeamRootPage` to handle the absence of a team by redirecting to a fallback URL.

### Detailed summary
- Renamed function `getLastVisitedTeamOrDefaultTeam` to `getLastVisitedTeam`.
- Changed return value of `getLastVisitedTeam` from `getDefaultTeam()` to `null`.
- Updated `TeamRootPage` to redirect to `/team/~` if no team is found, instead of calling `notFound()`.

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

<!-- end pr-codex -->
2025-05-22 06:42:40 +00:00
arcoraven 1ad109f3bc chore: update endpoint to rotate secret key (#7122)
## [Dashboard] Fix: Update Secret Key Rotation API Endpoint

## Notes for the reviewer

This PR updates the secret key rotation functionality to use the new team-based API endpoint structure. The `rotateSecretKeyClient` function now requires both `teamId` and `projectId` parameters, and the API endpoint has been updated to follow the `/v1/teams/{teamId}/projects/{projectId}/rotate-secret-key` pattern.

## How to test

Test the secret key rotation functionality in:
1. Project FTUX
2. Nebula FTUX
3. Project General Settings page

Verify that the secret key can be successfully rotated in all these locations.

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

- **Refactor**
  - Updated secret key rotation to require both team and project identifiers for enhanced security and context.
  - Modified components to include team ID alongside project data, improving integration and user experience in project-related sections.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the handling of project and team identifiers in the `NebulaFTUX` and related components, enhancing the functionality for rotating secret keys by including the `teamId` alongside the `projectId`.

### Detailed summary
- Added `teamId` prop to the `NebulaFTUX` component.
- Passed `teamId` from `NebulaFTUX` to `SecretKeySection`.
- Updated `rotateSecretKeyClient` to accept an object with `teamId` and `projectId`.
- Modified the API call in `rotateSecretKeyClient` to use the new parameters.
- Updated the `SecretKeySection` to handle the new `teamId` prop.

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

<!-- end pr-codex -->
2025-05-22 06:25:22 +00:00
376bdb23d2 [Dashboard] Feature: Setup payment link UI (#7121)
Co-authored-by: Claude <[email protected]>
2025-05-21 22:40:59 -07:00
Joaquim VergesandGitHub dcd6b99e67 [SDK] Improve error handling in PayEmbed (#7119) 2025-05-21 21:52:31 -07:00
Jonas DanielsandGitHub 88533d9113 Remove advancedEnabled field from Account interface (#7118) 2025-05-21 16:16:21 -07:00
Jonas DanielsandGitHub 6c3a8d36a7 Remove unused account usage API and interface (#7117) 2025-05-21 16:09:16 -07:00
Robby UitbeijerseandGitHub a31ac0c065 [Docs] Chore: Remove Kado references (#7109) 2025-05-21 15:21:40 -07:00
7b72e88611 [SDK] Fix: Apply accentButtonText theme key to the Spinner inside of PayEmbed Buttons (#7110)
Co-authored-by: Joaquim Verges <[email protected]>
2025-05-21 15:21:14 -07:00
Joaquim VergesandGitHub 4b9a506fdf [service-utils] Improve 403 error messages with team and project details (#7114) 2025-05-21 14:07:40 -07:00
Joaquim VergesandGitHub 77da352366 [Dashboard] Add ecosystem name editing functionality (#7115) 2025-05-21 13:54:12 -07:00
Joaquim VergesandGitHub 72cc27a69a [Dashboard] Add ecosystem logo upload functionality (#7113) 2025-05-21 11:02:18 -07:00
Joaquim VergesGitHubJonas Danielsgraphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
46d1dab11e [Docs] Add Vault SDK documentation and guides (#7054)
Signed-off-by: Joaquim Verges <[email protected]>
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-21 11:01:56 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a1c432ca17 Version Packages (#7105)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-21 09:59:10 -07:00
kumaryash90 243c661929 Update deploy config (#7111)
<!--

## 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 value of `DEFAULT_FEE_BPS_NEW` in the `addresses.ts` file to reflect a new default fee setting.

### Detailed summary
- Updated the value of `DEFAULT_FEE_BPS_NEW` from `150` to `200` in `apps/dashboard/src/constants/addresses.ts`.

> ✨ 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

- **Chores**
  - Updated the default fee value from 150 to 200.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-21 16:07:00 +00:00
GiselleNessi b64ffe369a Gi/portal knowledge base (#6993)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the `thirdweb Knowledge Base` by adding new troubleshooting guides, updating existing resources, and improving documentation structure for better user support.

### Detailed summary
- Added a new troubleshooting guide for `Nebula`.
- Introduced detailed steps for resolving `device link errors` in `thirdweb CLI`.
- Updated support links in `Header.tsx`.
- Created new sections for `batch upload issues` and `airdrop contract troubleshooting`.
- Improved layout in `layout.tsx` with `DocLayout`.
- Removed unused `ContractsIcon` from `page.tsx`.
- Enhanced the `knowledge base` sidebar structure.
- Added visual guides and images in multiple `mdx` files for clarity.
- Improved metadata for better SEO and documentation clarity.

> ✨ 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 comprehensive Knowledge Base with structured navigation, guides, and troubleshooting for web3 development and thirdweb support.
  - Added detailed documentation on deploying NFT mint sites, creating soulbound tokens, sharing console errors, and resolving CLI device link errors.
  - Launched troubleshooting guides for airdrop contracts, batch upload issues, embed feature changes, and Nebula-specific support.

- **Improvements**
  - Updated navigation links to add a Nebula API reference and consolidated support links to internal resources.

- **Removals**
  - Removed the "Contracts" article card from the learning resources section.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-21 10:32:06 +00:00
Joaquim VergesandGitHub 30fafd16db [SDK] Fix SIWE auth forcing unnecessary chain switch (#7104) 2025-05-20 23:28:53 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
99901dcfd7 Version Packages (#7079)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-20 15:48:08 -07:00
jnsdls f5072d882e Rename "Secret Key" to "Webhook Secret" for consistency (#7102)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Updated labels and tooltips to use "Webhook Secret" instead of "Secret Key" for improved clarity in the webhook creation interface.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-20 22:10:27 +00:00
iuwqyir e7f7a32dd4 show block gas limit on the chain page (#7101)
## [Dashboard] Feature: Add Block Gas Limit to Chain Live Stats

## Notes for the reviewer

as requested by @0xFirekeeper

This PR enhances the chain live stats component by adding the block gas limit information. The RPC method has been updated from `eth_blockNumber` to `eth_getBlockByNumber` to fetch additional block data including the gas limit.

## How to test

The block gas limit should now be displayed alongside other chain statistics in the dashboard. The component handles loading and error states appropriately.

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

## Summary by CodeRabbit

- **New Features**
  - Added display of the latest block's gas limit in the live stats section, including proper handling of loading, error, and unavailable states.

- **Enhancements**
  - Improved the accuracy of live stats by retrieving and displaying more detailed block information.

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

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `live-stats.tsx` component to fetch the latest block information using the `eth_getBlockByNumber` method instead of `eth_blockNumber`. It also introduces the display of the block gas limit in the UI.

### Detailed summary
- Changed JSON-RPC method from `eth_blockNumber` to `eth_getBlockByNumber`.
- Updated parameters to fetch the latest block.
- Added parsing for `blockGasLimit` from the response.
- Modified return object to include `blockGasLimit`.
- Introduced a new UI section to display the block gas limit.

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

<!-- end pr-codex -->
2025-05-20 21:55:31 +00:00
d2098ad0bf [SDK] Fix stray keccak256 reference (#7089)
Co-authored-by: Jonas Daniels <[email protected]>
2025-05-20 14:54:41 -07:00
+1
Joaquim VergesGitHubgregfromstlClaudekumaryash90Prithvish BaidyasaminaMaximilian Hubertgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Jonas Danielsarcoraven
d6239785cf [SDK] Always show pay modal even on non-supported UB chains (#7092)
Signed-off-by: samina <[email protected]>
Signed-off-by: Maximilian Hubert <[email protected]>
Co-authored-by: gregfromstl <[email protected]>
Co-authored-by: Claude <[email protected]>
Co-authored-by: kumaryash90 <[email protected]>
Co-authored-by: Prithvish Baidya <[email protected]>
Co-authored-by: samina <[email protected]>
Co-authored-by: Maximilian Hubert <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: arcoraven <[email protected]>
2025-05-20 14:51:58 -07:00
iuwqyir 20fdd5b6d7 show deprecation in playground UI (#7100)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on adding a `deprecated` property to various types and components within the application, allowing for better handling and display of deprecated links and features in the UI.

### Detailed summary
- Added `deprecated` property to `pathInfo` in `navLinks.ts`.
- Updated `MinimalBlueprintSpec` type in `utils.ts` to include `deprecated`.
- Modified `SidebarLink` type in `sidebar.tsx` to optionally include `crossedOut`.
- Enhanced UI to visually indicate deprecated items in `page.tsx`.
- Included `deprecated` status for various API endpoints in `insightBlueprints.ts`.

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

<!-- end pr-codex -->
2025-05-20 21:04:28 +00:00
iuwqyir 438c019f01 replace chain with chain_id in insight playground (#7099)
## [Playground] Fix: Update chain parameter handling in blueprint playground

## Notes for the reviewer
This PR updates the blueprint playground to use `chain_id` instead of `chain` parameter throughout the codebase. It also adds logic to remove deprecated parameters from the form schema and specifically removes the `chain` parameter if it's present.

## How to test
Test the blueprint playground with various chain selections to ensure the chain_id parameter is correctly populated and that deprecated parameters are not displayed in the form.

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

- **Refactor**
  - Updated all references from "chain" to "chain_id" for improved consistency across the playground interface.
  - Deprecated parameters are now excluded from form generation and display.

- **New Features**
  - Blueprint listings can now indicate if a blueprint is deprecated.

- **Chores**
  - Made the API domain configurable via environment variables with a default fallback.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces changes to the `BlueprintSection`, `fetchBlueprintSpec`, and other components to enhance the handling of blueprint parameters, including the addition of a `deprecated` flag and renaming of parameters for consistency. It also updates the API URL handling.

### Detailed summary
- Added optional `deprecated` property to `blueprints` in `BlueprintSection`.
- Introduced `THIRDWEB_INSIGHT_API_DOMAIN` for dynamic API URL in `fetchBlueprintSpec`.
- Renamed parameter `chain` to `chain_id` in multiple places for consistency.
- Removed `chain` parameter if present in `modifyParametersForPlayground`.
- Updated form handling to use `chain_id` instead of `chain` in `ParameterSection`.
- Skipped deprecated parameters in schema creation with `createParametersFormSchema`.

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

<!-- end pr-codex -->
2025-05-20 20:46:17 +00:00
Jonas DanielsandGitHub 61044013c8 update dependencies (#7098) 2025-05-20 11:52:26 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e6213db8af chore(deps-dev): bump @storybook/react-vite from 8.6.12 to 8.6.14 (#7088)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:24:24 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c46b2597be chore(deps-dev): bump vitest from 3.1.2 to 3.1.4 (#7087)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:24:12 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9ff97688ce chore(deps-dev): bump vite from 6.3.4 to 6.3.5 in the npm_and_yarn group (#7068)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:20:20 -07:00
gregandGitHub b4f5cc7b5a Fix: Bridge jsdoc to not mention ox (#7097) 2025-05-20 11:15:06 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
256850d1c6 chore(deps): bump @next/mdx from 15.3.1 to 15.3.2 (#7085)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:13:10 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
acb2b9f85f chore(deps): bump github/codeql-action from 3.28.17 to 3.28.18 (#7084)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:12:54 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6ac62ffe96 chore(deps): bump codecov/codecov-action from 5.4.2 to 5.4.3 (#7083)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:12:40 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
80f4bd4e53 chore(deps): bump changesets/action from 1.5.0 to 1.5.3 (#7082)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:12:25 -07:00
Joe-Thirdweb a3072c4335 [Dashboard] Adding EDU Chain custom info + assets (#7095)
Adding custom chain info for
EDU Mainnet ( 41923 )
EDU Testnet ( 656476 )

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new assets and metadata for an education-focused blockchain initiative, enhancing the dashboard with information about the `EDU Mainnet` and `EDU Testnet`.

### Detailed summary
- Added new image imports: `eduBanner` and `eduCTA`.
- Introduced metadata for `EDU Mainnet` (ID: 41923) with header image, description, and CTA.
- Introduced metadata for `EDU Testnet` (ID: 656476) with similar structure as `EDU Mainnet`.

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

<!-- end pr-codex -->
2025-05-20 17:53:29 +00:00
saminacodes 3277cf6e8e [Docs] Fixed broken links (#7091)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on updating references from "backend wallets" to "server wallets" across various files and adding a new `ArticleCardIndex` component in the `page.tsx` file.

### Detailed summary
- In `sidebar.tsx`, updated `href` for the "Overview" link.
- Added a new `ArticleCardIndex` for "Universal Bridge" in `page.tsx`.
- Changed text from "backend wallets" to "server wallets" in `page.mdx`.
- Updated link in `engine-overview.tsx` to point to server wallet documentation.

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

<!-- end pr-codex -->
2025-05-20 17:44:56 +00:00
Jake LooandGitHub 77938c4d5c Update page.mdx for thirdweb-mcp (#7096)
Signed-off-by: Jake Loo <[email protected]>
2025-05-20 10:15:57 -07:00
Vasiliy KirochkaandGitHub 3080a9baba fixing code snippets (#7094) 2025-05-20 09:04:18 -07:00
Joaquim VergesandGitHub 6cf92c6b11 Add AGENTS.md with guidelines (#7093) 2025-05-20 00:18:02 -07:00
+1
Joaquim VergesGitHubgregfromstlClaudekumaryash90Prithvish BaidyasaminaMaximilian Hubertgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Jonas Danielsarcoraven
89ccc80d9c Add Bridge.Onramp functions + port old functions (#7076)
Signed-off-by: samina <[email protected]>
Signed-off-by: Maximilian Hubert <[email protected]>
Co-authored-by: gregfromstl <[email protected]>
Co-authored-by: Claude <[email protected]>
Co-authored-by: kumaryash90 <[email protected]>
Co-authored-by: Prithvish Baidya <[email protected]>
Co-authored-by: samina <[email protected]>
Co-authored-by: Maximilian Hubert <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: arcoraven <[email protected]>
2025-05-19 19:24:32 -07:00
saminacodes 713ebe0a2d [Docs] Update Engine Pricing (#7080)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the FAQ section in the `page.mdx` file regarding pricing calculations for Engine Cloud. It clarifies the types of requests that incur costs and modifies the note about legacy pricing plans.

### Detailed summary
- Updated pricing calculation to include both write and sign requests.
- Changed the cost description for requests to specify both write and sign requests.
- Revised the note on legacy pricing plans to include a link to the pricing page and clarify the fee specifics.

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

<!-- end pr-codex -->
2025-05-19 15:51:01 +00:00
saminacodes bfdac10dcc [Docs] UB Migration & Other Small Things (#7075)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on restructuring the `pay` section of the portal by updating links, modifying sidebar navigation, and adding new assets and pages to improve the user experience and documentation clarity.

### Detailed summary
- Added new links to the `links` array for `Bridge`.
- Updated `href` values in `apisLinks` for the Insight API.
- Modified `FooterLinkItem` labels for consistency.
- Created and adjusted multiple `.mdx` pages for the `pay` section.
- Introduced `payRedirects` for improved navigation.
- Updated the sidebar structure for the `pay` section, including new links and icons.
- Added new layout and metadata to `layout.tsx` for better documentation presentation.

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

<!-- end pr-codex -->
2025-05-19 04:26:51 +00:00
+1
gregGitHubClaudeJoaquim Vergeskumaryash90Prithvish BaidyasaminaMaximilian Hubertgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Jonas Danielsarcoraven
64964da22e [SDK] Refactor: Update Pay Functions to use UB (#7064)
Signed-off-by: samina <[email protected]>
Signed-off-by: Maximilian Hubert <[email protected]>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
Co-authored-by: kumaryash90 <[email protected]>
Co-authored-by: Prithvish Baidya <[email protected]>
Co-authored-by: samina <[email protected]>
Co-authored-by: Maximilian Hubert <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: arcoraven <[email protected]>
2025-05-19 16:05:15 +12:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
260c2fd370 Version Packages (#7078)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-18 19:12:58 -07:00
Joaquim VergesandGitHub c61e258ec3 [SDK] Expose getInstalledWallets() utility (#7077) 2025-05-18 19:07:42 -07:00
Joaquim VergesandGitHub fdd344184b [Dashboard] Remove taint for THIRDWEB_ENGINE_URL (#7074) 2025-05-17 00:51:54 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0c853e8709 Version Packages (#7073)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-17 00:36:55 -07:00
arcoraven 7d47b4cde0 chore: Update Accelerate features list (#7053)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating the pricing plans and features for the embedded wallets in the dashboard application. It adjusts the plan requirements and modifies the features associated with different plans.

### Detailed summary
- Changed `authRequiredPlan` from `"accelerate"` to `"growth"`.
- Added `"Custom Wallet Branding & Auth"` to the features list.
- Removed `"Custom Wallet Branding & Auth"` from the `TEAM_PLANS` features.
- Increased `$400 Usage Credits` to `$500 Usage Credits` in the `TEAM_PLANS` features.

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

<!-- end pr-codex -->
2025-05-17 07:01:00 +00:00
Jonas DanielsandGitHub feae304da2 Prioritize JWT over service API keys in authentication (#7020) 2025-05-16 16:39:45 -07:00
Joaquim VergesandGitHub 8f605d989c [SDK] Update package READMEs with correct naming and documentation links (#7071) 2025-05-16 05:55:22 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
d0c2ac63a9 Version Packages (#7042)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-16 05:06:17 -07:00
Joaquim VergesandGitHub 170f14fb54 [Dashboard] Add pagination to server wallets page (#7070) 2025-05-16 05:04:26 -07:00
Joaquim VergesandGitHub da2a2a0e86 [SDK] Fix buyWithCrypto false not respected when returning from quote (#7069) 2025-05-16 04:57:48 -07:00
saminaandGitHub e62fd3fe40 [Docs] Engine Update (#7052)
Signed-off-by: samina <[email protected]>
2025-05-16 04:09:33 -07:00
Joaquim VergesandGitHub 99a7cce624 [Engine] Add all testnets to test transaction options (#7066) 2025-05-16 04:08:17 -07:00
Joaquim VergesandGitHub 61152dd098 [Engine] Add FAILED status to execution result and update error handling (#7065) 2025-05-16 03:34:27 -07:00
Maximilian HubertandGitHub 05519fcdde [DOCS] fix link in contributing.md (#7063)
Signed-off-by: Maximilian Hubert <[email protected]>
2025-05-16 00:11:57 -07:00
saminaandGitHub 7257747520 [Docs] Engine (#7057)
Signed-off-by: samina <[email protected]>
2025-05-16 00:10:35 -07:00
Prithvish BaidyaandGitHub e71e7d70a6 [Vault] default to engine-cloud vault proxy (#7058) 2025-05-15 16:44:03 -07:00
Joaquim VergesandGitHub 8d47864967 [SDK] Export prepareUserOp and add support for new wallets (#7061) 2025-05-15 16:43:11 -07:00
kumaryash90 4a8adc143f [Docs] Stylus dev workflow (#7060)
<!--

## 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 documentation and assets for integrating thirdweb tools into the Stylus contract development workflow, specifically for Rust. It includes setup instructions, publishing and deployment commands, and images to enhance the documentation.

### Detailed summary
- Added new sections in `page.mdx` for Arbitrum Stylus.
- Included prerequisites for Rust and Solidity installations.
- Provided setup commands for creating a Stylus project.
- Added commands for publishing and deploying Stylus contracts.
- Included images (`stylus-publish-page.png`, `stylus-deploy-page.png`) to illustrate the process.

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

<!-- end pr-codex -->
2025-05-15 21:30:32 +00:00
MananTank db538473cf [NEB-248] Show native token balances in assets, show popular tokens on top (#7056)
<!--

## 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 `AssetsSection` component by integrating wallet balance retrieval, modifying how assets are displayed, and implementing new logic for sorting tokens. It also introduces a new constant for native tokens and adjusts rendering based on whether an asset is native.

### Detailed summary
- Added `NATIVE_TOKEN_ADDRESS` and `getWalletBalance` import from `thirdweb`.
- Updated `key` for `AssetItem` to include `chain_id`.
- Introduced `isNativeToken` to conditionally render the `ChainIconClient`.
- Modified the `href` for `Link` based on whether the asset is a native token.
- Added a new set `tokensToShowOnTop` for sorting specific tokens.
- Implemented `nativeBalances` query to fetch wallet balances for various chains.
- Combined `nativeBalances` and `assetsQuery` data for rendering in `AssetsSectionUI`.

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

<!-- end pr-codex -->
2025-05-15 20:52:49 +00:00
Manan TankandGitHub 2057546934 Nebula: Recent Activity improvements in sidebar (#7055) 2025-05-15 13:52:15 -07:00
gregandGitHub ba52e68eab [Dashboard] Fix: Trend markers and UB volume type dropdown (#7059) 2025-05-15 12:05:40 -07:00
0eff64824b [Dashboard] Feature: Add Universal Bridge to analytics dashboard (#7051)
Co-authored-by: Claude <[email protected]>
2025-05-14 18:07:07 -07:00
saminacodes ae7407dca4 [Docs] engine and ub changes (#7049)
<!--

## 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 documentation and user guidance for the `thirdweb` platform, specifically around the Engine v3 features, onramp providers, and pricing details. It introduces new FAQs, clarifies migration steps, and adds visual content.

### Detailed summary
- Added new section for "Onramp Providers" in `sidebar.tsx`.
- Updated Engine FAQs in `page.mdx` with pricing and transaction details.
- Revised migration guide in `page.mdx` with clearer instructions and a callout for wallet migration.
- Enhanced `faqs/page.mdx` with additional questions about Universal Bridge and supported payment methods.
- Updated "Buy With Fiat" availability to 160+ countries.
- Included images for creating a vault and sending transactions in the `get-started/page.mdx`.
- Revised onramp providers section to clarify provider preferences and fees in `onramp-providers/page.mdx`.

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

<!-- end pr-codex -->
2025-05-14 22:53:11 +00:00
Joe-Thirdweb a443b309dc Adding NeoX custom chain info. (#7045)
Adding Custom chain info to

- NeoX Mainnet 47763
- NeoX Testnt 12227332

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new assets and metadata for the `Neo X` blockchain, enhancing the dashboard with images and information about its features and offerings.

### Detailed summary
- Added imports for `NeoXBanner` and `NeoXCTA` images.
- Introduced metadata for `Neo X` (ID: 47763) including:
  - `headerImgUrl`
  - `about` description
  - `cta` object with `backgroundImageUrl`, `title`, `buttonLink`, and `buttonText`.
- Duplicated metadata for `Neo X Testnet` (ID: 12227332) with similar structure.

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

<!-- end pr-codex -->
2025-05-14 22:44:28 +00:00
Joaquim VergesGitHubgraphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
ae2ff743c0 [Engine] Update OpenAPI spec and configuration (#7050)
Signed-off-by: Joaquim Verges <[email protected]>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-14 15:43:30 -07:00
MananTank dfa8603593 [NEB-213] Nebula: Show wallet Assets and Activity in sidebar (#7046)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the `AssetsSection` and `TransactionsSection` components by adding new features and improving the UI. It also modifies several existing components for better functionality and user experience.

### Detailed summary
- Changed `mergedSessions.push(session)` to `mergedSessions.unshift(session)` in `useSessionsWithLocalOverrides.ts`.
- Updated the width of the sidebar in `ChatPageLayout.tsx` from `280px` to `300px`.
- Added `customDetailsButton` prop to `NebulaConnectWallet` in `NebulaConnectButton.tsx`.
- Implemented conditional rendering for `customDetailsButton` in `NebulaConnectWallet`.
- Created storybook stories for `AssetsSection` and `TransactionsSection` with stubs for tokens and transactions.
- Enhanced `AssetsSection` and `TransactionsSection` components to handle loading states and display messages when there are no assets or transactions.
- Introduced `WalletDetails` component in `ChatSidebar` to manage assets and transactions tabs.
- Refactored `WalletSelector` to integrate `useActiveWallet` for better wallet management and user feedback.
- Improved button components with tooltips for better accessibility and UX.

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

<!-- end pr-codex -->
2025-05-14 21:47:42 +00:00
saminacodes 9f1ca54c26 Update page.mdx (#7048)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `preferredProvider` options in the `detailsModal` configuration for the `connectbutton` component, enhancing payment provider flexibility.

### Detailed summary
- Modified `preferredProvider` options in `detailsModal`:
  - Removed `"KADO"`
  - Added `"COINBASE"`

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

<!-- end pr-codex -->
2025-05-14 17:44:23 +00:00
MananTank 7df81b7323 [TOOL-4493] Dashboard: Add starter legacy discontinuation banner (#7047)
<!--

## 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 `StarterLegacyDiscontinuedBanner` component to the `RootTeamLayout` in `layout.tsx`. It displays a notification for users on the "starter_legacy" billing plan, informing them of the discontinuation and providing a link to select a new plan.

### Detailed summary
- Added a conditional rendering for `StarterLegacyDiscontinuedBanner` based on the team's billing plan.
- Created the `StarterLegacyDiscontinuedBanner` component to inform users about the discontinuation of starter legacy plans.
- Included a button with a link to the billing settings for selecting a new plan, featuring an `ArrowRightIcon`.

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

<!-- end pr-codex -->
2025-05-14 16:25:35 +00:00
GiselleNessi ab74fcdaf8 add new apis tab to nav (#7044)
<!--

## 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 adds a new section to the `Header` component in the `Header.tsx` file, introducing `apisLinks` for API documentation links and integrating a `DropdownLinks` component for displaying these links.

### Detailed summary
- Added a new constant `apisLinks` containing links to API documentation.
- Introduced a `DropdownLinks` component to display `apisLinks` in the header.
- Adjusted padding in the header's layout from `xl:px-2` to `xl:px-1` for the new dropdown.

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

<!-- end pr-codex -->
2025-05-14 15:09:12 +00:00
Joaquim VergesandGitHub 1191d5dad9 [Engine] Add EOA read/create permissions for server wallets (#7043) 2025-05-14 22:32:35 +12:00
Joaquim VergesandGitHub 20b5ba943a [SDK] Propagate 401 errors when connecting in-app wallet (#7040) 2025-05-14 02:03:41 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f0f8da8fab Version Packages (#7032)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-13 19:50:56 -07:00
Joaquim VergesandGitHub da3fc99c0b [SDK] Default to in-memory storage for inAppWallet outside browser (#7038) 2025-05-13 18:32:37 -07:00
fb4c190eb3 [Dashboard] Feature: Default analytics range to 30 days and save preference (#7039)
Co-authored-by: Claude <[email protected]>
2025-05-13 15:25:09 -07:00
FirekeeperandGitHub 1c31a1e4df [Portal] .NET ThirdwebBridge Onramping & InAppWallet 7702 Account Abstraction (#7017) 2025-05-13 14:14:45 -07:00
MananTank 04c10f82fd [TOOL-4471] GatedSwitch updates, Make In-app wallet branding gated by starter+ (#7021)
<!--

## 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 billing and plan management features in the dashboard. It introduces new props for handling plan highlights and open plan sheets, modifies tooltip styles, and adjusts billing card components to improve user experience.

### Detailed summary
- Updated `tooltip.tsx` to change padding in tooltip content.
- Changed `requiredPlan` from `authRequiredPlan` to `brandingRequiredPlan` in `Configure/index.tsx`.
- Added `openPlanSheetButtonByDefault` and `highlightPlan` props in various components.
- Modified `PlanInfoCardClient` and `Billing` components to utilize new props.
- Updated `TeamPlanBadge` to include an optional postfix.
- Enhanced `BillingPricing` logic to handle plan highlights.
- Adjusted `GatedSwitch` tooltip content for better user guidance.

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

<!-- end pr-codex -->
2025-05-13 20:22:10 +00:00
MananTank ab173886ac [TOOL-4483] Move changelog to team landing page from notifications (#7037)
<!--

## 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 refactoring notification handling in the application, removing unused functions, and enhancing the UI components related to notifications and alerts.

### Detailed summary
- Removed `getChangelogNotifications` and related props from several components.
- Introduced `DismissibleAlert` component for better alert management.
- Updated `NotificationButtonUI` to streamline notification handling.
- Adjusted UI elements for consistency and clarity.

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

<!-- end pr-codex -->
2025-05-13 19:19:55 +00:00
Joe-Thirdweb df06bec699 [Dashboard]Adding zkCandy + Onyx chain updates (#7035)
Adding custom chain info to

- Onyx Mainnet
- zkCandy Mainnet

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new assets and updates the `chainMetaRecord` object to include information for the `Onyx` and `zkCandy` blockchains, enhancing the dashboard with additional details and visuals.

### Detailed summary
- Added imports for `onyxBanner`, `onyxCTA`, and `zkCandyBanner` images.
- Introduced `Onyx` blockchain entry in `chainMetaRecord` with header image, description, and CTA.
- Added `zkCandy` blockchain entry in `chainMetaRecord` with header image, description, and CTA.
- Updated comment for `zkCandy` entry to specify it as "zkCandy Testnet".

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

<!-- end pr-codex -->
2025-05-13 15:42:08 +00:00
Joaquim VergesandGitHub 6000dacfbb [Docs] Add Engine v2 to v3 migration guide (#7036) 2025-05-13 04:12:27 -07:00
Joaquim VergesandGitHub fc6535069b [SDK] Preload wallet balances on pay embed (#7034) 2025-05-13 17:08:26 +12:00
Joaquim VergesandGitHub b15c3c0747 Add contract availability documentation to ERC methods (#7033) 2025-05-12 17:47:28 -07:00
Joaquim VergesandGitHub 1985de89aa [SDK] Expose inMemoryStorage for inAppWallet backend usage (#7016) 2025-05-12 16:04:22 -07:00
MananTank 2b87b7bb75 [NEB-246] Nebula: handle aborted session with no messages (#7025)
<!--

## 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 `ChatSidebar`, `useSessionsWithLocalOverrides`, and `ChatPageContent` components. It improves session handling, updates the chat sidebar title, and adds a user-friendly message for sessions without messages.

### Detailed summary
- Updated `title` prop in `<ChatSidebarLink>` to default to "Untitled Chat".
- Merged new sessions with existing ones in `useSessionsWithLocalOverrides`.
- Introduced a message and icon for sessions with no messages in `ChatPageContent`.
- Conditional rendering of `<Chats>` based on message availability.

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

<!-- end pr-codex -->
2025-05-12 20:54:35 +00:00
MananTank d99a985ab4 [NEB-238] Show ENS name in selected account badge (#7024)
<!--

## 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 rendering of the `AccountName` component in the `ChatBar.tsx` file to improve the styling and structure of the address display.

### Detailed summary
- Replaced `span` with `AccountName` component for displaying the selected address.
- Added `className="text-xs"` to the `AccountName` component.
- Updated `loadingComponent` and `fallbackComponent` to use a consistent `span` with `className="text-xs"`.
- Changed `className` of `span` in `WalletSelector` from `font-mono text-sm` to `text-sm` for a consistent style.

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

<!-- end pr-codex -->
2025-05-12 20:54:35 +00:00
MananTank e74574ccf5 [NEB-239] Fix chain search not working properly for chain name starting with number (#7023)
<!--

## 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 improving the code by replacing `Number.parseInt` with `Number` for checking if a value is an integer across multiple files, enhancing readability and consistency.

### Detailed summary
- In `TokenSelector.tsx`, changed `Number.parseInt(searchValue)` to `Number(searchValue)`.
- In `single-network-selector.tsx`, modified the same integer check.
- In `ConfigureCustomChain.tsx`, updated integer check for `chainSlug`.
- In `NetworkSelectors.tsx`, replaced the integer check similarly.

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

<!-- end pr-codex -->
2025-05-12 20:54:35 +00:00
MananTank 98d05d6fb6 [NEB-247] Show error event in chat UI (#7022)
<!--

## 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 error handling for chat responses in the `ChatPageContent` and `chat.ts` files. It adds functionality to display error messages when an error event is received.

### Detailed summary
- Added handling for `"error"` case in `ChatPageContent.tsx` to update messages with error details.
- Implemented parsing of error data in `chat.ts` to extract error code and message.
- Updated `ChatStreamedEvent` type to include an `"error"` event with relevant data structure.

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

<!-- end pr-codex -->
2025-05-13 02:09:03 +05:30
MananTank f4ac3c2a3a [NEB-245] Limit image format types for upload (#7019)
<!--

## 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 `ImageUploadButton` component by allowing it to accept specific image file types and updating the `ChatBar` component to utilize this feature.

### Detailed summary
- In `ChatBar.tsx`, added `accept` attribute with value `"image/jpeg,image/png,image/webp"` to the file input.
- In `image-upload-button.tsx`, changed `accept` property from a static value `"image/*"` to a dynamic value using `props.accept`.
- Updated `ImageUploadButton` to include `accept` in its props definition.

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

<!-- end pr-codex -->
2025-05-12 20:26:53 +00:00
MananTank a3a4bf4361 Nebula: Fix User Image message type (#7018)
<!--

## 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 image handling in the `types.ts` and related components by modifying the structure of image objects, allowing for optional `image_url` and `b64` properties.

### Detailed summary
- In `types.ts`, changed the `image` type to include `image_url: string | null` and `b64: string | null`.
- In `ChatBar.tsx`, added `image_url: null` when pushing the image object to `userMessage.content`.
- In `Chats.tsx`, updated the condition to check if `msg.b64` is a string and used `msg.image_url ?? ""` for the URL.
- In `NebulaImage.tsx`, adjusted the skeleton div to have a minimum height of 300px.

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

<!-- end pr-codex -->
2025-05-12 17:15:52 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e8ec54e6e2 Version Packages (#7014)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-12 01:43:18 -07:00
Joaquim VergesandGitHub 58e343c10c [SDK] Improve EIP-5792 wallet capabilities support (#7003) 2025-05-12 01:25:30 -07:00
Prithvish BaidyaandGitHub bad9db9760 postfix slash to engine url (#7013) 2025-05-12 00:41:45 -07:00
977 changed files with 43683 additions and 22738 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ We use [Turborepo](https://turbo.build/repo/docs) to manage the repository, and
We use [pnpm](https://pnpm.io) for package management across the repo. `pnpm` is similar to `npm` or `yarn` but with more efficient disk space usage.
**With the v5 SDK, we've consolidated everything into a single project at [/packages/thirdweb](./packages/thirdweb). You can still find the legacy packages at [/legacy_packages](./legacy_packages).**
**With the v5 SDK, we've consolidated everything into a single project at [/packages/thirdweb](../packages/thirdweb). You can still find the legacy packages at [/legacy_packages](../legacy_packages).**
This single package provides a performant & lightweight SDK to interact with any EVM chain across Node, React, and React Native. Learn more about how to use the thirdweb SDK in our [documentation](https://portal.thirdweb.com/typescript/v5).
+1 -1
View File
@@ -89,7 +89,7 @@ jobs:
- run: pnpm test
- name: Code Coverage
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
with:
directory: packages/
flags: packages
+3 -3
View File
@@ -46,7 +46,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -59,7 +59,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -72,4 +72,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: Create release Pull Request or publish to NPM
id: changesets
uses: changesets/action@746c25e23caa47dceb6a48ee85b4cbc5a9f5f293 #v1.4.0
uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba #v1.4.0
with:
publish: pnpm release
version: pnpm version-packages
+2 -1
View File
@@ -31,4 +31,5 @@ storybook-static
.aider*
tsconfig.tsbuildinfo
.cursor
.cursor
apps/dashboard/node-compile-cache
+123
View File
@@ -0,0 +1,123 @@
🤖 Codex Agent Guidelines for thirdweb-dev/js
Welcome, AI copilots! This guide captures the coding standards, architectural decisions, and workflow conventions that every automated agent (and human contributor!) must follow. Unless a rule explicitly targets a sub‑project, it applies repo‑wide.
⸻
1. GitHub Workflow & Etiquette
- Pull‑request titles must start with the affected workspace in brackets (e.g. [SDK], [Dashboard], [Portal], [Playground]).
- Begin the PR description with a one‑sentence summary, then add a checklist of changes and reference issues with Fixes #123.
- Keep commits small and topical – one logical change per commit.
- Branch names should follow area/brief-topic (e.g. sdk/fix-gas-estimate). Avoid personal names.
- Request at least one core maintainer review. Do not self‑merge unless you are the sole owner of that package.
- All CI checks (type‑check, Biome, tests) must pass before merging.
⸻
2. Formatting & Linting
- Biome governs formatting and linting; its rules live in biome.json.
- Run pnpm biome check --apply before committing.
- Avoid editor‑specific configs; rely on the shared settings.
⸻
3. TypeScript Style Guide
- Write idiomatic TypeScript: explicit function declarations and return types.
- Limit each file to one stateless, single‑responsibility function for clarity and testability.
- Re‑use shared types from @/types or local types.ts barrels.
- Prefer type aliases over interface except for nominal shapes.
- Avoid any and unknown unless unavoidable; narrow generics whenever possible.
- Choose composition over inheritance; leverage utility types (Partial, Pick, etc.).
⸻
4. Testing Strategy
- Co‑locate tests: foo.ts ↔ foo.test.ts.
- Use real function invocations with stub data; avoid brittle mocks.
- For network interactions, use Mock Service Worker (MSW) to intercept fetch/HTTP calls, mocking only scenarios that are hard to reproduce.
- Keep tests deterministic and side‑effect free; Jest is pre‑configured.
⸻
5. packages/thirdweb
5.1 Public API Surface
- Export everything via the exports/ directory, grouped by feature.
- Every public symbol must have comprehensive TSDoc:
- Include at least one @example block that compiles.
- Tag with one custom annotation (@beta, @internal, @experimental, etc.).
- Comment only ambiguous logic; avoid restating TypeScript in prose.
5.2 Performance
- Lazy‑load heavy dependencies inside async paths to keep the initial bundle lean:
`const { jsPDF } = await import("jspdf");`
⸻
6. apps/dashboard & apps/playground
6.1 Core UI Toolkit
- Import primitives from @/components/ui/\_ (e.g. Button, Input, Select, Tabs, Card, Sidebar, Badge, Separator).
- Use NavLink for internal navigation so active states are handled automatically.
- Group feature‑specific components under feature/components/\_ and expose a barrel index.ts when necessary.
6.2 Styling Conventions
- Tailwind CSS is the styling system – no inline styles or CSS modules.
- Merge class names with cn() from @/lib/utils to keep conditional logic readable.
- Stick to design tokens: backgrounds (bg-card), borders (border-border), muted text (text-muted-foreground), etc.
- Expose a className prop on the root element of every component for overrides.
6.3 Component Patterns
- Server Components (run on the Node edge):
-- Read cookies/headers with next/headers.
-- Access server‑only environment variables or secrets.
-- Perform heavy data fetching that should not ship to the client.
-- Implement redirect logic with redirect() from next/navigation.
-- Start files with import "server-only"; to prevent client bundling.
- Client Components (run in the browser):
-- Begin files with 'use client'; before imports.
-- Handle interactive UI relying on React hooks (useState, useEffect, React Query, wallet hooks).
-- Access browser APIs (localStorage, window, IntersectionObserver, etc.).
-- Support fast transitions where data is prefetched on the client.
6.4 Data Fetching Guidelines
- Server Side
-- Always call getAuthToken() to retrieve the JWT from cookies.
-- Inject the token as an Authorization: Bearer header – never embed it in the URL.
-- Return typed results (Project[], User[], …) – avoid any.
- Client Side
-- Wrap calls in React Query (@tanstack/react-query).
-- Use descriptive, stable queryKeys for cache hits.
-- Configure staleTime / cacheTime based on freshness requirements (default ≥ 60 s).
-- Keep tokens secret by calling internal API routes or server actions.
⸻
7. Performance & Bundle Size
- Track bundle budgets via package.json#size-limit.
- Lazy‑import optional features; avoid top‑level side‑effects.
- De‑duplicate dependencies across packages through pnpm workspace hoisting.
⸻
8. Documentation & Developer Experience
- Each change in packages/\* should contain a changeset for the appropriate package, with the appropriate version bump
- patch for changes that don't impact the public API
- minor for any new/modified public API
- Surface breaking changes prominently in PR descriptions.
- For new UI components, add Storybook stories (\*.stories.tsx) alongside the code.
+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 --
+22 -22
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",
@@ -71,13 +71,13 @@
"ioredis": "^5.6.1",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.503.0",
"next": "15.3.1",
"next": "15.3.2",
"next-plausible": "^3.12.4",
"next-themes": "^0.4.6",
"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,30 +105,30 @@
"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",
"@chromatic-com/storybook": "3.2.6",
"@next/bundle-analyzer": "15.3.1",
"@next/eslint-plugin-next": "15.3.1",
"@next/bundle-analyzer": "15.3.2",
"@next/eslint-plugin-next": "15.3.2",
"@playwright/test": "1.52.0",
"@storybook/addon-essentials": "8.6.12",
"@storybook/addon-interactions": "8.6.12",
"@storybook/addon-links": "8.6.12",
"@storybook/addon-onboarding": "8.6.12",
"@storybook/addon-viewport": "8.6.12",
"@storybook/blocks": "8.6.12",
"@storybook/nextjs": "8.6.12",
"@storybook/react": "8.6.12",
"@storybook/test": "8.6.12",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "8.6.14",
"@storybook/addon-onboarding": "8.6.14",
"@storybook/addon-viewport": "8.6.14",
"@storybook/blocks": "8.6.14",
"@storybook/nextjs": "8.6.14",
"@storybook/react": "8.6.14",
"@storybook/test": "8.6.14",
"@types/color": "4.2.0",
"@types/node": "22.14.1",
"@types/papaparse": "^5.3.15",
"@types/papaparse": "^5.3.16",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"@types/react": "19.1.4",
"@types/react-dom": "19.1.5",
"@types/react-table": "^7.7.20",
"@types/spdx-correct": "^3.1.3",
"@types/swagger-ui-react": "^5.18.0",
@@ -140,10 +140,10 @@
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
"eslint-plugin-storybook": "0.12.0",
"knip": "5.50.5",
"knip": "5.56.0",
"next-sitemap": "^4.2.3",
"postcss": "8.5.3",
"storybook": "8.6.12",
"storybook": "8.6.14",
"tailwindcss": "3.4.17",
"typescript": "5.8.3"
}
@@ -0,0 +1,10 @@
"use server";
import { revalidatePath } from "next/cache";
export async function revalidatePathAction(
path: string,
type: "page" | "layout",
) {
revalidatePath(path, type);
}
+8 -10
View File
@@ -48,15 +48,15 @@ async function fetchAnalytics(
);
}
// client id DEBUG OVERRIDE
// ANALYTICS_SERVICE_URL.searchParams.delete("projectId");
// ANALYTICS_SERVICE_URL.searchParams.delete("teamId");
// ANALYTICS_SERVICE_URL.searchParams.append(
// analyticsServiceUrl.searchParams.delete("projectId");
// analyticsServiceUrl.searchParams.delete("teamId");
// analyticsServiceUrl.searchParams.append(
// "teamId",
// "team_clmb33q9w00gn1x0u2ri8z0k0",
// "team_cm0lde33r02344w129k5hm2xz",
// );
// ANALYTICS_SERVICE_URL.searchParams.append(
// analyticsServiceUrl.searchParams.append(
// "projectId",
// "prj_clyqwud5y00u1na7nzxnzlz7o",
// "prj_cm4rqwx9b002qrnsnr37wqpo6",
// );
return fetch(analyticsServiceUrl, {
@@ -377,7 +377,7 @@ export async function getEcosystemWalletUsage(args: {
export async function getUniversalBridgeUsage(args: {
teamId: string;
projectId: string;
projectId?: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
@@ -395,11 +395,10 @@ export async function getUniversalBridgeUsage(args: {
console.error(
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
);
return null;
return [];
}
const json = await res.json();
return json.data as UniversalBridgeStats[];
}
@@ -430,6 +429,5 @@ export async function getUniversalBridgeWalletUsage(args: {
}
const json = await res.json();
return json.data as UniversalBridgeWalletStats[];
}
@@ -0,0 +1,207 @@
/* 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(
`https://${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(
`https://${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(
`https://${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(
`https://${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"}`,
};
}
}
+2 -2
View File
@@ -84,7 +84,7 @@ export async function getDefaultTeam() {
return null;
}
export async function getLastVisitedTeamOrDefaultTeam() {
export async function getLastVisitedTeam() {
const token = await getAuthToken();
if (!token) {
return null;
@@ -100,5 +100,5 @@ export async function getLastVisitedTeamOrDefaultTeam() {
}
}
return getDefaultTeam();
return null;
}
@@ -0,0 +1,47 @@
import "server-only";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { UB_BASE_URL } from "./constants";
type PaymentLink = {
clientId: string;
title?: string;
imageUrl?: string;
receiver: string;
destinationToken: {
address: string;
symbol: string;
decimals: number;
chainId: number;
};
amount: bigint | undefined;
purchaseData: unknown;
};
export async function getPaymentLink(props: {
paymentId: string;
}) {
const res = await fetch(`${UB_BASE_URL}/v1/links/${props.paymentId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
},
});
if (!res.ok) {
const text = await res.text();
throw new Error(text);
}
const { data } = await res.json();
return {
clientId: data.clientId,
title: data.title,
imageUrl: data.imageUrl,
receiver: data.receiver,
destinationToken: data.destinationToken,
amount: data.amount ? BigInt(data.amount) : undefined,
purchaseData: data.purchaseData,
} as PaymentLink;
}
@@ -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>;
}
@@ -71,7 +71,7 @@ export function MultiNetworkSelector(props: {
return false;
}
if (Number.isInteger(Number.parseInt(searchValue))) {
if (Number.isInteger(Number(searchValue))) {
return String(chain.chainId).startsWith(searchValue);
}
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
@@ -181,7 +181,7 @@ export function SingleNetworkSelector(props: {
return false;
}
if (Number.isInteger(Number.parseInt(searchValue))) {
if (Number.isInteger(Number(searchValue))) {
return String(chain.chainId).startsWith(searchValue);
}
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
@@ -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>
);
}
@@ -137,7 +137,7 @@ function RenderSidebarGroup(props: {
}
if ("separator" in link) {
return <SidebarSeparator className="my-1" />;
return <SidebarSeparator key={Math.random()} className="my-1" />;
}
return (
@@ -35,14 +35,28 @@ function Variant(props: {
label: string;
selectedChainId?: number;
}) {
const [tokenAddress, setTokenAddress] = useState<string>("");
const [token, setToken] = useState<
| {
address: string;
chainId: number;
}
| undefined
>(undefined);
return (
<BadgeContainer label={props.label}>
<TokenSelector
tokenAddress={tokenAddress}
addNativeTokenIfMissing={false}
showCheck={false}
selectedToken={token}
chainId={props.selectedChainId}
client={storybookThirdwebClient}
onChange={setTokenAddress}
onChange={(v) => {
setToken({
address: v.address,
chainId: v.chainId,
});
}}
/>
</BadgeContainer>
);
@@ -1,9 +1,15 @@
import { useCallback, useMemo } from "react";
import type { ThirdwebClient } from "thirdweb";
import {
NATIVE_TOKEN_ADDRESS,
type ThirdwebClient,
getAddress,
} from "thirdweb";
import { shortenAddress } from "thirdweb/utils";
import { useAllChainsData } from "../../../hooks/chains/allChains";
import { useTokensData } from "../../../hooks/tokens/tokens";
import { replaceIpfsUrl } from "../../../lib/sdk";
import { fallbackChainIcon } from "../../../utils/chain-icons";
import type { TokenMetadata } from "../../api/universal-bridge/tokens";
import { cn } from "../../lib/utils";
import { Badge } from "../ui/badge";
import { Img } from "./Img";
@@ -11,42 +17,92 @@ import { SelectWithSearch } from "./select-with-search";
type Option = { label: string; value: string };
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
export function TokenSelector(props: {
tokenAddress: string | undefined;
onChange: (tokenAddress: string) => void;
selectedToken: { chainId: number; address: string } | undefined;
onChange: (token: TokenMetadata) => void;
className?: string;
popoverContentClassName?: string;
chainId?: number;
side?: "left" | "right" | "top" | "bottom";
disableChainId?: boolean;
disableAddress?: boolean;
align?: "center" | "start" | "end";
placeholder?: string;
client: ThirdwebClient;
disabled?: boolean;
enabled?: boolean;
showCheck: boolean;
addNativeTokenIfMissing: boolean;
}) {
const { tokens, isFetching } = useTokensData({
const tokensQuery = useTokensData({
chainId: props.chainId,
enabled: props.enabled,
});
const { idToChain } = useAllChainsData();
const tokens = useMemo(() => {
if (!tokensQuery.data) {
return [];
}
if (props.addNativeTokenIfMissing) {
const hasNativeToken = tokensQuery.data.some(
(token) => token.address === checksummedNativeTokenAddress,
);
if (!hasNativeToken && props.chainId) {
return [
{
name:
idToChain.get(props.chainId)?.nativeCurrency.name ??
"Native Token",
symbol:
idToChain.get(props.chainId)?.nativeCurrency.symbol ?? "ETH",
decimals: 18,
chainId: props.chainId,
address: checksummedNativeTokenAddress,
} satisfies TokenMetadata,
...tokensQuery.data,
];
}
}
return tokensQuery.data;
}, [
tokensQuery.data,
props.chainId,
props.addNativeTokenIfMissing,
idToChain,
]);
const addressChainToToken = useMemo(() => {
const value = new Map<string, TokenMetadata>();
for (const token of tokens) {
value.set(`${token.chainId}:${token.address}`, token);
}
return value;
}, [tokens]);
const options = useMemo(() => {
return tokens.allTokens.map((token) => {
return {
label: token.symbol,
value: `${token.chainId}:${token.address}`,
};
});
}, [tokens.allTokens]);
return (
tokens.map((token) => {
return {
label: token.symbol,
value: `${token.chainId}:${token.address}`,
};
}) || []
);
}, [tokens]);
const searchFn = useCallback(
(option: Option, searchValue: string) => {
const token = tokens.addressChainToToken.get(option.value);
const token = addressChainToToken.get(option.value);
if (!token) {
return false;
}
if (Number.isInteger(Number.parseInt(searchValue))) {
if (Number.isInteger(Number(searchValue))) {
return String(token.chainId).startsWith(searchValue);
}
return (
@@ -55,12 +111,12 @@ export function TokenSelector(props: {
token.address.toLowerCase().includes(searchValue.toLowerCase())
);
},
[tokens],
[addressChainToToken],
);
const renderOption = useCallback(
(option: Option) => {
const token = tokens.addressChainToToken.get(option.value);
const token = addressChainToToken.get(option.value);
if (!token) {
return option.label;
}
@@ -87,8 +143,8 @@ export function TokenSelector(props: {
{token.symbol}
</span>
{!props.disableChainId && (
<Badge variant="outline" className="gap-2 max-sm:hidden">
{!props.disableAddress && (
<Badge variant="outline" className="gap-2 py-1 max-sm:hidden">
<span className="text-muted-foreground">Address</span>
{shortenAddress(token.address, 4)}
</Badge>
@@ -96,27 +152,37 @@ export function TokenSelector(props: {
</div>
);
},
[tokens, props.disableChainId, props.client],
[addressChainToToken, props.disableAddress, props.client],
);
const selectedValue = props.selectedToken
? `${props.selectedToken.chainId}:${props.selectedToken.address}`
: undefined;
return (
<SelectWithSearch
searchPlaceholder="Search by name or symbol"
value={props.tokenAddress}
value={selectedValue}
options={options}
onValueChange={(tokenAddress) => {
props.onChange(tokenAddress);
const token = addressChainToToken.get(tokenAddress);
if (!token) {
return;
}
props.onChange(token);
}}
closeOnSelect={true}
showCheck={false}
showCheck={props.showCheck}
placeholder={
isFetching ? "Loading Tokens..." : props.placeholder || "Select Token"
tokensQuery.isPending
? "Loading Tokens..."
: props.placeholder || "Select Token"
}
overrideSearchFn={searchFn}
renderOption={renderOption}
className={props.className}
popoverContentClassName={props.popoverContentClassName}
disabled={isFetching || props.disabled}
disabled={tokensQuery.isPending || props.disabled}
side={props.side}
align={props.align}
/>
@@ -0,0 +1,75 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ArrowRightIcon, RocketIcon, StarIcon } from "lucide-react";
import { BadgeContainer } from "../../../stories/utils";
import { UpsellBannerCard } from "./UpsellBannerCard";
function Story() {
return (
<div className="container flex max-w-4xl flex-col gap-8 py-10">
<BadgeContainer label="Green with icon (default)">
<UpsellBannerCard
title="Unlock more with thirdweb"
description="Upgrade to increase limits and access advanced features."
cta={{
text: "View plans",
icon: <ArrowRightIcon className="size-4" />,
link: "#",
}}
trackingCategory="storybook"
trackingLabel="green"
icon={<RocketIcon className="size-5" />}
accentColor="green"
/>
</BadgeContainer>
<BadgeContainer label="Blue accent">
<UpsellBannerCard
title="Need more storage?"
description="Add additional space to your account."
cta={{
text: "Upgrade storage",
icon: <ArrowRightIcon className="size-4" />,
link: "#",
}}
trackingCategory="storybook"
trackingLabel="blue"
icon={<StarIcon className="size-5" />}
accentColor="blue"
/>
</BadgeContainer>
<BadgeContainer label="Purple without leading icon">
<UpsellBannerCard
title="Join the beta"
description="Get early access to experimental features."
cta={{
text: "Request access",
icon: <ArrowRightIcon className="size-4" />,
link: "#",
}}
trackingCategory="storybook"
trackingLabel="purple"
accentColor="purple"
/>
</BadgeContainer>
</div>
);
}
const meta = {
title: "blocks/Banners/UpsellBannerCard",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Variants: Story = {
args: {},
};
@@ -0,0 +1,119 @@
"use client";
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { cn } from "@/lib/utils";
import type React from "react";
const ACCENT = {
green: {
border: "border-green-600 dark:border-green-700",
bgFrom: "from-green-50 dark:from-green-900/20",
blur: "bg-green-600",
title: "text-green-900 dark:text-green-200",
desc: "text-green-800 dark:text-green-300",
iconBg: "bg-green-600 text-white",
btn: "bg-green-600 text-white hover:bg-green-700",
},
blue: {
border: "border-blue-600 dark:border-blue-700",
bgFrom: "from-blue-50 dark:from-blue-900/20",
blur: "bg-blue-600",
title: "text-blue-900 dark:text-blue-200",
desc: "text-blue-800 dark:text-blue-300",
iconBg: "bg-blue-600 text-white",
btn: "bg-blue-600 text-white hover:bg-blue-700",
},
purple: {
border: "border-purple-600 dark:border-purple-700",
bgFrom: "from-purple-50 dark:from-purple-900/20",
blur: "bg-purple-600",
title: "text-purple-900 dark:text-purple-200",
desc: "text-purple-800 dark:text-purple-300",
iconBg: "bg-purple-600 text-white",
btn: "bg-purple-600 text-white hover:bg-purple-700",
},
} as const;
type UpsellBannerCardProps = {
title: React.ReactNode;
description: React.ReactNode;
cta: {
text: React.ReactNode;
icon?: React.ReactNode;
link: string;
};
trackingCategory: string;
trackingLabel: string;
accentColor?: keyof typeof ACCENT;
icon?: React.ReactNode;
};
export function UpsellBannerCard(props: UpsellBannerCardProps) {
const color = ACCENT[props.accentColor || "green"];
return (
<div
className={cn(
"relative overflow-hidden rounded-lg border bg-gradient-to-r p-5",
color.border,
color.bgFrom,
)}
>
{/* Decorative blur */}
<div
className={cn(
"-right-10 -top-10 pointer-events-none absolute size-28 rounded-full opacity-20 blur-2xl",
color.blur,
)}
/>
<div className="relative flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-start gap-3">
{props.icon ? (
<div
className={cn(
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
color.iconBg,
)}
>
{props.icon}
</div>
) : null}
<div>
<h3
className={cn(
"font-semibold text-lg tracking-tight",
color.title,
)}
>
{props.title}
</h3>
<p className={cn("mt-0.5 text-sm", color.desc)}>
{props.description}
</p>
</div>
</div>
<Button
asChild
size="sm"
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
>
<TrackedLinkTW
href={props.cta.link}
category={props.trackingCategory}
label={props.trackingLabel}
>
{props.cta.text}
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
</TrackedLinkTW>
</Button>
</div>
</div>
);
}
@@ -0,0 +1,36 @@
"use client";
import { XIcon } from "lucide-react";
import { useLocalStorage } from "../../../hooks/useLocalStorage";
import { Button } from "../ui/button";
export function DismissibleAlert(props: {
title: React.ReactNode;
description: React.ReactNode;
localStorageId: string;
}) {
const [isVisible, setIsVisible] = useLocalStorage(
props.localStorageId,
true,
false,
);
if (!isVisible) return null;
return (
<div className="relative rounded-lg border border-border bg-card p-4">
<Button
onClick={() => setIsVisible(false)}
className="absolute top-4 right-4 h-auto w-auto p-1 text-muted-foreground"
aria-label="Close alert"
variant="ghost"
>
<XIcon className="size-5" />
</Button>
<div>
<h2 className="mb-0.5 font-semibold">{props.title} </h2>
<div className="text-muted-foreground text-sm">{props.description}</div>
</div>
</div>
);
}
@@ -0,0 +1,71 @@
import { cn } from "@/lib/utils";
export type Segment = {
label: string;
percent: number;
color: string;
};
type DistributionBarChartProps = {
segments: Segment[];
title: string;
};
export function DistributionBarChart(props: DistributionBarChartProps) {
const totalPercentage = props.segments.reduce(
(sum, segment) => sum + segment.percent,
0,
);
const invalidTotalPercentage = totalPercentage !== 100;
return (
<div>
<div className="mb-2 flex items-center justify-between">
<h3 className="font-medium text-sm">{props.title}</h3>
<div
className={cn(
"font-medium text-muted-foreground text-sm",
invalidTotalPercentage && "text-red-500",
)}
>
Total: {totalPercentage}%
</div>
</div>
{/* Bar */}
<div className="flex h-3 overflow-hidden rounded-lg">
{props.segments.map((segment) => {
return (
<div
key={segment.label}
className="flex h-full items-center justify-center transition-all duration-200"
style={{
width: `${segment.percent}%`,
backgroundColor: segment.color,
}}
/>
);
})}
</div>
{/* Legends */}
<div className="mt-3 flex flex-col gap-1 lg:flex-row lg:gap-6">
{props.segments.map((segment) => {
return (
<div key={segment.label} className="flex items-center gap-1.5">
<div
className="size-3 rounded-full"
style={{
backgroundColor: segment.color,
}}
/>
<p className="text-sm">
{segment.label}: {segment.percent}%
</p>
</div>
);
})}
</div>
</div>
);
}
@@ -0,0 +1,66 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MultiStepStatus } from "./multi-step-status";
const meta = {
title: "Blocks/MultiStepStatus",
component: MultiStepStatus,
decorators: [
(Story) => (
<div className="container w-full max-w-md py-10">
<Story />
</div>
),
],
} satisfies Meta<typeof MultiStepStatus>;
export default meta;
type Story = StoryObj<typeof meta>;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const AllStates: Story = {
args: {
steps: [
{
status: "completed",
label: "Connect Wallet",
retryLabel: "Failed to connect wallet",
execute: async () => {
await sleep(1000);
},
},
{
status: "pending",
label: "Sign Message",
retryLabel: "Failed to sign message",
execute: async () => {
await sleep(1000);
},
},
{
status: "error",
label: "Approve Transaction",
retryLabel: "Transaction approval failed",
execute: async () => {
await sleep(1000);
},
},
{
status: "idle",
label: "Confirm Transaction",
retryLabel: "Transaction confirmation failed",
execute: async () => {
await sleep(1000);
},
},
{
status: "idle",
label: "Finalize",
retryLabel: "Finalization failed",
execute: async () => {
await sleep(1000);
},
},
],
},
};
@@ -0,0 +1,72 @@
"use client";
import { Button } from "@/components/ui/button";
import {
AlertCircleIcon,
CircleCheckIcon,
CircleIcon,
RefreshCwIcon,
} from "lucide-react";
import { DynamicHeight } from "../../ui/DynamicHeight";
import { Spinner } from "../../ui/Spinner/Spinner";
export type MultiStepState = {
status: "idle" | "pending" | "completed" | "error";
retryLabel: string;
label: string;
execute: () => Promise<void>;
};
export function MultiStepStatus(props: {
steps: MultiStepState[];
}) {
return (
<DynamicHeight>
<div className="space-y-4">
{props.steps.map((step) => (
<div key={step.label} className="flex items-start space-x-3 ">
{step.status === "completed" ? (
<CircleCheckIcon className="mt-0.5 size-5 flex-shrink-0 text-green-500" />
) : step.status === "pending" ? (
<Spinner className="mt-0.5 size-5 flex-shrink-0 text-foreground" />
) : step.status === "error" ? (
<AlertCircleIcon className="mt-0.5 size-5 flex-shrink-0 text-red-500" />
) : (
<CircleIcon className="mt-0.5 size-5 flex-shrink-0 text-muted-foreground/70" />
)}
<div className="flex-1">
<p
className={`font-medium ${
step.status === "pending"
? "text-foreground"
: step.status === "completed"
? "text-green-500"
: step.status === "error"
? "text-red-500"
: "text-muted-foreground/70"
}`}
>
{step.label}
</p>
{step.status === "error" && (
<div className="mt-1 space-y-2">
<p className="mb-1 text-red-500 text-sm">{step.retryLabel}</p>
<Button
variant="destructive"
size="sm"
className="gap-2"
onClick={() => step.execute()}
>
<RefreshCwIcon className="size-4" />
Retry
</Button>
</div>
)}
</div>
</div>
))}
</div>
</DynamicHeight>
);
}
@@ -42,7 +42,7 @@ export function CopyTextButton(props: {
variant={props.variant || "outline"}
aria-label={props.tooltip}
className={cn(
"flex h-auto w-auto gap-2 rounded-lg px-2.5 py-1.5 font-normal text-foreground",
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground",
props.className,
)}
onClick={(e) => {
@@ -12,6 +12,7 @@ export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
isCurrency?: boolean;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
@@ -11,6 +11,7 @@ interface ImageUploadProps {
variant?: React.ComponentProps<typeof Button>["variant"];
className?: string;
multiple?: boolean;
accept: string;
}
export function ImageUploadButton(props: ImageUploadProps) {
@@ -34,7 +35,7 @@ export function ImageUploadButton(props: ImageUploadProps) {
ref={fileInputRef}
type="file"
multiple={props.multiple}
accept="image/*"
accept={props.accept}
onChange={handleFileChange}
className="hidden"
aria-label="Upload image"
@@ -54,11 +54,11 @@ export function ToolTipLabel(props: {
align={props.align}
sideOffset={10}
className={cn(
"max-w-[400px] whitespace-normal leading-relaxed",
"max-w-[400px] whitespace-normal p-0 leading-relaxed",
props.contentClassName,
)}
>
<div className="flex items-center gap-1.5 p-2 text-sm">
<div className="flex items-center gap-1.5 p-4 text-sm">
{props.leftIcon}
{props.label}
{props.rightIcon}
@@ -36,14 +36,6 @@ if (API_SERVER_SECRET) {
export const THIRDWEB_ENGINE_URL = process.env.THIRDWEB_ENGINE_URL || "";
if (THIRDWEB_ENGINE_URL) {
experimental_taintUniqueValue(
"Do not pass THIRDWEB_ENGINE_URL to the client",
process,
THIRDWEB_ENGINE_URL,
);
}
export const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN || "";
if (THIRDWEB_ACCESS_TOKEN) {
@@ -3,6 +3,7 @@ import {
NEXT_PUBLIC_IPFS_GATEWAY_URL,
} from "@/constants/public-envs";
import {
THIRDWEB_BRIDGE_URL,
THIRDWEB_BUNDLER_DOMAIN,
THIRDWEB_INAPP_WALLET_DOMAIN,
THIRDWEB_INSIGHT_API_DOMAIN,
@@ -35,6 +36,7 @@ export function getConfiguredThirdwebClient(options: {
social: THIRDWEB_SOCIAL_API_DOMAIN,
bundler: THIRDWEB_BUNDLER_DOMAIN,
insight: THIRDWEB_INSIGHT_API_DOMAIN,
bridge: THIRDWEB_BRIDGE_URL,
});
}
+5 -4
View File
@@ -39,9 +39,10 @@ export function getFiltersFromSearchParams(params: {
};
const defaultInterval =
differenceInCalendarDays(range.to, range.from) > 30
? "week"
: ("day" as const);
params.interval ??
(differenceInCalendarDays(range.to, range.from) > 30
? ("week" as const)
: ("day" as const));
return {
range,
@@ -50,6 +51,6 @@ export function getFiltersFromSearchParams(params: {
? ("day" as const)
: params.interval === "week"
? ("week" as const)
: defaultInterval,
: (defaultInterval as "day" | "week"),
};
}
@@ -32,7 +32,6 @@ export type Account = {
image?: string | null;
name?: string;
email?: string;
advancedEnabled: boolean;
emailConfirmedAt?: string;
unconfirmedEmail?: string;
emailConfirmationWalletAddress?: string;
@@ -41,7 +40,6 @@ export type Account = {
billing: "email" | "none";
updates: "email" | "none";
};
// TODO - add image URL
};
interface UpdateAccountNotificationsInput {
@@ -53,27 +51,6 @@ interface ConfirmEmailInput {
confirmationToken: string;
}
interface UsageStorage {
sumFileSizeBytes: number;
}
export interface UsageBillableByService {
usage: {
storage: UsageStorage;
};
limits: {
storage: number;
};
rateLimits: {
storage: number;
rpc: number;
};
rateLimitedAt: {
storage?: string;
rpc?: string;
};
}
interface BillingProduct {
name: string;
id: string;
@@ -345,13 +322,14 @@ export type RotateSecretKeyAPIReturnType = {
};
};
export async function rotateSecretKeyClient(projectId: string) {
export async function rotateSecretKeyClient(params: {
teamId: string;
projectId: string;
}) {
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
pathname: "/v2/keys/rotate-secret-key",
pathname: `/v1/teams/${params.teamId}/projects/${params.projectId}/rotate-secret-key`,
method: "POST",
body: JSON.stringify({
projectId,
}),
body: JSON.stringify({}),
headers: {
"Content-Type": "application/json",
},
@@ -11,8 +11,8 @@ import type { EngineBackendWalletType } from "lib/engine";
import { useState } from "react";
import { useActiveAccount } from "thirdweb/react";
import invariant from "tiny-invariant";
import type { ResultItem } from "../../../app/(app)/team/[team_slug]/[project_slug]/engine/dedicated/(instance)/[engineId]/metrics/components/StatusCodes";
import type { EngineStatus } from "../../../app/(app)/team/[team_slug]/[project_slug]/engine/dedicated/(instance)/[engineId]/overview/components/transactions-table";
import type { ResultItem } from "../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/metrics/components/StatusCodes";
import type { EngineStatus } from "../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/overview/components/transactions-table";
import { engineKeys } from "../cache-keys";
// Engine instances
@@ -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,40 +0,0 @@
import type { Metadata } from "next";
import { getClientThirdwebClient } from "../../../../../@/constants/thirdweb-client.client";
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
const title = "Universal Bridge: Swap, Bridge, and On-Ramp";
const description =
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Universal Bridge.";
export const metadata: Metadata = {
title,
description,
openGraph: {
title,
description,
},
};
export default async function RoutesPage({
searchParams,
}: { searchParams: Record<string, string | string[]> }) {
const { chainId } = searchParams;
const client = getClientThirdwebClient(undefined);
return (
<div className="relative mx-auto flex h-screen w-full flex-col items-center justify-center overflow-hidden border py-10">
<main className="container z-10 flex justify-center">
<UniversalBridgeEmbed
chainId={chainId ? Number(chainId) : undefined}
client={client}
/>
</main>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt=""
src="/assets/login/background.svg"
className="-bottom-12 -right-12 pointer-events-none absolute lg:right-0 lg:bottom-0"
/>
</div>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

@@ -47,7 +47,7 @@ export const SearchInput: React.FC = () => {
<div className="group relative w-full">
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
<Input
placeholder="Search with anything!"
placeholder="Search by token name or symbol"
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
defaultValue={searchParams?.get("query") || ""}
onChange={(e) => handleSearch(e.target.value)}
@@ -52,20 +52,20 @@ export async function RouteListRow({
]);
return (
<TableRow linkBox className="hover:bg-accent/50">
<TableRow linkBox className="group transition-colors hover:bg-accent/50">
<TableCell>
<div className="flex flex-row items-center gap-4">
<div className="flex items-center gap-1">
<div className="flex flex-row items-center gap-3">
<div className="flex items-center gap-2">
{resolvedOriginTokenIconUri ? (
// For now we're using a normal img tag because the domain for these images is unknown
// eslint-disable-next-line @next/next/no-img-element
<img
src={resolvedOriginTokenIconUri}
alt={originTokenAddress}
className="size-6 rounded-full border border-muted-foreground"
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
/>
) : (
<div className="size-6 rounded-full bg-muted-foreground" />
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{originTokenSymbol && (
<CopyTextButton
@@ -76,7 +76,7 @@ export async function RouteListRow({
: originTokenSymbol
}
tooltip="Copy Token Address"
className="relative z-10 text-base"
className="relative z-10 font-medium text-base"
variant="ghost"
copyIconPosition="right"
/>
@@ -85,22 +85,22 @@ export async function RouteListRow({
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{originChain.name}
<TableCell className="text-muted-foreground/90">
<span className="font-medium">{originChain.name}</span>
</TableCell>
<TableCell>
<div className="flex flex-row items-center gap-4">
<div className="flex items-center gap-1">
<div className="flex flex-row items-center gap-3">
<div className="flex items-center gap-2">
{resolvedDestinationTokenIconUri ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={resolvedDestinationTokenIconUri}
alt={destinationTokenAddress}
className="size-6 rounded-full border border-muted-foreground"
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
/>
) : (
<div className="size-6 rounded-full bg-muted-foreground" />
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{destinationTokenSymbol && (
<CopyTextButton
@@ -111,7 +111,7 @@ export async function RouteListRow({
: destinationTokenSymbol
}
tooltip="Copy Token Address"
className="relative z-10 text-base"
className="relative z-10 font-medium text-base"
variant="ghost"
copyIconPosition="right"
/>
@@ -120,8 +120,8 @@ export async function RouteListRow({
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{destinationChain.name}
<TableCell className="text-muted-foreground/90">
<span className="font-medium">{destinationChain.name}</span>
</TableCell>
</TableRow>
);
@@ -7,7 +7,6 @@ import {
TableRow,
} from "@/components/ui/table";
import type { Address } from "thirdweb";
import { checksumAddress } from "thirdweb/utils";
import { getRoutes } from "../../../utils";
import { ChainlistPagination } from "../client/pagination";
import { RouteListCard } from "./routelist-card";
@@ -25,18 +24,15 @@ export type SearchParams = Partial<{
// 120 is divisible by 2, 3, and 4 so card layout looks nice
const DEFAULT_PAGE_SIZE = 120;
const DEFAULT_PAGE = 1;
async function getRoutesToRender(params: SearchParams) {
const filters: Partial<{
limit: number;
offset: number;
originQuery?: string;
destinationQuery?: string;
originChainId?: number;
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
originTextQuery?: string;
destinationTextQuery?: string;
}> = {};
if (params.type === "origin" || typeof params.type === "undefined") {
@@ -45,7 +41,7 @@ async function getRoutesToRender(params: SearchParams) {
} else if (Number.isInteger(Number(params.query))) {
filters.originChainId = Number(params.query);
} else if (params.query) {
filters.originTextQuery = params.query;
filters.originQuery = params.query;
}
} else if (params.type === "destination") {
if (params.query?.startsWith("0x")) {
@@ -53,69 +49,20 @@ async function getRoutesToRender(params: SearchParams) {
} else if (Number.isInteger(Number(params.query))) {
filters.destinationChainId = Number(params.query);
} else if (params.query) {
filters.destinationTextQuery = params.query;
filters.destinationQuery = params.query;
}
}
// Temporary, will update this after the /routes endpoint
let routes = await getRoutes({ limit: 100_000 });
const totalCount = routes.length;
if (filters.originChainId) {
routes = routes.filter(
(route) => route.originToken.chainId === filters.originChainId,
);
}
if (filters.originTokenAddress) {
const originTokenAddress = filters.originTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.originToken.address) ===
checksumAddress(originTokenAddress),
);
}
if (filters.destinationChainId) {
routes = routes.filter(
(route) => route.destinationToken.chainId === filters.destinationChainId,
);
}
if (filters.destinationTokenAddress) {
const destinationTokenAddress = filters.destinationTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.destinationToken.address) ===
checksumAddress(destinationTokenAddress),
);
}
if (filters.originTextQuery) {
const originTextQuery = filters.originTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.originToken.name.toLowerCase().includes(originTextQuery) ||
route.originToken.symbol.toLowerCase().includes(originTextQuery)
);
});
}
if (filters.destinationTextQuery) {
const destinationTextQuery = filters.destinationTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.destinationToken.name
.toLowerCase()
.includes(destinationTextQuery) ||
route.destinationToken.symbol
.toLowerCase()
.includes(destinationTextQuery)
);
});
}
const routes = await getRoutes({
limit: DEFAULT_PAGE_SIZE,
offset: DEFAULT_PAGE_SIZE * ((params.page || 1) - 1),
originQuery: filters.originQuery,
destinationQuery: filters.destinationQuery,
});
return {
routesToRender: routes,
totalCount,
filteredCount: routes.length,
routesToRender: routes.data,
totalCount: routes.meta.totalCount,
filteredCount: routes.meta.filteredCount,
};
}
@@ -128,36 +75,37 @@ export async function RoutesData(props: {
props.searchParams,
);
// pagination
const totalPages = Math.ceil(routesToRender.length / DEFAULT_PAGE_SIZE);
const activePage = Number(props.searchParams.page || DEFAULT_PAGE);
const pageSize = DEFAULT_PAGE_SIZE;
const startIndex = (activePage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedRoutes = routesToRender.slice(startIndex, endIndex);
const totalPages = Math.ceil(filteredCount / DEFAULT_PAGE_SIZE);
return (
<>
<main>
{/* empty state */}
{paginatedRoutes.length === 0 ? (
{routesToRender.length === 0 ? (
<div className="flex h-[300px] items-center justify-center rounded-lg border p-8 lg:h-[500px]">
<p className="text-2xl">No Results found</p>
</div>
) : props.activeView === "table" ? (
<TableContainer>
<TableContainer className="overflow-hidden rounded-xl border border-border/50 bg-card/50 shadow-sm transition-all">
<Table>
<TableHeader className="z-0">
<TableRow>
<TableHead>Origin Token</TableHead>
<TableHead>Origin Chain</TableHead>
<TableHead>Destination Token</TableHead>
<TableHead>Destination Chain</TableHead>
<TableRow className="border-border/50 border-b bg-muted/50">
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Origin Token
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Origin Chain
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Destination Token
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Destination Chain
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedRoutes.map((route) => (
{routesToRender.map((route) => (
<RouteListRow
key={`${route.originToken.chainId}:${route.originToken.address}-${route.destinationToken.chainId}:${route.destinationToken.address}`}
originChainId={route.originToken.chainId}
@@ -177,7 +125,7 @@ export async function RoutesData(props: {
</TableContainer>
) : (
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{paginatedRoutes.map((route) => (
{routesToRender.map((route) => (
<li
key={`${route.originToken.chainId}:${route.originToken.address}-${route.destinationToken.chainId}:${route.destinationToken.address}`}
className="h-full"
@@ -201,12 +149,15 @@ export async function RoutesData(props: {
</main>
<div className="h-10" />
{totalPages > 1 && (
<ChainlistPagination totalPages={totalPages} activePage={activePage} />
<ChainlistPagination
totalPages={totalPages}
activePage={Number(props.searchParams.page) || 1}
/>
)}
<div className="h-4" />
<p className="text-balance text-center text-muted-foreground text-sm">
Showing{" "}
<span className="text-accent-foreground">{paginatedRoutes.length}</span>{" "}
<span className="text-accent-foreground">{routesToRender.length}</span>{" "}
out of{" "}
{filteredCount !== totalCount ? (
<>
@@ -1,3 +1,4 @@
import { ArrowUpRightIcon } from "lucide-react";
import type { Metadata } from "next";
import { headers } from "next/headers";
import { getAuthToken } from "../../../api/lib/getAuthToken";
@@ -9,7 +10,7 @@ import {
type SearchParams,
} from "./components/server/routes-table";
const title = "Routes: Swap, Bridge, and On-Ramp";
const title = "Universal Bridge Routes: Swap, Bridge, and Onramp";
const description =
"A list of token routes for swapping, bridging, and on-ramping between EVM chains with thirdweb.";
@@ -43,15 +44,15 @@ export default async function RoutesPage(props: {
return (
<section className="container mx-auto flex h-full flex-col px-4 py-10">
<header className="flex flex-col gap-4">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-row items-center justify-between gap-4 lg:flex-col lg:justify-start">
<header className="flex flex-col gap-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-col gap-2">
<h1 className="font-semibold text-4xl tracking-tighter lg:text-5xl">
Routes
</h1>
</div>
<div className="flex flex-row items-end gap-4 lg:flex-col ">
<div className="flex w-full flex-row gap-4">
<div className="flex flex-row items-end gap-4 lg:flex-col">
<div className="flex w-full flex-row items-center gap-4">
<SearchInput />
<QueryType activeType={activeType} />
<RouteListView activeView={activeView} />
@@ -60,6 +61,29 @@ export default async function RoutesPage(props: {
</div>
</header>
<div className="h-10" />
<div className="relative overflow-hidden rounded-lg border-2 border-green-500/20 bg-gradient-to-br from-card/80 to-card/50 p-4 shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.02)]">
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
<div className="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1">
<h3 className="font-medium text-lg">
Get Started with Universal Bridge
</h3>
<p className="text-muted-foreground text-sm">
Simple, instant, and secure payments across any token and chain.
</p>
</div>
<a
href="https://portal.thirdweb.com/pay"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 font-medium text-sm text-white transition-all hover:bg-green-600/90 hover:shadow-sm"
>
Learn More
<ArrowUpRightIcon className="size-4" />
</a>
</div>
</div>
<div className="h-10" />
<RoutesData
searchParams={searchParams}
activeView={activeView}
@@ -2,35 +2,69 @@ import "server-only";
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
import type { Address } from "thirdweb";
import type { Route } from "./types/route";
export async function getRoutes({
limit,
offset,
originQuery,
destinationQuery,
originChainId,
destinationChainId,
originTokenAddress,
destinationTokenAddress,
}: {
limit?: number;
offset?: number;
originQuery?: string;
destinationQuery?: string;
originChainId?: number;
destinationChainId?: number;
originTokenAddress?: Address;
destinationTokenAddress?: Address;
} = {}) {
const url = new URL(`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/routes`);
if (limit) {
url.searchParams.set("limit", limit.toString());
}
if (offset) {
url.searchParams.set("offset", offset.toString());
}
if (originQuery) {
url.searchParams.set("originQuery", originQuery);
}
if (destinationQuery) {
url.searchParams.set("destinationQuery", destinationQuery);
}
if (originChainId) {
url.searchParams.set("originChainId", originChainId.toString());
}
if (destinationChainId) {
url.searchParams.set("destinationChainId", destinationChainId.toString());
}
if (originTokenAddress) {
url.searchParams.set("originTokenAddress", originTokenAddress);
}
if (destinationTokenAddress) {
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
}
url.searchParams.set("sortBy", "popularity");
// It's faster to filter client side, doesn't seem to be a big performance boost to paginate/filter server side
const token = await getAuthToken();
const routesResponse = await fetch(url, {
headers: token
? {
authorization: `Bearer ${token}`,
}
: {
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
},
headers: {
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
},
next: { revalidate: 60 * 60 },
});
if (!routesResponse.ok) {
throw new Error("Failed to fetch routes");
}
const routes: { data: Route[] } = await routesResponse.json();
const routes: {
data: Route[];
meta: { totalCount: number; filteredCount: number };
} = await routesResponse.json();
return routes.data;
return routes;
}
@@ -32,8 +32,8 @@ function useChainStatswithRPC(_rpcUrl: string) {
method: "POST",
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_blockNumber",
params: [],
method: "eth_getBlockByNumber",
params: ["latest", false],
id: 1,
}),
});
@@ -41,9 +41,12 @@ function useChainStatswithRPC(_rpcUrl: string) {
const json = await res.json();
const latency = (performance.now() - startTimeStamp).toFixed(0);
const blockNumber = Number.parseInt(json.result.number, 16);
const blockGasLimit = Number.parseInt(json.result.gasLimit, 16);
return {
latency,
blockNumber: Number.parseInt(json.result, 16),
blockNumber,
blockGasLimit,
};
},
refetchInterval: (query) => {
@@ -115,6 +118,21 @@ export function ChainLiveStats(props: { rpc: string }) {
</div>
)}
</PrimaryInfoItem>
{/* Block Gas Limit */}
<PrimaryInfoItem title="Block Gas Limit" titleIcon={<PulseDot />}>
{stats.isError ? (
<p className="fade-in-0 animate-in text-destructive-text">N/A</p>
) : stats.data ? (
<p className="fade-in-0 animate-in">
{stats.data.blockGasLimit ?? "N/A"}
</p>
) : (
<div className="flex h-[28px] w-[140px] py-1">
<Skeleton className="h-full w-full" />
</div>
)}
</PrimaryInfoItem>
</>
);
}
@@ -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}
@@ -126,8 +126,10 @@ const getClaimConditionTypeFromPhase = (
if (!phase.snapshot) {
return "public";
}
if (phase.snapshot) {
if (
phase.price === "0" &&
typeof phase.snapshot !== "string" &&
phase.snapshot.length === 1 &&
phase.snapshot.some(
@@ -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 />;
}
@@ -11,7 +11,7 @@ export function ConfigureCustomChain(props: {
chainSlug: string;
}) {
const { chainSlug } = props;
const isSlugNumber = Number.isInteger(Number.parseInt(chainSlug));
const isSlugNumber = Number.isInteger(Number(chainSlug));
const [isConfigured, setIsConfigured] = useState(false);
if (isConfigured) {
@@ -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} />
@@ -25,7 +25,9 @@ import { usePathname } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import type { Chain, ThirdwebClient } from "thirdweb";
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/hooks/project-contracts";
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}
@@ -147,6 +161,8 @@ function AddToProjectModalContent(props: {
teamId: params.teamId,
projectId: params.projectId,
chainId: props.chainId,
deploymentType: undefined,
contractType: undefined,
},
{
onSuccess: () => {
@@ -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}
/>
</>
);
}
@@ -29,7 +29,7 @@ export function SingleNetworkSelector(props: {
return false;
}
if (Number.isInteger(Number.parseInt(searchValue))) {
if (Number.isInteger(Number(searchValue))) {
return String(chain.chainId).startsWith(searchValue);
}
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
@@ -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,25 +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 {
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<{
@@ -29,141 +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:`;
const examplePrompts: string[] = [
"What does this contract do?",
"What permissions or roles exist in this contract?",
"Which functions are used the most?",
"Has this contract been used recently?",
"Who are the largest holders/users of this?",
];
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.map((prompt) => ({
title: prompt,
message: prompt,
}))}
/>
{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;
@@ -171,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 }}

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