Compare commits

...
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>GitHub Action
34e6f69568 Version Packages (#3953)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <[email protected]>
2024-08-05 17:04:41 -07:00
Jonas Daniels c9e14e366c Revert "Version Packages (#3950)"
This reverts commit 65eb299eb0.
2024-08-05 16:24:13 -07:00
Jonas DanielsandGitHub 4308c4a1cf [Chore] Update thirdweb dependency to use workspace protocol (#3952) 2024-08-05 16:20:44 -07:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
65eb299eb0 Version Packages (#3950)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-05 16:18:53 -07:00
gregfromstl 1309602889 docs: adds telegram changeset (#3951)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a new feature to the `thirdweb` module, enabling users to log in to in-app wallets using Telegram.

### Detailed summary
- Added support for Telegram login option in in-app wallets using `thirdweb`
- Updated `inAppWallet` function to include Telegram login strategy
- Added example code snippet for connecting to wallet using Telegram strategy

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

<!-- end pr-codex -->
2024-08-05 22:54:17 +00:00
GWSzeto 09ebee8e41 added build back into the docs (#3949)
Modified .gitignore so that it accepts the build folder

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add detailed documentation for various ERC standards in smart contracts.

### Detailed summary
- Added documentation for ERC721, ERC1155, and ERC20 smart contract standards
- Included usage examples and base contracts for each extension
- Updated metadata and images for clarity and consistency

> The following files were skipped due to too many changes: `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155Mintable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721Mintable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155Enumerable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721Revealable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155Revealable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721Enumerable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155Burnable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721BatchMintable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-20/ERC20BatchMintable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721Supply/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155Supply/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155Claimable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155BatchMintable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721Claimable/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-20/ERC20ClaimConditions/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-20/ERC20/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721ClaimConditions/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-20/base/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/Multicall/page.mdx`, `apps/portal/src/app/contracts/build/modular-contracts/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721ClaimCustom/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155ClaimCustom/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-4337/account-factory/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-20/vote/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155ClaimConditions/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/PermissionsEnumerable/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-4337/managed-account-factory/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-20/drop/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155ClaimPhases/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/Ownable/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-20/drop-vote/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721ClaimPhases/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/BatchMintMetadata/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/Permissions/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721Staking/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-20/ERC20Permit/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-20/signature-mint/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-4337/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-20/ERC20Staking/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/ContractMetadata/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/PlatformFee/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-721/delayed-reveal/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-20/signature-mint-vote/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-1155/delayed-reveal/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/LazyMint/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-721/signature-mint/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-4337/AccountExtension/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/PrimarySale/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-1155/signature-mint/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-4337/managed-account/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155Staking/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-721/base/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-4337/SmartWallet/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-4337/account/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-721/lazy-mint/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/DelayedReveal/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-4337/SmartWalletFactory/page.mdx`, `apps/portal/src/app/contracts/build/get-started/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/Royalty/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-721/drop/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-1155/lazy-mint/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-1155/base/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/erc-1155/drop/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-20/ERC20SignatureMint/page.mdx`, `apps/portal/src/app/contracts/build/base-contracts/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-721/ERC721SignatureMint/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155SignatureMint/page.mdx`, `apps/portal/src/app/contracts/build/extensions/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155DropSinglePhase/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/DropSinglePhase/page.mdx`, `apps/portal/src/app/contracts/build/extensions/general/Drop/page.mdx`, `apps/portal/src/app/contracts/build/extensions/erc-1155/ERC1155Drop/page.mdx`, `apps/portal/src/app/contracts/build/overview/assets/extension-model.svg`

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

<!-- end pr-codex -->
2024-08-05 22:42:20 +00:00
ea2247c40c Added Celo Alfajores Testnet within the chain-definitions (#3941)
Signed-off-by: Jonas Daniels <[email protected]>
Co-authored-by: Jonas Daniels <[email protected]>
2024-08-05 15:30:42 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a8061277fb chore(deps-dev): bump typescript from 5.5.2 to 5.5.4 (#3945)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 15:23:19 -07:00
Jonas DanielsandGitHub eb94d2ad18 [Feature] Add Content Security Policy and security headers configuration (#3943) 2024-08-05 15:20:13 -07:00
kien-ngo b3aabee23b Dashboard v5: Remove more v4 code (#3927)
This PR is the first step to eventually remove all the v4 code inside `src/contract-ui/tabs/nfts/page.tsx`

2 main components that is now v4-free:
- [x] useIsMinter
- [x] MinterOnly

In fear of putting up a huge PR, I submit this PR as the first in this series

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates contract handling and minter role detection across multiple components.

### Detailed summary
- Updated contract handling to use `ThirdwebContract`
- Improved minter role detection logic
- Refactored minting components to use updated contract handling

> The following files were skipped due to too many changes: `apps/dashboard/src/contract-ui/tabs/nfts/components/shared-metadata-button.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/batch-lazy-mint-button.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/page.tsx`

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

<!-- end pr-codex -->
2024-08-05 20:43:02 +00:00
kien-ngo 72fad8f22d Dashboard v5: Remove useStorageUpload (#3929)
- Add new React hook that uses v5's `upload` under the hood
- Sacrifice has been made - we remove the progress % from the loading bar

<!-- start pr-codex -->

---

## PR-Codex overview
The PR focuses on replacing `useStorageUpload` with `useDashboardStorageUpload` in multiple components for IPFS file uploads.

### Detailed summary
- Replaced `useStorageUpload` with `useDashboardStorageUpload` in `button.tsx`, `string-input.tsx`, `IconUpload.tsx`, and `dropzone.tsx`.
- Updated file upload logic in `FileUpload` component in `dropzone.tsx`.
- Added `useDashboardStorageUpload` in `useDashboardStorageUpload.tsx` for IPFS file uploads.

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

<!-- end pr-codex -->
2024-08-05 20:26:18 +00:00
MananTank e2db196790 Fix Forced dark theme on certain pages (#3940)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add a new `useForceDarkTheme` hook to enforce dark theme across multiple pages.

### Detailed summary
- Added `useForceDarkTheme` hook to enforce dark theme.
- Updated multiple pages to use the new hook.
- Removed `DarkMode` component from affected pages.

> The following files were skipped due to too many changes: `apps/dashboard/src/pages/404.tsx`, `apps/dashboard/src/pages/index.tsx`, `apps/dashboard/src/pages/template/[id].tsx`, `apps/dashboard/src/pages/open-source.tsx`, `apps/dashboard/src/pages/contact-us.tsx`, `apps/dashboard/src/pages/community/ambassadors.tsx`

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

<!-- end pr-codex -->
2024-08-05 20:06:59 +00:00
gregfromstl ae28e27a8c feat: telegram login (#3830)
## TL;DR

This PR introduces support for Telegram OAuth across various components of the thirdweb packages.

## What changed?

- Added 'Telegram' as an OAuth option in `WALLETS` array within the `constants.ts` file.
- Introduced `telegramIconUri` constant and its usage across the application.
- Updated `socialIcons` object and methods to handle new Telegram icon.
- Enhanced `WalletImage` components to recognize and display the Telegram icon.
- Included Telegram in OAuth login path determination within `oauthSignIn.ts`.
- Updated enumerations and type definitions to include Telegram.
- Modified `InAppNativeConnector` and `InAppWebConnector` to handle Telegram OAuth redirection.

## How to test?

1. Navigate to the authentication section in the application.
2. Check for the new 'Sign in with Telegram' option.
3. Attempt to sign in using a Telegram account and verify the successful login flow.

## Why make this change?

To expand the authentication options available to users, providing more flexibility and convenience. Telegram is a popular messaging app, and integrating it as an OAuth option can enhance user experience.

---

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add `Telegram` as a supported authentication option across various parts of the application.

### Detailed summary
- Added `Telegram` as an authentication option in various files
- Included `TELEGRAM_ICON` in multiple components
- Updated social icons with `telegramIconUri`

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

<!-- end pr-codex -->
2024-08-05 19:49:16 +00:00
kien-ngo 9640b184ad Dashboard: Fix [Open IPFS folder] link (#3935)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor the logic for getting the folder path from an IPFS hash URL in the `dropzone.tsx` component.

### Detailed summary
- Refactored logic to extract folder path from IPFS hash URL
- Updated return statement to use the extracted folder path for the IPFS URL

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

<!-- end pr-codex -->
2024-08-05 16:59:18 +00:00
kien-ngo a2e13820f2 Dashboard v5: Update dashboard [...paths].tsx to v5 (#3925)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor contract detection logic and improve metadata handling.

### Detailed summary
- Removed `detectContractFeature` from `@thirdweb-dev/sdk`
- Added functions for detecting ERC20, ERC721, and ERC1155 extensions
- Improved contract metadata handling with `getContractMetadata` function

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

<!-- end pr-codex -->
2024-08-05 16:46:09 +00:00
jnsdls d0ea16e887 [Fix] Add 'Chainlist' link to header and update navigation structure (#3939)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add a new `Chainlist` link in the header and a corresponding button in the app shell.

### Detailed summary
- Added `Chainlist` link in the header navigation.
- Included a `Chainlist` button in the app shell for quick access.
- Removed previous `Chainlist` link implementation.

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

<!-- end pr-codex -->
2024-08-05 16:41:37 +00:00
kien-ngo 1f3448e1ea Portal: Improve ts docs (#3937)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances error handling and callback functionality in various components like `TransactionButton` and `CreateDirectListingButton`.

### Detailed summary
- Added guidance for error handling and callbacks in `TransactionButton`
- Updated comments in `CreateDirectListingButton`
- Improved error handling info in `ClaimButton`
- Added examples and instructions for handling errors and callbacks in components

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

<!-- end pr-codex -->
2024-08-05 06:27:14 +00:00
MananTank ad619a779c Use shadcn checkbox in deploy form (#3928)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor and improve the UI components in the custom contract form.

### Detailed summary
- Added `CheckboxWithLabel` component with enhanced functionality.
- Refactored `Tooltip` to `ToolTipLabel` for better usability.
- Updated usage of components like `TrackedLinkTW` and `Spinner`.
- Improved layout and styling for better user experience.

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

<!-- end pr-codex -->
2024-08-04 18:55:17 +00:00
MananTank 09e89e5e6b Move FileInput from chakra to tailwind (#3926)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates various components to replace Chakra UI props with className for styling consistency.

### Detailed summary
- Replaced Chakra UI props with className for consistent styling in multiple components.
- Updated styling in `FileInput` component for better consistency and maintainability.
- Added new imports and removed unnecessary imports in `FileInput` component.

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

<!-- end pr-codex -->
2024-08-04 18:46:46 +00:00
jnsdls baefa5e9c7 [Chore] Uniform formatting in package.json and update fs imports (#3933) 2024-08-03 23:53:16 +00:00
kien-ngo 57e87528c8 chore: add a small test for numberToHex() (#3932)
Happened to write this quick test when debugging an issue

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add a test for the `numberToHex` function in `hex.ts`.

### Detailed summary
- Added a test suite for `numberToHex` function in `hex.ts`
- Tested conversion of a number to a hexadecimal string with specified size and sign
- Asserted the correctness of the conversion result

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

<!-- end pr-codex -->
2024-08-03 23:39:39 +00:00
Jonas DanielsandGitHub 0b5dc2a137 [Fix] Validate and decode nextPath to ensure it is a relative path before redirecting (#3930) 2024-08-03 15:56:32 -07:00
jnsdls 0d23d5d6d5 [Fix] Exclude auth token from pay URLs when setting request headers (#3931)
This PR updates the fetch utility in the Thirdweb package to exclude the authorization token from being sent when the endpoint is related to the pay service. This ensures that requests to 'pay.' prefixed URLs are always made on behalf of the 'developer' rather than the 'end user.' It introduces a new function, isPayUrl, to determine if the URL belongs to the pay service.

---
2024-08-03 22:38:12 +00:00
kien-ngo 16376e56d7 Fix typo in docs (#3924)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces information about using Ethers.js in version 5 of the thirdweb SDK.

### Detailed summary
- Added a section titled "Ethers.js in v5" in `page.mdx`
- Clarified that SDK v5 is compatible with Ethers.js v5 and v6

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

<!-- end pr-codex -->
2024-08-03 05:19:07 +00:00
kien-ngo 3f923e4469 Update to React migration docs (#3923)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on migrating React SDK v4 to v5. It includes changes in installation steps, interacting with contracts, and introducing an ethers.js adapter.

### Detailed summary
- Updated installation steps for React SDK v5
- Added new components for interacting with contracts
- Introduced an ethers.js adapter for compatibility with v5
- Updated sidebar links for migration guidance

> The following files were skipped due to too many changes: `apps/portal/src/app/react/v5/sidebar.tsx`

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

<!-- end pr-codex -->
2024-08-03 05:09:30 +00:00
kien-ngo c7a417e285 <Crusade> Claim NFT drop to v5 (#3910)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to optimize NFT claiming functionality in the dashboard.

### Detailed summary
- Refactored NFTClaimButton to use `contractAddress` and `chainId`
- Updated NFTClaimForm to use `contractAddress` and `chainId` instead of `contract`
- Improved error handling in NFTClaimForm
- Added transaction handling in NFTClaimForm

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

<!-- end pr-codex -->
2024-08-03 01:58:31 +00:00
gregfromstl a97d7636ba docs: add callouts for generating client ID across multiple guides (#3921)
### TL;DR

Added information callouts across various SDK documentation pages, prompting users to generate a client ID to access blockchain APIs, storage, and more.

### What changed?

1. Integrated `Link` and `Button` components from `next/link` and `@/components/ui/button` respectively.
2. Added `<Callout>` components with instructional text and links for generating client IDs.
3. Changed existing callout in TypeScript client page from 'Api Key' to 'Client ID'.

### How to test?

Check the following pages for the new callout additions:
- `/connect`
- `/react-native/v5`
- `/react/v5`
- `/typescript/v5`
- `/typescript/v5/client`

### Why make this change?

To guide users in generating a client ID, which is necessary to access various features offered by the SDK.

---

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update references from "Api Key" to "Client ID" and add a `Link` and `Button` component for generating client IDs in various pages.

### Detailed summary
- Updated "Api Key" to "Client ID" in callouts
- Added `Link` and `Button` components for generating client IDs in multiple pages

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

<!-- end pr-codex -->
2024-08-03 01:55:02 +00:00
Jonas DanielsandGitHub 80e0957857 [Feature] Add portal build configuration to turbo.json and update changeset (#3922) 2024-08-02 18:11:35 -07:00
gregfromstl 14d3d18ad2 fix: portal dependencies (#3920)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating dependencies and adding webpack configuration.

### Detailed summary
- Skipped a test for executing zkcandy tx via SDK
- Updated `remark-gfm` version
- Added webpack configuration for external libraries
- Updated dependencies for linting and fixed scripts
- Updated various `mdast-util` packages
- Updated `micromark-extension` packages
- Updated `remark-gfm` version in multiple places

> The following files were skipped due to too many changes: `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2024-08-03 00:38:06 +00:00
MananTank 079dc921d4 [dashboard] Enhance UI consistency across chain page components (#3919)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update various UI components in the dashboard app for better styling consistency and functionality.

### Detailed summary
- Updated styling of `CopyTextButton`, `ExplorerSection`, `LiveStats`, and `ChainOverviewSection`.
- Replaced `CopyButton` import with `CopyTextButton`.
- Improved button and link styling for better user experience.

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

<!-- end pr-codex -->
2024-08-03 00:10:36 +00:00
kien-ngo c2e86c6828 <Crusade> Type-only migration! (#3909)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update types and imports related to NFT metadata handling in various files.

### Detailed summary
- Updated types to use `NFTInput` instead of `NFTMetadataInput` for NFT metadata handling.
- Updated imports to use `NFTInput` from "thirdweb/utils" in multiple files.
- Adjusted function signatures and parameters accordingly for consistency and compatibility.

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

<!-- end pr-codex -->
2024-08-02 23:32:06 +00:00
gregfromstl 9d519c6590 chore: move the docs to the monorepo (#3915)
Adds the docs to the monorepo
2024-08-02 22:41:36 +00:00
kien-ngo d04b03226a <Crusade> Dashboard to v5: Marketplace v3 (#3907)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add a `type` field to `DirectListing` and `EnglishAuction` types in the marketplace extensions.

### Detailed summary
- Added `type` field to `DirectListing` and `EnglishAuction` types
- Updated components to handle `type` field for pricing display
- Refactored contract role checks for Marketplace v3 compatibility

> The following files were skipped due to too many changes: `apps/dashboard/src/contract-ui/tabs/listings/components/list-form.tsx`

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

<!-- end pr-codex -->
2024-08-02 22:17:03 +00:00
MananTank c24dadaa60 Chain page tweaks (#3916)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates various components and functions related to chain metadata and wallet connection logic.

### Detailed summary
- Improved conditional rendering for `ChainCTA` component
- Updated styles and behavior in `SidebarContent` and `MobileMenu`
- Added `useCustomConnectModal` hook for custom wallet connection modal
- Enhanced `AddChainToWallet` component with custom connect modal functionality

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

<!-- end pr-codex -->
2024-08-02 22:09:42 +00:00
gregfromstl 2c751b4fce test: adds msw (#3913)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add `msw` for network call mocking, update dependencies, and enhance wallet integration tests.

### Detailed summary
- Added `msw` for network call mocking in tests
- Updated dependencies
- Improved wallet integration tests with new functions

> The following files were skipped due to too many changes: `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2024-08-02 20:16:31 +00:00
Joe-Thirdweb e7f7ae8452 Chainlist default banner for PAID CHAINS (#3911)
## Problem solved

Short description of the bug fixed or feature added
Added a default banner and CTA image for every paying chain except lens.
Added an about section to every paying chain
Added a link to their website for every paying chain

##How to test
Visit the chain's chainlist page and view the images.
Click on their "learn More" butttons to be taken to their website.
Chains Added
- Lumia Mainnet
- Superposition Testnet
- Aleph 0 Mainnet
- b3 Mainnet
- Campnetwork v2 testnet
- Donatuz mainnet
- Fuse
- Gemuchain
- 0g labs testnet
- zkCandy
- zkSync

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add metadata for different blockchains in the dashboard.

### Detailed summary
- Added metadata for Lumia Mainnet, Superposition Testnet, Aleph 0 Mainnet, B3 Mainnet, CampNetwork Testnet v2, Donatuz, Fuse, Gemuchain, 0G-Newton-Testnet, Plume, zkCandy, and zkSync
- Updated `utils.ts` with imports for new assets
- Updated chain metadata record with details for each blockchain

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

<!-- end pr-codex -->
2024-08-02 19:14:22 +00:00
9c292a4903 [dashboard] Chain page redesign (#3826)
Co-authored-by: Manan Tank <[email protected]>
2024-08-02 19:01:12 +00:00
jnsdls b06272c35d [Feature] Add Chainlist link and update header styles (#3899)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to add a new link "Chainlist" to the dashboard header and update styles for better user experience.

### Detailed summary
- Added "Chainlist" link to the dashboard header
- Updated styles for the "Chainlist" link to match the design
- Adjusted styles for active links in the header for better visibility and consistency

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

<!-- end pr-codex -->
2024-08-02 17:44:14 +00:00
joaquim-verges 366646e354 chore(jsdocs): enhance JSDoc examples across multiple hooks and functions (#3908)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces enhancements related to wallet interactions, contract calls, and event monitoring in the application.

### Detailed summary
- Added functionality to pre-authenticate users via email or phone number
- Enhanced wallet connection components with account abstraction and gas sponsorship options
- Updated wallet connection buttons to support account abstraction and gas sponsorship
- Improved wallet information retrieval with examples
- Expanded capabilities for fetching wallet balances for native and specific tokens
- Enhanced contract call preparation with examples of different method signatures and value options
- Improved contract event monitoring with event extensions and custom event watching
- Updated contract read hooks to automatically update on contract changes

> The following files were skipped due to too many changes: `packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts`, `packages/thirdweb/src/transaction/actions/send-transaction.ts`, `packages/thirdweb/src/react/web/hooks/transaction/useSendTransaction.tsx`

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

<!-- end pr-codex -->
2024-08-02 03:17:02 +00:00
joaquim-verges 8eeec64388 fix(tests): update PayEmbed tests to use waitFor for render assertions (#3906)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update test files in `PayEmbed-disconnected.test.tsx` and `PayEmbed.test.tsx` to use `waitFor` for asynchronous operations.

### Detailed summary
- Added `waitFor` for asynchronous operations in test files
- Updated test cases to handle asynchronous rendering and interactions

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

<!-- end pr-codex -->
2024-08-01 20:03:45 +00:00
1478 changed files with 106375 additions and 3965 deletions
+1
View File
@@ -20,6 +20,7 @@
"playground-web",
"thirdweb-dashboard",
"wallet-ui",
"portal",
"@thirdweb-dev/auth",
"@thirdweb-dev/chains",
"@thirdweb-dev/cli",
+2 -1
View File
@@ -44,7 +44,8 @@ packages/unity-js-bridge/ @joaquim-verges @0xFirekeeper @jnsdls
apps/ @jnsdls @joaquim-verges
apps/dashboard/ @jnsdls @MananTank @joaquim-verges
apps/playground-web/ @joaquim-verges @gregfromstl @jnsdls @joaquim-verges
apps/wallet-ui/ @gregfromstl @jnsdls @joaquim-verges
apps/wallet-ui/ @gregfromstl @jnsdls @joaquim-verges @MananTank
apps/portal/ @gregfromstl @jnsdls @joaquim-verges @MananTank
# .github folder + .changeset + turbo config is owned by jonas for now
scripts/ @jnsdls @joaquim-verges
+15
View File
@@ -77,6 +77,21 @@ Many of the tests use forked versions of live chains like mainnet. We fork these
If you need to use accounts in your tests, use the predefined accounts in `test/src/test-wallets.ts`. These are the default anvil accounts that are pre-funded on the local test forks.
##### Mocking Network Calls
If your test depends on a downstream network call, you must mock the call using `msw`. You can use one of the existing mocks in `test/src/mocks` or create your own. Once you've created a mock request, setup the server and add all your mocks like so:
```ts
import { setupServer } from "msw/node";
import { downloadMock, uploadMock } from "../../../test/src/mocks/storage.js";
const server = setupServer(uploadMock("HASH"), downloadMock({ name: "Test NFT" }));
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
```
#### Linting
We use a linter to maintain best practices across projects. Once your changes are complete (or periodically while making changes), run the linter with the following command from the repo root:
+3
View File
@@ -11,3 +11,6 @@ legacy_packages:
packages:
- changed-files:
- any-glob-to-any-file: "packages/**/*"
portal:
- changed-files:
- any-glob-to-any-file: "apps/portal/**/*"
+2 -2
View File
@@ -14,7 +14,7 @@ public/dist
.idea/
.yalc/
yalc.lock
build/
./build/
playwright-report/
.env/
@@ -25,4 +25,4 @@ legacy_packages/cli/artifacts/
codecov
# Remove sitemap files (generated at build time)
sitemap*.xml
sitemap*.xml
+2 -2
View File
@@ -106,13 +106,13 @@
"server-only": "^0.0.1",
"sonner": "^1.5.0",
"spdx-correct": "^3.2.0",
"spin-wheel-game": "1.0.3-alpha.3",
"swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
"tiny-invariant": "^1.3.3",
"use-debounce": "^10.0.1",
"vaul": "^0.9.1",
"zksync-ethers": "5.9.0",
"zod": "3.23.8"
},
@@ -150,7 +150,7 @@
"next-sitemap": "^4.2.3",
"next-unused": "^0.0.6",
"tailwindcss": "^3.4.4",
"typescript": "5.5.2"
"typescript": "5.5.4"
},
"next-unused": {
"alias": {},
@@ -1,9 +1,18 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
import type { ThemeProviderProps } from "next-themes/dist/types";
import * as React from "react";
import { useEffect } from "react";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
export function useForceDarkTheme() {
const { setTheme } = useTheme();
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
setTheme("dark");
}, [setTheme]);
}
@@ -36,7 +36,7 @@ export function CopyTextButton(props: {
variant={props.variant || "outline"}
aria-label={props.tooltip}
className={cn(
"h-auto w-auto px-2.5 py-1.5 flex gap-2 rounded-lg text-foreground",
"h-auto w-auto px-2.5 py-1.5 flex gap-2 rounded-lg text-foreground font-normal",
props.className,
)}
onClick={() => {
@@ -28,3 +28,19 @@ const Checkbox = React.forwardRef<
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
export function CheckboxWithLabel(props: {
children: React.ReactNode;
className?: string;
}) {
return (
<label
className={cn(
"flex gap-2 items-center text-secondary-foreground text-sm cursor-pointer select-none",
props.className,
)}
>
{props.children}
</label>
);
}
+11 -3
View File
@@ -31,10 +31,13 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
dialogOverlayClassName?: string;
dialogCloseClassName?: string;
}
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogOverlay className={props.dialogOverlayClassName} />
<DialogPrimitive.Content
ref={ref}
className={cn(
@@ -54,7 +57,12 @@ const DialogContent = React.forwardRef<
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close
className={cn(
"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
props.dialogCloseClassName,
)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
@@ -0,0 +1,118 @@
"use client";
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
);
Drawer.displayName = "Drawer";
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className,
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
));
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};
+12 -3
View File
@@ -31,15 +31,24 @@ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
export function ToolTipLabel(props: {
children: React.ReactNode;
label: string;
label: string | undefined;
rightIcon?: React.ReactNode;
leftIcon?: React.ReactNode;
}) {
if (!props.label) {
return props.children;
}
return (
<TooltipProvider>
<Tooltip delayDuration={100} disableHoverableContent={true}>
<TooltipTrigger asChild>{props.children}</TooltipTrigger>
<TooltipContent sideOffset={10}>
<TooltipTrigger asChild className="!pointer-events-auto">
{props.children}
</TooltipTrigger>
<TooltipContent
sideOffset={10}
className="max-w-[300px] leading-relaxed"
>
<div className="p-2 text-sm flex items-center gap-1.5">
{props.leftIcon}
{props.label}
+16 -16
View File
@@ -55,7 +55,7 @@
--inverted: var(--foreground);
/* bg - colorful */
--primary: 215.88 100% 55%;
--primary: 221 83% 54%;
--destructive: 0 62.8% 15%;
--success: 156.2 71.6% 10%;
--warning: 40.61deg 96.12% 8%;
@@ -93,23 +93,23 @@
/* shadcn charts */
@layer base {
:root {
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
:root {
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@keyframes skeleton {
0%,
100% {
@@ -14,13 +14,19 @@ import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useCallback, useMemo } from "react";
import { defineChain } from "thirdweb";
import { AutoConnect, ConnectButton } from "thirdweb/react";
import { type Chain, defineChain } from "thirdweb";
import { AutoConnect, ConnectButton, useConnectModal } from "thirdweb/react";
import { getSDKTheme } from "../../../../app/components/sdk-component-theme";
import { useFavoriteChains } from "../../hooks/useFavoriteChains";
import { useLoggedInUser } from "../../hooks/useLoggedInUser";
import { popularChains } from "../popularChains";
export const CustomConnectWallet: React.FC = () => {
export const CustomConnectWallet = (props: {
loginRequired?: boolean;
connectButtonClassName?: string;
}) => {
const loginRequired =
props.loginRequired === undefined ? true : props.loginRequired;
const { theme } = useTheme();
const recentChainsv4 = useRecentlyUsedChains();
const addRecentlyUsedChainId = useAddRecentlyUsedChainId();
@@ -66,7 +72,7 @@ export const CustomConnectWallet: React.FC = () => {
);
}
if (!isLoggedIn) {
if (!isLoggedIn && loginRequired) {
return (
<>
<Button asChild variant="default" className="gap-2" size="lg">
@@ -84,7 +90,7 @@ export const CustomConnectWallet: React.FC = () => {
return (
<ConnectButton
theme={t}
theme={getSDKTheme(t)}
client={thirdwebClient}
connectModal={{
privacyPolicyUrl: "/privacy",
@@ -107,6 +113,9 @@ export const CustomConnectWallet: React.FC = () => {
console.error("Failed to log out", err);
}
}}
connectButton={{
className: props.connectButtonClassName,
}}
chains={allChains}
detailsModal={{
networkSelector: {
@@ -193,6 +202,35 @@ export function ConnectWalletWelcomeScreen(props: {
);
}
export function useCustomConnectModal() {
const { connect } = useConnectModal();
const { theme } = useTheme();
return useCallback(
(options?: { chain?: Chain }) => {
return connect({
client: thirdwebClient,
appMetadata: {
name: "thirdweb",
logoUrl: "https://thirdweb.com/favicon.ico",
url: "https://thirdweb.com",
},
chain: options?.chain,
privacyPolicyUrl: "/privacy",
termsOfServiceUrl: "/tos",
showThirdwebBranding: false,
welcomeScreen: () => (
<ConnectWalletWelcomeScreen
theme={theme === "light" ? "light" : "dark"}
/>
),
theme: getSDKTheme(theme === "light" ? "light" : "dark"),
});
},
[connect, theme],
);
}
/**
* A link component extends the `Link` component and adds tracking.
*/
@@ -1,9 +1,9 @@
import { useIsLister } from "@3rdweb-sdk/react/hooks/useContractRoles";
import type { ValidContractInstance } from "@thirdweb-dev/sdk";
import type { ThirdwebContract } from "thirdweb";
import type { ComponentWithChildren } from "types/component-with-children";
interface IListerOnlyProps {
contract?: ValidContractInstance;
contract: ThirdwebContract;
}
export const ListerOnly: ComponentWithChildren<IListerOnlyProps> = ({
@@ -1,9 +1,9 @@
import { useIsMinter } from "@3rdweb-sdk/react/hooks/useContractRoles";
import type { ValidContractInstance } from "@thirdweb-dev/sdk";
import type { ThirdwebContract } from "thirdweb";
import type { ComponentWithChildren } from "types/component-with-children";
interface IMinterOnlyProps {
contract?: ValidContractInstance;
contract: ThirdwebContract;
}
export const MinterOnly: ComponentWithChildren<IMinterOnlyProps> = ({
@@ -5,8 +5,9 @@ import {
useRoleMembers,
} from "@thirdweb-dev/react";
import type { ValidContractInstance } from "@thirdweb-dev/sdk";
import { ZERO_ADDRESS } from "thirdweb";
import { useActiveAccount } from "thirdweb/react";
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
import { hasRole } from "thirdweb/extensions/permissions";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import type { RequiredParam } from "utils/types";
function isContractWithRoles(
@@ -73,38 +74,45 @@ export function useIsAdminOrSelf<TContract extends ValidContractInstance>(
return isAdmin;
}
export function useIsMinter<TContract extends ValidContractInstance>(
contract: RequiredParam<TContract>,
) {
export function useIsMinter(contract: ThirdwebContract) {
const address = useActiveAccount()?.address;
const { data: contractType } = useContractType(contract?.getAddress());
const contractHasRoles = isContractWithRoles(contract);
const isAccountRole = useIsAccountRole(
"minter",
contractHasRoles ? contract : undefined,
address,
);
const userCanMintQuery = useReadContract(hasRole, {
contract,
targetAccountAddress: address ?? "",
role: "minter",
queryOptions: { enabled: !!address },
});
if (contractType === "custom") {
return true;
}
return isAccountRole;
const anyoneCanMintQuery = useReadContract(hasRole, {
contract,
targetAccountAddress: ZERO_ADDRESS,
role: "minter",
});
// If both requests "error out", we can safely assume that
// this is a custom (aka. non-thirdweb) contract
// so we just return `true` in that case
const isCustomContract =
userCanMintQuery.isError && anyoneCanMintQuery.isError;
return anyoneCanMintQuery.data || userCanMintQuery.data || isCustomContract;
}
export function useIsLister<TContract extends ValidContractInstance>(
contract: RequiredParam<TContract>,
) {
// Applicable to Marketplace v3 only
export function useIsLister(contract: ThirdwebContract) {
const address = useActiveAccount()?.address;
const { data: contractType } = useContractType(contract?.getAddress());
const contractHasRoles = isContractWithRoles(contract);
const isAccountRole = useIsAccountRole(
"lister",
contractHasRoles ? contract : undefined,
address,
);
const { data: userCanList } = useReadContract(hasRole, {
contract,
targetAccountAddress: address ?? "",
role: "lister",
queryOptions: { enabled: !!address },
});
if (contractType === "custom") {
return true;
}
return isAccountRole;
const { data: anyoneCanList } = useReadContract(hasRole, {
contract,
targetAccountAddress: ZERO_ADDRESS,
role: "lister",
});
return anyoneCanList || userCanList;
}
@@ -0,0 +1,26 @@
import { thirdwebClient } from "@/constants/client";
import { useMutation } from "@tanstack/react-query";
import { upload } from "thirdweb/storage";
type DashboardUploadOptions = {
uploadWithoutDirectory?: boolean;
metadata?: Record<string, string>;
};
export function useDashboardStorageUpload(options?: DashboardUploadOptions) {
return useMutation({
mutationFn: async (files: Array<File | string>): Promise<string[]> => {
const uris = await upload({
client: thirdwebClient,
files,
...options,
});
// the upload method in SDK v5 has this logic
// that will make it return only a string if you upload only 1 file
if (typeof uris === "string") {
return [uris];
}
return uris;
},
});
}
@@ -1,59 +0,0 @@
import { redirect } from "next/navigation";
import { getChain } from "../../../utils";
import { InfoCard } from "../components/server/info-card";
export default async function Page(props: { params: { chain_id: string } }) {
const chain = await getChain(props.params.chain_id);
const enabled = chain.services.find(
(s) => s.service === "account-abstraction",
)?.enabled;
if (!enabled) {
redirect(`/${props.params.chain_id}`);
}
return (
<div>
<InfoCard
title="Account Abstraction"
links={[
{
label: "Get Started",
href: "/dashboard/connect/account-abstraction",
},
{
label: "Learn More",
href: "https://portal.thirdweb.com/connect/account-abstraction/how-it-works",
},
]}
>
<p>
Everything you need to leverage account abstraction technology to
enable seamless user experiences for your users.
</p>
<p>
You get all the tools to integrate account abstraction into your app.
This includes:
</p>
<div className="h-3" />
<ul className="text-secondary-foreground pl-4 [&_li]:list-disc [&_li]:mb-3 [&_li]:pl-1">
<li>
Account factory contracts that let you spin up smart accounts for
your users
</li>
<li>
Bundler, which is the infrastructure needed to process account
abstraction transactions (known as User Ops)
</li>
<li>
Paymaster, which lets you sponsor transaction fees for your users
</li>
</ul>
</InfoCard>
</div>
);
}
@@ -0,0 +1,143 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import { useMutation } from "@tanstack/react-query";
import { thirdwebClient } from "lib/thirdweb-client";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { defineChain, toUnits } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
function formatTime(seconds: number) {
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return rtf.format(+hours, "hour");
}
if (minutes > 0) {
return rtf.format(+minutes, "minute");
}
return rtf.format(+seconds, "second");
}
export function FaucetButton({
chain,
ttlSeconds,
amount,
}: {
chain: ChainMetadata;
ttlSeconds: number;
amount: string;
}) {
const address = useActiveAccount()?.address;
const chainId = chain.chainId;
const definedChain = defineChain(chainId);
const faucetWalletBalanceQuery = useWalletBalance({
address: THIRDWEB_ENGINE_FAUCET_WALLET,
chain: definedChain,
client: thirdwebClient,
});
const claimMutation = useMutation({
mutationFn: async () => {
const response = await fetch("/api/testnet-faucet/claim", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chainId: chainId,
toAddress: address,
amount,
}),
});
if (!response.ok) {
const data = await response.json();
throw data.error;
}
},
onSuccess: () => {
router.refresh();
},
});
const router = useRouter();
const isFaucetEmpty =
faucetWalletBalanceQuery.data !== undefined &&
faucetWalletBalanceQuery.data.value < toUnits("1", 17);
// checking faucet balance
if (faucetWalletBalanceQuery.isLoading) {
return (
<Button variant="secondary" className="w-full gap-2">
Checking Faucet Balance <Spinner className="size-3" />
</Button>
);
}
// faucet is empty
if (isFaucetEmpty) {
return (
<Button variant="outline" disabled className="w-full">
Faucet is empty right now, come back later
</Button>
);
}
// not eligible to claim right now
if (ttlSeconds > 0) {
return (
<Button variant="outline" className="w-full" disabled>
Your next claim is available {formatTime(ttlSeconds)}
</Button>
);
}
if (!address) {
return (
<CustomConnectWallet
loginRequired={false}
connectButtonClassName="!w-full !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm"
/>
);
}
// eligible to claim and faucet has balance
return (
<div className="flex flex-col w-full text-center">
<Button
variant="primary"
className="w-full gap-2"
onClick={() => {
const claimPromise = claimMutation.mutateAsync();
toast.promise(claimPromise, {
success: `${amount} ${chain.nativeCurrency.symbol} sent successfully`,
error: `Failed to claim ${amount} ${chain.nativeCurrency.symbol}`,
});
}}
>
{claimMutation.isLoading ? (
<>
Claiming <Spinner className="size-3" />
</>
) : (
`Get ${amount} ${chain.nativeCurrency.symbol}`
)}
</Button>
{faucetWalletBalanceQuery.data && (
<p className="mt-3 text-xs text-secondary-foreground">
{Number(faucetWalletBalanceQuery.data.displayValue).toFixed(3)}{" "}
{faucetWalletBalanceQuery.data.symbol} left in the faucet
</p>
)}
</div>
);
}
@@ -0,0 +1,34 @@
"use client";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { MenuIcon } from "lucide-react";
import { useState } from "react";
import type { ChainMetadata } from "thirdweb/chains";
import { SidebarContent } from "../server/sidebar-content";
export function MobileMenu(props: {
chain: ChainMetadata;
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button size="icon" variant="outline" className="lg:hidden">
<MenuIcon strokeWidth={1} />
</Button>
</DialogTrigger>
<DialogContent
className="p-0"
dialogCloseClassName="hidden"
onClick={(e) => {
if (e.target instanceof HTMLAnchorElement) {
setIsOpen(false);
}
}}
>
<SidebarContent chain={props.chain} />
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,42 @@
"use client";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { thirdwebClient } from "@/constants/client";
import { useTheme } from "next-themes";
import { defineChain } from "thirdweb";
import { PayEmbed } from "thirdweb/react";
import { getSDKTheme } from "../../../../../../components/sdk-component-theme";
export function PayModalButton(props: { chainId: number; label: string }) {
const { theme } = useTheme();
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="primary" className="w-full">
{props.label}
</Button>
</DialogTrigger>
<DialogContent
className="md:max-w-[360px] p-0 border-none bg-transparent"
dialogOverlayClassName="backdrop-blur-lg"
dialogCloseClassName="focus:ring-0"
>
<PayEmbed
client={thirdwebClient}
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
className="!w-auto"
payOptions={{
prefillBuy: {
chain: defineChain(props.chainId),
allowEdits: {
chain: false,
amount: true,
token: true,
},
},
}}
/>
</DialogContent>
</Dialog>
);
}
@@ -3,8 +3,9 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { useCustomConnectModal } from "@3rdweb-sdk/react/components/connect-wallet";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import type { Chain } from "thirdweb";
import {
useActiveAccount,
@@ -14,56 +15,54 @@ import {
import { useDebounce } from "use-debounce";
type AddChainToWalletProps = {
chain?: Chain;
hasBackground?: boolean;
chain: Chain;
};
export const AddChainToWallet: React.FC<AddChainToWalletProps> = (props) => {
const address = useActiveAccount()?.address;
const account = useActiveAccount();
const switchChain = useSwitchActiveWalletChain();
const activeWalletChainId = useActiveWalletChain()?.id;
const customConnectModal = useCustomConnectModal();
const switchChainMutation = useMutation({
mutationFn: async () => {
if (!props.chain) {
throw new Error("Chain is not defined");
}
await switchChain(props.chain);
},
});
// debounce the loading state to prevent flickering
const [debouncedLoading] = useDebounce(switchChainMutation.isLoading, 50);
const buttonContent = (
<Button
disabled={
!address || debouncedLoading || activeWalletChainId === props.chain?.id
}
className={cn(
"w-full md:min-w-40 gap-2",
props.hasBackground && "invert dark:invert-0",
)}
onClick={() => switchChainMutation.mutate()}
>
<span>Add to wallet</span>
{debouncedLoading && <Spinner className="size-3" />}
</Button>
);
if (address && activeWalletChainId !== props.chain?.id) {
return buttonContent;
}
const disabled = debouncedLoading || activeWalletChainId === props.chain?.id;
return (
<ToolTipLabel
label={
activeWalletChainId === props.chain?.id
? "You are already connected to this chain"
: "Connect your wallet first"
activeWalletChainId === props.chain.id
? "Wallet is already connected to this chain"
: undefined
}
>
<div className="cursor-not-allowed">{buttonContent}</div>
<Button
disabled={disabled}
className="gap-2 w-full"
variant="outline"
onClick={() => {
// Connct directly to this chain
if (!account) {
return customConnectModal({ chain: props.chain });
}
// switch to this chain
const switchPromise = switchChainMutation.mutateAsync();
toast.promise(switchPromise, {
success: `${props.chain.name} added to wallet`,
error: `Failed to add ${props.chain.name} to wallet`,
});
}}
>
<span>Add to wallet</span>
{debouncedLoading && <Spinner className="size-3" />}
</Button>
</ToolTipLabel>
);
};
@@ -1,6 +1,6 @@
"use client";
import { CopyButton } from "@/components/ui/CopyButton";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useQuery } from "@tanstack/react-query";
@@ -9,7 +9,7 @@ import { thirdwebClient } from "lib/thirdweb-client";
import { CircleCheck, XIcon } from "lucide-react";
import { useState } from "react";
import { hostnameEndsWith } from "utils/url";
import { PrimaryInfoItem } from "../../../components/server/primary-info-item";
import { PrimaryInfoItem } from "../server/primary-info-item";
function useChainStatswithRPC(_rpcUrl: string) {
const [shouldRefetch, setShouldRefetch] = useState(true);
@@ -79,19 +79,24 @@ export function ChainLiveStats(props: { rpc: string }) {
}
>
<div className="flex items-center gap-1">
<div className="text-lg">{new URL(props.rpc).origin}</div>
<CopyButton text={new URL(props.rpc).origin} />
<CopyTextButton
tooltip="Copy RPC URL"
textToShow={new URL(props.rpc).origin}
textToCopy={props.rpc}
copyIconPosition="right"
variant="ghost"
className="px-2 py-1 -translate-x-2 text-base"
iconClassName="size-4"
/>
</div>
</PrimaryInfoItem>
{/* Latency */}
<PrimaryInfoItem title="RPC Latency" titleIcon={<PulseDot />}>
{stats.isError ? (
<p className="text-lg fade-in-0 animate-in text-destructive-text">
N/A
</p>
<p className="fade-in-0 animate-in text-destructive-text">N/A</p>
) : stats.data ? (
<p className="text-lg fade-in-0 animate-in">{stats.data.latency}ms</p>
<p className="fade-in-0 animate-in">{stats.data.latency}ms</p>
) : (
<div className="flex py-1 h-[28px] w-[70px]">
<Skeleton className="h-full w-full" />
@@ -102,13 +107,9 @@ export function ChainLiveStats(props: { rpc: string }) {
{/* Block Height */}
<PrimaryInfoItem title="Block Height" titleIcon={<PulseDot />}>
{stats.isError ? (
<p className="text-lg fade-in-0 animate-in text-destructive-text">
N/A
</p>
<p className="fade-in-0 animate-in text-destructive-text">N/A</p>
) : stats.data ? (
<p className="text-lg fade-in-0 animate-in">
{stats.data.blockNumber}
</p>
<p className="fade-in-0 animate-in">{stats.data.blockNumber}</p>
) : (
<div className="flex py-1 h-[28px] w-[140px]">
<Skeleton className="h-full w-full" />
@@ -122,9 +123,9 @@ export function ChainLiveStats(props: { rpc: string }) {
function PulseDot() {
return (
<ToolTipLabel label={"Live Data"}>
<span className="relative flex size-3">
<span className="relative flex size-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75" />
<span className="relative inline-flex rounded-full size-3 bg-primary" />
<span className="relative inline-flex rounded-full size-2 bg-primary" />
</span>
</ToolTipLabel>
);
@@ -0,0 +1,24 @@
"use client";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { usePathname } from "next/navigation";
export type NavButtonProps = {
className?: string;
activeClassName?: string;
href: string;
};
export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
const pathname = usePathname();
const isActive = pathname === props.href;
return (
<Link
href={props.href}
className={cn(props.className, isActive && props.activeClassName)}
>
{props.children}
</Link>
);
}
@@ -1,50 +0,0 @@
"use client";
import { TabLinks } from "@/components/ui/tabs";
import { useSelectedLayoutSegment } from "next/navigation";
import { useMemo } from "react";
import { products } from "../../../../components/server/products";
import type { ChainSupportedService } from "../../../../types/chain";
export function getEnabledTabs(enabledServices: ChainSupportedService[]) {
return products
.map((p) => ({
name: p.name,
segment: p.id === "contracts" ? "" : p.id,
serviceId: p.id,
icon: p.icon,
isEnabled: enabledServices.includes(p.id) ?? false,
}))
.sort((a, b) => {
if (a.isEnabled && !b.isEnabled) {
return -1;
}
if (!a.isEnabled && b.isEnabled) {
return 1;
}
return 0;
});
}
export function ChainPageTabs(props: {
chainSlug: string;
enabledServices: ChainSupportedService[];
}) {
const layoutSegment = useSelectedLayoutSegment() || "";
const links = useMemo(() => {
return getEnabledTabs(props.enabledServices);
}, [props.enabledServices]);
return (
<TabLinks
links={links.map((tab) => ({
name: tab.name,
href: `/${props.chainSlug}/${tab.segment}`,
isActive: layoutSegment === tab.segment,
isEnabled: tab.isEnabled,
icon: tab.icon,
}))}
/>
);
}
@@ -0,0 +1,89 @@
export function CreditCardIcon(props: {
bg: string;
className?: string;
}) {
return (
<svg
width={48}
height={48}
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={props.className}
>
<rect width={48} height={48} rx={16} fill={props.bg} />
<rect
x={1}
y={1}
width={46}
height={46}
rx={15}
stroke="url(#paint0_linear_1315_3638)"
strokeOpacity="0.2"
strokeWidth={2}
/>
<rect x={5} y={5} width={38} height={38} rx={11} fill={props.bg} />
<rect
x={5}
y={5}
width={38}
height={38}
rx={11}
stroke="url(#paint1_linear_1315_3638)"
strokeWidth={2}
/>
<path
d="M32.3337 22.3333H15.667M23.167 25.6666H19.0003M15.667 20.8333L15.667 27.1666C15.667 28.1 15.667 28.5668 15.8486 28.9233C16.0084 29.2369 16.2634 29.4918 16.577 29.6516C16.9335 29.8333 17.4002 29.8333 18.3337 29.8333L29.667 29.8333C30.6004 29.8333 31.0671 29.8333 31.4236 29.6516C31.7372 29.4918 31.9922 29.2369 32.152 28.9233C32.3337 28.5668 32.3337 28.1 32.3337 27.1666V20.8333C32.3337 19.8999 32.3337 19.4332 32.152 19.0766C31.9922 18.763 31.7372 18.5081 31.4236 18.3483C31.0671 18.1666 30.6004 18.1666 29.667 18.1666L18.3337 18.1666C17.4002 18.1666 16.9335 18.1666 16.577 18.3483C16.2634 18.5081 16.0084 18.763 15.8486 19.0766C15.667 19.4332 15.667 19.8999 15.667 20.8333Z"
stroke="url(#paint2_linear_1315_3638)"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<defs>
<linearGradient
id="paint0_linear_1315_3638"
x1="-3.26045"
y1="15.4021"
x2="45.7807"
y2="50.1547"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset={1} stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint1_linear_1315_3638"
x1="1.28296"
y1="16.835"
x2="42.1506"
y2="45.7956"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset={1} stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint2_linear_1315_3638"
x1="14.5349"
y1="21.9102"
x2="27.1676"
y2="34.6989"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset={1} stopColor="#5204BF" />
</linearGradient>
</defs>
</svg>
);
}
@@ -0,0 +1,98 @@
export function GiftIcon(props: {
bg: string;
className?: string;
}) {
return (
<svg
width={48}
height={48}
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<rect width={48} height={48} rx={16} fill={props.bg} />
<rect
x={1}
y={1}
width={46}
height={46}
rx={15}
stroke="url(#paint0_linear_1214_43510)"
strokeOpacity="0.2"
strokeWidth={2}
/>
<rect x={5} y={5} width={38} height={38} rx={11} fill={props.bg} />
<rect
x={5}
y={5}
width={38}
height={38}
rx={11}
stroke="url(#paint1_linear_1214_43510)"
strokeWidth={2}
/>
<g clipPath="url(#clip0_1214_43510)">
<path
d="M23.9993 19.0001V32.3334M23.9993 19.0001H21.0529C20.6188 19.0001 20.2024 18.8245 19.8955 18.5119C19.5885 18.1994 19.416 17.7754 19.416 17.3334C19.416 16.8914 19.5885 16.4675 19.8955 16.1549C20.2024 15.8423 20.6188 15.6667 21.0529 15.6667C23.3446 15.6667 23.9993 19.0001 23.9993 19.0001ZM23.9993 19.0001H26.9458C27.3799 19.0001 27.7963 18.8245 28.1032 18.5119C28.4102 18.1994 28.5827 17.7754 28.5827 17.3334C28.5827 16.8914 28.4102 16.4675 28.1032 16.1549C27.7963 15.8423 27.3799 15.6667 26.9458 15.6667C24.6541 15.6667 23.9993 19.0001 23.9993 19.0001ZM30.666 23.1667V29.6667C30.666 30.6002 30.666 31.0669 30.4844 31.4234C30.3246 31.737 30.0696 31.992 29.756 32.1518C29.3995 32.3334 28.9328 32.3334 27.9993 32.3334L19.9993 32.3334C19.0659 32.3334 18.5992 32.3334 18.2427 32.1518C17.9291 31.992 17.6741 31.737 17.5143 31.4234C17.3327 31.0669 17.3327 30.6002 17.3327 29.6667V23.1667M15.666 20.3334L15.666 21.8334C15.666 22.3001 15.666 22.5335 15.7568 22.7117C15.8367 22.8685 15.9642 22.996 16.121 23.0759C16.2993 23.1667 16.5326 23.1667 16.9993 23.1667L30.9994 23.1667C31.4661 23.1667 31.6994 23.1667 31.8777 23.0759C32.0345 22.996 32.162 22.8685 32.2419 22.7117C32.3327 22.5335 32.3327 22.3001 32.3327 21.8334V20.3334C32.3327 19.8667 32.3327 19.6333 32.2419 19.4551C32.162 19.2983 32.0345 19.1708 31.8777 19.0909C31.6994 19.0001 31.4661 19.0001 30.9993 19.0001L16.9993 19.0001C16.5326 19.0001 16.2993 19.0001 16.121 19.0909C15.9642 19.1708 15.8367 19.2983 15.7568 19.4551C15.666 19.6333 15.666 19.8667 15.666 20.3334Z"
stroke="url(#paint2_linear_1214_43510)"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_1214_43510"
x1="-3.26045"
y1="15.4021"
x2="45.7807"
y2="50.1547"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset={1} stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint1_linear_1214_43510"
x1="1.28296"
y1="16.835"
x2="42.1506"
y2="45.7956"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset={1} stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint2_linear_1214_43510"
x1="14.5339"
y1="21.0147"
x2="31.5621"
y2="33.0816"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset={1} stopColor="#5204BF" />
</linearGradient>
<clipPath id="clip0_1214_43510">
<rect
width={20}
height={20}
fill="white"
transform="translate(14 14)"
/>
</clipPath>
</defs>
</svg>
);
}
@@ -0,0 +1,61 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import type { ChainMetadata } from "thirdweb/chains";
import { ChainIcon } from "../../../../components/server/chain-icon";
import { PayModalButton } from "../client/PayModal";
import { CreditCardIcon } from "../icons/CreditCardIcon";
import { SectionTitle } from "./SectionTitle";
export function BuyFundsSection(props: { chain: ChainMetadata }) {
const sanitizedChainName = props.chain.name.replace("Mainnet", "").trim();
return (
<section>
<SectionTitle title="Buy Funds" />
<div className="border border-border px-4 py-10 rounded-lg flex justify-center">
<div className="max-w-[520px] flex flex-col items-center ">
<div className="flex items-center">
<ChainIcon
className="size-12 -mr-2 rounded-full border p-1"
iconUrl={props.chain.icon?.url}
/>
<CreditCardIcon
bg="hsl(var(--background))"
className="size-12 -ml-2"
/>
</div>
<div className="h-6" />
<h2 className="text-lg tracking-tight font-semibold text-center px-4">
Buy Funds on {sanitizedChainName} using thirdweb Pay
</h2>
<div className="h-2" />
<p className="text-secondary-foreground text-sm max-w-[520px] text-center px-4">
Instantly onramp, bridge & swap into {sanitizedChainName} with any
token.
</p>
<div className="h-8" />
<PayModalButton
chainId={props.chain.chainId}
label={`Buy Funds on ${sanitizedChainName}`}
/>
<div className="h-4" />
<Link
href="https://portal.thirdweb.com/connect/pay/overview"
className="inline-flex gap-1.5 items-center text-sm text-secondary-foreground hover:text-foreground"
>
Learn more about thirdweb Pay{" "}
<ExternalLinkIcon className="size-3" />
</Link>
</div>
</div>
</section>
);
}
@@ -0,0 +1,52 @@
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import React from "react";
import type { ChainMetadata } from "thirdweb/chains";
import { ChainLiveStats } from "../client/live-stats";
import { SectionTitle } from "./SectionTitle";
import { PrimaryInfoItem } from "./primary-info-item";
export function ChainOverviewSection(props: { chain: ChainMetadata }) {
const { chain } = props;
return (
<section>
<SectionTitle title="Chain Overview" />
<div className="grid grid-cols-1 gap-6 lg:gap-8 md:grid-cols-2 lg:grid-cols-3 border rounded-lg p-4 md:p-6">
{/* Info */}
{chain.infoURL && (
<PrimaryInfoItem title="Info">
<Link
href={chain.infoURL}
target="_blank"
className="inline-flex items-center gap-1.5 hover:text-link-foreground"
>
{new URL(chain.infoURL).hostname}
<ExternalLinkIcon className="size-4" />
</Link>
</PrimaryInfoItem>
)}
{/* Chain Id */}
<PrimaryInfoItem title="Chain ID">
<CopyTextButton
textToCopy={chain.chainId.toString()}
textToShow={chain.chainId.toString()}
tooltip="Copy Chain ID"
variant="ghost"
className="-translate-x-2 py-1 px-2 text-base"
iconClassName="size-4"
copyIconPosition="right"
/>
</PrimaryInfoItem>
{/* Native token */}
<PrimaryInfoItem title="Native Token">
{chain.nativeCurrency.name} ({chain.nativeCurrency.symbol})
</PrimaryInfoItem>
{chain.rpc[0] && <ChainLiveStats rpc={chain.rpc[0]} />}
</div>
</section>
);
}
@@ -0,0 +1,30 @@
import { Button } from "@/components/ui/button";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { SectionTitle } from "./SectionTitle";
export function ClaimChainSection() {
return (
<section>
<SectionTitle title="Claim Chain" />
<div className="border p-4 rounded-lg">
<p className="mb-1"> This chain page is unclaimed </p>
<p className="text-secondary-foreground text-sm mb-5">
Are you the owner of this ecosystem? Claim this page to add additional
information on this page to help your developers get started!
</p>
<Button variant="outline" asChild>
<Link
className="gap-2"
href="https://share.hsforms.com/1o01TyfsZRAao2eCrzuXSVgea58c"
target="_blank"
>
<span>Claim Chain</span>
<ExternalLinkIcon className="size-4 -mt-[3px]" />
</Link>
</Button>
</div>
</section>
);
}
@@ -0,0 +1,57 @@
import { getIpAddress } from "lib/ip";
import { cacheTtl } from "lib/redis";
import type { ChainMetadata } from "thirdweb/chains";
import { ChainIcon } from "../../../../components/server/chain-icon";
import { FaucetButton } from "../client/FaucetButton";
import { GiftIcon } from "../icons/GiftIcon";
import { SectionTitle } from "./SectionTitle";
const amountToGive = "0.1";
export async function FaucetSection(props: { chain: ChainMetadata }) {
const { chain } = props;
// Check eligibilty.
const ipAddress = getIpAddress();
const cacheKey = `testnet-faucet:${chain.chainId}:${ipAddress}`;
const ttlSeconds = await cacheTtl(cacheKey);
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
return (
<section>
<SectionTitle title="Faucet" />
<div className="border border-border px-4 py-10 rounded-lg flex justify-center">
<div className="max-w-[520px] w-full flex flex-col items-center ">
<div className="flex items-center">
<ChainIcon
className="size-12 -mr-2 rounded-full border p-1"
iconUrl={props.chain.icon?.url}
/>
<GiftIcon bg="hsl(var(--background))" className="size-12 -ml-2" />
</div>
<div className="h-6" />
<h2 className="text-lg tracking-tight font-semibold text-center px-4">
{sanitizedChainName} Faucet
</h2>
<div className="h-2" />
<p className="text-secondary-foreground text-sm max-w-[520px] text-center px-4">
Get free {chain.nativeCurrency.symbol} fast and reliably.{" "}
{amountToGive} {chain.nativeCurrency.symbol}/day.
</p>
<div className="h-8" />
<FaucetButton
chain={chain}
ttlSeconds={ttlSeconds}
amount={amountToGive}
/>
</div>
</div>
</section>
);
}
@@ -0,0 +1,5 @@
export function SectionTitle(props: { title: string }) {
return (
<h2 className="text-xl tracking-tight mb-2 font-semibold">{props.title}</h2>
);
}
@@ -0,0 +1,53 @@
import { CircleCheckIcon } from "lucide-react";
import Link from "next/link";
import { products } from "../../../../components/server/products";
import type { ChainMetadataWithServices } from "../../../../types/chain";
import { SectionTitle } from "./SectionTitle";
export function SupportedProductsSection(props: {
services: ChainMetadataWithServices["services"];
}) {
const enabledProducts = products.filter((product) => {
return props.services.find(
(service) => service.service === product.id && service.enabled,
);
});
if (enabledProducts.length === 0) {
return null;
}
return (
<section>
<SectionTitle title="thirdweb Products" />
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{enabledProducts.map((product) => {
return (
<div
key={product.id}
className="border rounded-lg p-4 flex gap-3 relative hover:bg-secondary transition-colors duration-300 pr-8"
>
<CircleCheckIcon className="size-5 text-success-text absolute top-4 right-4" />
<product.icon className="size-5 shrink-0 mt-0.5" />
<div>
<h3 className="font-medium mb-1.5">
<Link
href={product.link}
className="before:absolute before:inset-0"
target="_blank"
>
{" "}
{product.name}{" "}
</Link>
</h3>
<p className="text-sm text-secondary-foreground">
{product.description}
</p>
</div>
</div>
);
})}
</div>
</section>
);
}
@@ -0,0 +1,65 @@
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { defineChain } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ChainIcon } from "../../../../components/server/chain-icon";
import { MobileMenu } from "../client/MobileMenu";
import { AddChainToWallet } from "../client/add-chain-to-wallet";
type ChainHeaderProps = {
headerImageUrl?: string;
logoUrl?: string;
chain: ChainMetadata;
};
// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
export function ChainHeader(props: ChainHeaderProps) {
return (
// force the banner image to be 4:1 aspect ratio and full-width on mobile devices
<div className="flex flex-col">
{!props.headerImageUrl && <div className="h-8 md:hidden" />}
<AspectRatio
ratio={props.headerImageUrl ? 4 : 8}
className="border-b border-border -mx-4 lg:-mx-6"
>
{props.headerImageUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={props.headerImageUrl}
alt=""
className="object-cover object-center h-full w-full"
/>
)}
</AspectRatio>
{/* below header */}
<div className="relative flex flex-row justify-end items-end">
{/* chain logo */}
<ChainIcon
iconUrl={props.logoUrl}
className="p-2 lg:p-4 absolute top-0 left-0 size-20 lg:size-36 rounded-full bg-background -translate-y-[50%] overflow-hidden border border-border"
/>
{/* action group */}
<div className="pt-3 lg:pt-6">
{/* Desktop only */}
<div className="hidden lg:flex flex-row gap-2">
<AddChainToWallet chain={defineChain(props.chain)} />
<Button variant="primary" asChild>
<Link href="https://thirdweb.com/dashboard" target="_blank">
Get started with thirdweb
</Link>
</Button>
</div>
{/* Mobile Menu */}
<MobileMenu chain={props.chain} />
</div>
</div>
</div>
);
}
@@ -11,36 +11,33 @@ export type ChainCTAProps = {
export function ChainCTA(props: ChainCTAProps) {
return (
<div className="container">
<div
className="w-full overflow-hidden rounded-xl shadow-lg bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url(${props.backgroundImageUrl})`,
}}
>
<div className="grid items-center justify-center gap-6 bg-gradient-to-r from-accent/0 to-accent/0 p-8 sm:p-12 md:grid-cols-[1fr_auto] md:gap-12 lg:p-16">
<div className="space-y-4 text-center md:text-left">
<h2 className="text-3xl font-bold tracking-tighter text-white sm:text-4xl md:text-5xl">
{props.title}
</h2>
{props.description && (
<p className="max-w-[600px] md:text-lg text-white">
{props.description}
</p>
)}
</div>
<Button asChild size="lg" className="invert dark:invert-0">
<Link
href={props.buttonLink}
prefetch={false}
target={
props.buttonLink.startsWith("http") ? "_blank" : undefined
}
>
{props.buttonText}
</Link>
</Button>
<div
className="bg-cover bg-center bg-no-repeat rounded-xl border border-border overflow-hidden"
style={{
backgroundImage: `url(${props.backgroundImageUrl})`,
}}
>
<div className="grid items-center justify-center gap-6 p-8 md:grid-cols-[1fr_auto] md:gap-12 bg-gradient-to-r from-background to-background/90">
<div className="space-y-4 text-center md:text-left">
<h2 className="text-2xl md:text-3xl font-bold tracking-tighter text-foreground md:max-w-[85%]">
{props.title}
</h2>
{props.description && (
<p className="max-w-[600px] text-secondary-foreground leading-relaxed text-sm md:text-base">
{props.description}
</p>
)}
</div>
<Button asChild>
<Link
href={props.buttonLink}
prefetch={false}
target={props.buttonLink.startsWith("http") ? "_blank" : undefined}
className="font-semibold"
>
{props.buttonText}
</Link>
</Button>
</div>
</div>
);
@@ -1,22 +1,22 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import type { ChainMetadataWithServices } from "../../../../types/chain";
import { SectionTitle } from "./SectionTitle";
export function ExplorersSection(props: {
explorers: NonNullable<ChainMetadataWithServices["explorers"]>;
}) {
return (
<div>
<h2 className="text-base text-muted-foreground mb-2 font-medium">
Explorers
</h2>
<section>
<SectionTitle title="Explorers" />
<div className="grid grid-cols-1 gap-3 md:gap-4 md:grid-cols-2 lg:grid-cols-3">
{props.explorers.map((explorer) => {
return (
<div
key={explorer.url}
className="p-4 border rounded-xl relative bg-secondary hover:bg-muted"
className="p-4 border rounded-xl relative hover:bg-secondary"
>
<ExternalLinkIcon className="size-4 absolute top-4 right-4 text-muted-foreground" />
<h3 className="mb-1 text-base font-semibold capitalize">
{explorer.name}
</h3>
@@ -29,11 +29,10 @@ export function ExplorersSection(props: {
? explorer.url.slice(0, -1)
: explorer.url}
</Link>
<ExternalLinkIcon className="size-4 absolute top-4 right-4 text-muted-foreground" />
</div>
);
})}
</div>
</div>
</section>
);
}
@@ -1,44 +0,0 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
export function FaucetsSection(props: { faucets: readonly string[] }) {
return (
<div>
<h2 className="text-base text-muted-foreground mb-2 font-medium">
Faucets
</h2>
<div className="grid grid-cols-1 gap-3 md:gap-4 md:grid-cols-2 lg:grid-cols-3">
{props.faucets.map((faucet) => {
const url = new URL(faucet);
const hostnameSplit = url.hostname.split(".");
const tld = hostnameSplit.pop();
const domain = hostnameSplit.pop();
const displayTitle = `${domain}.${tld}`;
let displayUrl = url.port + url.hostname + url.pathname;
displayUrl = displayUrl.endsWith("/")
? displayUrl.slice(0, -1)
: displayUrl;
return (
<div
key={faucet}
className="p-4 border rounded-xl relative bg-secondary hover:bg-muted"
>
<h3 className="mb-1 text-base font-semibold capitalize">
{displayTitle}
</h3>
<Link
href={faucet}
target="_blank"
className="text-sm flex gap-1.5 items-center before:absolute before:inset-0 before:z-0 text-muted-foreground"
>
{displayUrl}
</Link>
<ExternalLinkIcon className="size-4 absolute top-4 right-4 text-muted-foreground" />
</div>
);
})}
</div>
</div>
);
}
@@ -1,41 +0,0 @@
import { Button } from "@/components/ui/button";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import type React from "react";
export function InfoCard(props: {
title: string;
links?: { label: string; href: string }[];
children: React.ReactNode;
}) {
return (
<div className="border rounded-xl px-4 py-4 bg-secondary relative">
<h2 className="text-xl font-semibold tracking-tight mb-4">
{props.title}
</h2>
<div className="[&_p]:mb-3 [&_p]:text-secondary-foreground max-w-[1000px]">
{props.children}
</div>
{props.links && (
<div className="flex flex-col lg:flex-row mt-7 gap-3">
{props.links.map((link) => {
return (
<Button asChild variant="outline" key={link.label}>
<Link
href={link.href}
className="items-center gap-2"
target="_blank"
>
{link.label}{" "}
<ExternalLinkIcon className="size-3 text-muted-foreground" />
</Link>
</Button>
);
})}
</div>
)}
</div>
);
}
@@ -4,14 +4,14 @@ export function PrimaryInfoItem(props: {
children: React.ReactNode;
}) {
return (
<section className="border-b pb-4 lg:pb-6">
<div>
<div className="flex items-center gap-2">
<h2 className="text-base text-muted-foreground font-medium">
<h3 className="text-base text-muted-foreground font-medium">
{props.title}
</h2>
</h3>
{props.titleIcon}
</div>
{props.children}
</section>
</div>
);
}
@@ -0,0 +1,70 @@
import { ArrowLeftIcon } from "lucide-react";
import Link from "next/link";
import type React from "react";
import type { ChainMetadata } from "thirdweb/chains";
import { Button } from "../../../../../../../@/components/ui/button";
import { Separator } from "../../../../../../../@/components/ui/separator";
import { NavLink } from "../client/nav-link.client";
export type SidebarContentProps = {
chain: ChainMetadata;
};
export function SidebarContent(props: SidebarContentProps) {
return (
<div className="p-4 md:pt-8 sticky top-0">
{/* Desktop only */}
<div className="lg:flex items-center gap-1.5 mb-5 hidden flex-wrap">
<Link
href="/chainlist"
className="text-secondary-foreground hover:text-foreground text-sm"
>
Chainlist
</Link>
<span className="text-secondary-foreground text-sm"> / </span>
<Link href={`/${props.chain.slug}`} className="text-foreground text-sm">
{props.chain.name.replace("Mainnet", "")}
</Link>
</div>
<div className="flex flex-col gap-2 lg:gap-4 lg:border-l-2 lg:pl-1">
<SidebarNavLink href={`/${props.chain.slug}`} label="Overview" />
<SidebarNavLink
href={`/${props.chain.slug}/popular`}
label="Popular Contracts"
/>
</div>
{/* mobile obly */}
<div className="lg:hidden">
<Separator className="mt-10 mb-4" />
<SidebarNavLink
href="/chainlist"
label={
<>
<ArrowLeftIcon className="size-4" />
<span className="text-secondary-foreground">View all chains</span>
</>
}
/>
</div>
</div>
);
}
function SidebarNavLink(props: {
href: string;
label: React.ReactNode;
}) {
return (
<Button size="sm" variant="ghost" asChild>
<NavLink
href={props.href}
className="!text-left justify-start flex gap-2 h-auto py-3.5 lg:text-secondary-foreground before:absolute before:h-full relative before:w-[2px] before:-left-1.5 lg:py-0 lg:h-auto lg:hover:bg-transparent"
activeClassName="bg-accent lg:text-foreground lg:bg-transparent lg:before:bg-link-foreground "
>
{props.label}
</NavLink>
</Button>
);
}
@@ -1,104 +0,0 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { FaReact } from "react-icons/fa";
import { FaUnity } from "react-icons/fa6";
import { SiDotnet, SiSolidity, SiTypescript } from "react-icons/si";
import { getChain } from "../../../utils";
import { InfoCard } from "../components/server/info-card";
type SDKInfo = {
name: string;
href: string;
icon: React.FC<{ className?: string }>;
};
const sdks: SDKInfo[] = [
{
name: "TypeScript",
href: "https://portal.thirdweb.com/typescript/v5",
icon: SiTypescript,
},
{
name: "React",
href: "https://portal.thirdweb.com/typescript/v5/react",
icon: FaReact,
},
{
name: "React Native",
href: "https://portal.thirdweb.com/typescript/v5/react-native",
icon: FaReact,
},
{
name: "Unity",
href: "https://portal.thirdweb.com/unity",
icon: FaUnity,
},
{
name: "Solidity",
href: "https://portal.thirdweb.com/contracts/build/overview",
icon: SiSolidity,
},
{
name: ".NET SDK",
href: "https://portal.thirdweb.com/dotnet",
icon: SiDotnet,
},
];
export default async function Page(props: { params: { chain_id: string } }) {
const chain = await getChain(props.params.chain_id);
const enabled = chain.services.find(
(s) => s.service === "connect-sdk",
)?.enabled;
if (!enabled) {
redirect(`/${props.params.chain_id}`);
}
return (
<div>
<InfoCard
title="Connect SDK"
links={[
{
label: "Learn More",
href: "https://portal.thirdweb.com/connect",
},
]}
>
<p>
Connect is the complete toolkit for connecting every user to your
application.
</p>
<p>
It features customizable onboarding flows, self-custodial in-app
wallets, account abstraction, onramps, and more.
</p>
</InfoCard>
<div className="h-10" />
<h3 className="text-foreground text-3xl tracking-tighter font-semibold">
Get Started
</h3>
<div className="h-3" />
<div className="grid gap-3 md:gap-4 grid-cols-1 md:grid-cols-3 lg:grid-cols-4">
{sdks.map((sdk) => {
return (
<Link
key={sdk.name}
href={sdk.href}
target="_blank"
className="border p-4 bg-secondary rounded-lg hover:bg-muted font-medium flex items-center gap-3"
>
<sdk.icon className="size-5 text-muted-foreground" />
{sdk.name}
</Link>
);
})}
</div>
</div>
);
}
@@ -1,35 +0,0 @@
import { redirect } from "next/navigation";
import { getChain } from "../../../utils";
import { InfoCard } from "../components/server/info-card";
export default async function Page(props: { params: { chain_id: string } }) {
const chain = await getChain(props.params.chain_id);
const enabled = chain.services.find((s) => s.service === "engine")?.enabled;
if (!enabled) {
redirect(`/${props.params.chain_id}`);
}
return (
<div>
<InfoCard
title="thirdweb Engine"
links={[
{
label: "Setup Engine Instance",
href: "/dashboard/engine",
},
{
label: "Learn More",
href: "https://portal.thirdweb.com/engine",
},
]}
>
<p>
thirdweb Engine is a backend HTTP server that reads, writes, and
deploys contracts at production scale.
</p>
</InfoCard>
</div>
);
}
@@ -1,205 +0,0 @@
"use client";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import { useMutation } from "@tanstack/react-query";
import type { ChainMetadataWithServices } from "app/(dashboard)/(chain)/types/chain";
import { thirdwebClient } from "lib/thirdweb-client";
import { useRouter } from "next/navigation";
import Confetti from "react-confetti";
import { toast } from "sonner";
import { type ISpinWheelProps, SpinWheel } from "spin-wheel-game";
import { defineChain, toUnits } from "thirdweb";
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
const segments = [
{ segmentText: "Come back tomorrow", segColor: "#f771c9" }, // Mid-dark, optimized for dark mode
{ segmentText: "0.1", segColor: "#f65ac0" }, // Lightest, optimized for dark mode
{ segmentText: "Come back tomorrow", segColor: "#f542b7" }, // Slightly darker, optimized
{ segmentText: "0.01", segColor: "#f32bae" }, // Mid-light, optimized for dark mode
{ segmentText: "0.05", segColor: "#f213a5" }, // Mid, optimized for dark mode
{ segmentText: "0.01", segColor: "#da1195" }, // Mid-dark, optimized for dark mode
{ segmentText: "0.05", segColor: "#c20f84" }, // Darker, optimized for dark mode
{ segmentText: "0.01", segColor: "#a90d73" }, // Even darker, optimized
{ segmentText: "0.05", segColor: "#910b63" }, // Near darkest, optimized for dark mode
{ segmentText: "0.01", segColor: "#790a53" }, // Lightest, optimized for dark mode
{ segmentText: "0.05", segColor: "#610842" }, // Slightly darker, optimized
{ segmentText: "0.01", segColor: "#490632" }, // Mid-light, optimized for dark mode
];
function formatTime(seconds: number) {
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return rtf.format(+hours, "hour");
}
if (minutes > 0) {
return rtf.format(+minutes, "minute");
}
return rtf.format(+seconds, "second");
}
export function TestnetSpinWheel({
chain,
ttlSeconds,
}: {
chain: ChainMetadataWithServices;
ttlSeconds: number;
}) {
const address = useActiveAccount()?.address;
const chainId = chain.chainId;
const definedChain = defineChain(chainId);
const { data: evmBalance, isLoading } = useWalletBalance({
address: THIRDWEB_ENGINE_FAUCET_WALLET,
chain: definedChain,
client: thirdwebClient,
});
const router = useRouter();
const claimMutation = useMutation({
mutationFn: async (result: string) => {
const amount = result === "Come back tomorrow" ? "0.01" : result;
const requestBody = {
chainId: chainId,
toAddress: address,
amount: amount,
};
const response = await fetch("/api/testnet-faucet/claim", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const error = await response.json();
throw error.error;
}
},
onError: async (error: string) => {
toast.error(error);
},
onSuccess: () => {
router.refresh();
const successMessage =
claimMutation.variables === "Come back tomorrow"
? "You're not lucky today, but don't worry you got 0.01 💜"
: `${claimMutation.variables} ${chain?.nativeCurrency?.name} sent to ${address}`;
toast.success(successMessage);
},
});
const isFaucetEmpty =
evmBalance?.value !== undefined && evmBalance.value < toUnits("1", 17);
const faucetMessage = isFaucetEmpty ? (
<div>
<p>
There is no {chain.nativeCurrency.symbol} available. Please send funds
to the following address:
</p>
<div className="h-2" />
<CopyTextButton
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
tooltip="Copy"
className="text-lg -translate-x-2 py-0.5"
copyIconPosition="right"
/>
</div>
) : (
<p>You may claim {chain.nativeCurrency.symbol} once every 24 hours.</p>
);
const claimButtonCta =
ttlSeconds > 0
? `Your next claim is available ${formatTime(ttlSeconds)}`
: `Spin for ${chain.nativeCurrency.symbol}`;
const closeButtonCta = ttlSeconds > 0 ? "Done" : "Close";
const spinWheelProps: ISpinWheelProps = {
segments,
onFinished: claimMutation.mutateAsync,
primaryColor: "black",
contrastColor: "white",
buttonText: "💜",
isOnlyOnce: false,
size: 290,
// Used to randomize result.
upDuration: Math.random() * 100 + 100,
downDuration: 600,
arrowLocation: "top",
showTextOnSpin: false,
isSpinSound: false,
};
if (!address) return <CustomConnectWallet />;
return (
<>
{isLoading ? (
<Skeleton className="h-20" />
) : (
<>
<div className="mb-3">{faucetMessage}</div>
<Dialog>
<DialogTrigger asChild>
{!isFaucetEmpty && (
<Button className="min-w-48" disabled={ttlSeconds > 0}>
{claimButtonCta}
</Button>
)}
</DialogTrigger>
<DialogContent className="w-auto h-fit max-w-none">
{claimMutation.isSuccess && (
<Confetti className="w-full h-full" />
)}
<DialogHeader>
<DialogTitle>
Spin to claim {chain.nativeCurrency.symbol} on {chain.name}
</DialogTitle>
<DialogDescription>
<p className="py-2">
Click the wheel to get your claim amount.
</p>
<div className="p-4">
<SpinWheel {...spinWheelProps} />
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex justify-between">
<DialogClose asChild>
<Button variant="outline">
<span className="text-muted-foreground">
{closeButtonCta}
</span>
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)}
</>
);
}
@@ -1,94 +0,0 @@
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
import { ClientOnly } from "components/ClientOnly/ClientOnly";
import { getIpAddress } from "lib/ip";
import { cacheTtl } from "lib/redis";
import { redirect } from "next/navigation";
import type { Metadata } from "next/types";
import { getChain } from "../../../utils";
import { InfoCard } from "../components/server/info-card";
import { TestnetSpinWheel } from "./components/SpinWheel";
export async function generateMetadata({
params,
}: { params: { chain_id: string } }): Promise<Metadata> {
const chain = await getChain(params.chain_id);
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
return {
title: `thirdweb ${sanitizedChainName} faucet`,
description: `Get ${chain.nativeCurrency.symbol} testnet funds on ${chain.name}. Powered by thirdweb.`,
};
}
export default async function Page(props: { params: { chain_id: string } }) {
const chain = await getChain(props.params.chain_id);
if (!chain.testnet) {
redirect(`/${props.params.chain_id}`);
}
// Check eligibilty.
const ipAddress = getIpAddress();
const cacheKey = `testnet-faucet:${chain.chainId}:${ipAddress}`;
const ttlSeconds = await cacheTtl(cacheKey);
return (
<>
<div className="items-center">
<h3 className="mb-3 text-lg font-semibold"> Faucet </h3>
<ClientOnly ssr={null}>
<TestnetSpinWheel chain={chain} ttlSeconds={ttlSeconds} />
</ClientOnly>
<div className="h-10" />
<InfoCard title="Resources">
<h3 className="mb-2 text-lg font-semibold">What is a faucet?</h3>
<p>
An testnet faucet is an online service that provides free testnet
currency to web3 app and blockchain developers. This allows them to
experiment with and test smart contracts and decentralized
applications (dApps) on EVM testnets like {chain.name} without using
real cryptocurrency.
</p>
<h2>How often can I claim from the faucet?</h2>
<p>You may claim funds once per testnet chain every 24 hours.</p>
<h3 className="mb-2 text-lg font-semibold">
How can I refill the faucet?
</h3>
<p>Please send funds the following address:</p>
<div>
<CopyTextButton
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
tooltip="Copy"
copyIconPosition="right"
className="mb-2"
/>
</div>
<h3 className="mb-2 text-lg font-semibold">
How does the faucet work?
</h3>
<p>
When you request testnet funds, a multi-chain backend wallet
transfers a small amount of {chain.nativeCurrency.symbol} on{" "}
{chain.name} with{" "}
<a
href="https://thirdweb.com/engine"
target="_blank"
className="underline"
rel="noreferrer"
>
thirdweb Engine
</a>
.
</p>
</InfoCard>
</div>
</>
);
}
@@ -1,36 +1,22 @@
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import {
ArrowLeftIcon,
CircleAlertIcon,
ExternalLinkIcon,
TicketCheckIcon,
} from "lucide-react";
import { TicketCheckIcon } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { redirect } from "next/navigation";
import { defineChain } from "thirdweb";
import { StarButton } from "../../components/client/star-button";
import { ChainIcon } from "../../components/server/chain-icon";
import { getChain, getChainMetadata } from "../../utils";
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
import { ChainPageTabs } from "./components/client/tabs";
import { ChainCTA } from "./components/server/cta-card";
import { ExplorersSection } from "./components/server/explorer-section";
import { PrimaryInfoItem } from "./components/server/primary-info-item";
import { ChainHeader } from "./components/server/chain-header";
import { SidebarContent } from "./components/server/sidebar-content";
export async function generateMetadata(
{ params }: { params: { chain_id: string } },
// parent: ResolvingMetadata,
): Promise<Metadata> {
// read route params
// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
export async function generateMetadata({
params,
}: { params: { chain_id: string } }): Promise<Metadata> {
const chain = await getChain(params.chain_id);
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
const title = `${sanitizedChainName}: RPC and Chain Settings`;
const description = `Use the best ${sanitizedChainName} RPC and add to your wallet. Discover the chain ID, native token, explorers, and ${
@@ -53,226 +39,88 @@ export default async function ChainPageLayout({
}) {
const chain = await getChain(params.chain_id);
// Enable faucet for all testnet chains.
if (chain.testnet) {
chain.services = [
...chain.services,
{
service: "faucet",
enabled: true,
},
];
}
if (params.chain_id !== chain.slug) {
redirect(chain.slug);
}
const chainMetadata = await getChainMetadata(chain.chainId);
// true if we *have* chainMetadata for the chain
const isVerified = !!chainMetadata;
const isDeprecated = chain.status === "deprecated";
return (
<>
{isDeprecated && (
<>
<div className="bg-destructive">
<div className="container py-4 flex flex-row items-center gap-4 text-white">
<CircleAlertIcon className="size-6 flex-shrink-0" />
<h3 className="font-semibold">
This chain has been marked as deprecated. Some or all services
may no longer function.
</h3>
<div className="container flex flex-row h-full">
<aside className="w-[280px] flex-shrink-0 hidden lg:block">
<SidebarContent chain={chain} />
</aside>
<div className="flex flex-col w-full lg:px-6 lg:border-x pb-10">
{/* Icon + Background */}
<ChainHeader
headerImageUrl={chainMetadata?.headerImgUrl}
logoUrl={chain.icon?.url}
chain={chain}
/>
<div className="h-4 md:h-8" />
<div className="flex flex-col gap-3 md:gap-2">
{/* Gas Sponsored badge - Mobile */}
{chainMetadata?.gasSponsored && (
<div className="md:hidden flex">
<GasSponseredBadge />
</div>
</div>
<Separator />
</>
)}
<section className="flex flex-col h-full gap-8">
{/* Header */}
<header
className={cn(
"py-6 md:py-8 border-b relative overflow-hidden",
!chainMetadata?.gasSponsored && !isVerified && "md:pb-2",
chainMetadata?.headerImgUrl && "md:py-10",
)}
>
{/* header background image shenanigans */}
<div className="absolute top-0 left-0 right-0 bottom-0 -z-10">
<div
className="absolute top-0 left-0 right-0 bottom-0 bg-cover bg-center bg-no-repeat bg-secondary"
style={
chainMetadata?.headerImgUrl
? {
backgroundImage: `url(${chainMetadata.headerImgUrl})`,
}
: undefined
}
{/* Chain name */}
<div className="flex flex-row gap-1.5 items-center">
<h1 className="text-2xl font-semibold lg:text-3xl lg:font-semibold tracking-tight">
{chain.name}
</h1>
{/* Favorite */}
<StarButton
chainId={chain.chainId}
iconClassName="size-5"
className="p-1"
/>
<div className="absolute top-0 left-0 right-0 bottom-0 bg-ba bg-gradient-to-b from-black/25 to-black/0 shadow-inner" />
</div>
{/* end header shaningans */}
<div className="container flex flex-col md:flex-row justify-between md:items-center">
<div className="flex flex-col gap-2 md:gap-6">
<Link
href="/chainlist"
className={cn(
"text-foreground mt-4 inline-flex items-center gap-1",
// if we have a header image always light text
chainMetadata?.headerImgUrl && "text-white",
)}
>
<ArrowLeftIcon className="size-5" />
Chainlist
</Link>
<div className="flex gap-3 md:gap-5 items-center">
{chain.icon?.url && (
<div
className={cn(
"bg-secondary p-2 border rounded-full overflow-hidden",
)}
>
<ChainIcon
iconUrl={chain.icon.url}
className={cn(
"size-14 md:size-18 rounded-full",
// if we it's "mantle" chain, invert the icon in dark mode
chain.chainId === 5000 && "dark:invert",
)}
/>
</div>
)}
{/* Chain Name */}
<h1
className={cn(
"font-semibold tracking-tighter text-4xl md:text-6xl",
// if we have a header image always light text
chainMetadata?.headerImgUrl && "text-white",
)}
>
{chain.name}
</h1>
<StarButton chainId={chain.chainId} variant="secondary" />
{/* Gas Sponsored badge - Desktop */}
{chainMetadata?.gasSponsored && (
<div className="hidden md:block">
<GasSponseredBadge />
</div>
<div className="flex flex-row gap-2 md:gap-4 items-center h-8 mb-4 md:mb-6">
{chainMetadata?.gasSponsored && (
<Badge
variant="secondary"
className="text-accent-foreground pointer-events-none flex flex-row items-center h-full gap-1.5 border border-border"
>
<TicketCheckIcon className="size-5" />
<span className="font-bold text-xs uppercase">
gas sponsored
</span>
</Badge>
)}
</div>
</div>
<div className="flex flex-row md:flex-col gap-4">
<AddChainToWallet
chain={defineChain(chain)}
hasBackground={!!chainMetadata?.headerImgUrl}
/>
{isVerified ? null : (
<Button
className="w-full md:min-w-40"
variant="outline"
asChild
>
<Link
className="gap-2"
href="https://share.hsforms.com/1o01TyfsZRAao2eCrzuXSVgea58c"
target="_blank"
>
<span>Claim this chain</span>
<ExternalLinkIcon className="size-3" />
</Link>
</Button>
)}
</div>
</div>
</header>
{chainMetadata?.cta && <ChainCTA {...chainMetadata.cta} />}
<main className="container pb-20 flex-1">
{/* About section */}
{chainMetadata?.about && (
<>
<div className="border rounded-xl px-4 py-4 bg-card relative">
<h2 className="text-xl font-semibold tracking-tight mb-4">
About
</h2>
<div className="[&_p]:mb-3 [&_p]:text-card-foreground">
<pre className="text-wrap font-sans">
{chainMetadata.about}
</pre>
</div>
</div>
<div className="h-8" />
</>
)}
{/* Chain Overview */}
<div className="flex flex-col gap-10 pt-2">
{/* Info Grid */}
<div className="grid grid-cols-1 gap-4 lg:gap-6 md:grid-cols-2 lg:grid-cols-3">
{/* Info */}
{chain.infoURL && (
<PrimaryInfoItem title="Info">
<div className="flex items-center gap-1.5 hover:text-primary">
<Link
href={chain.infoURL}
target="_blank"
className="text-lg"
>
{new URL(chain.infoURL).hostname}
</Link>
<ExternalLinkIcon className="size-4" />
</div>
</PrimaryInfoItem>
)}
{/* Chain Id */}
<PrimaryInfoItem title="Chain ID">
<CopyTextButton
textToCopy={chain.chainId.toString()}
textToShow={chain.chainId.toString()}
tooltip="Copy Chain ID"
variant="ghost"
className="text-lg -translate-x-2 py-0.5"
copyIconPosition="right"
/>
</PrimaryInfoItem>
{/* Native token */}
<PrimaryInfoItem title="Native Token">
<div className="text-lg">
{chain.nativeCurrency.name} ({chain.nativeCurrency.symbol})
</div>
</PrimaryInfoItem>
</div>
{/* Explorers */}
{chain.explorers && chain.explorers.length > 0 && (
<ExplorersSection explorers={chain.explorers} />
)}
</div>
<div className="h-14" />
<ChainPageTabs
chainSlug={params.chain_id}
enabledServices={chain.services
.filter((s) => s.enabled)
.map((s) => s.service)}
/>
<div className="h-8" />
<div>{children}</div>
</main>
</section>
</>
{/* description */}
{chainMetadata?.about && (
<p className="text-secondary-foreground text-sm lg:text-base mb-2">
{chainMetadata.about}
</p>
)}
{/* mobile action row */}
<div className="sm:hidden w-full">
<div className="grid grid-cols-2 gap-2">
<AddChainToWallet chain={defineChain(chain)} />
<Button variant="primary">
<Link href="https://thirdweb.com/dashboard" target="_blank">
Get started
</Link>
</Button>
</div>
</div>
</div>
<div className="h-8" />
{children}
</div>
</div>
);
}
function GasSponseredBadge() {
return (
<div className="flex items-center gap-2 rounded-full text-[hsl(334,81.12%,69.65%)] bg-[hsla(335,57%,51%,0.2)] px-2.5 py-1">
<TicketCheckIcon className="size-4" />
<span className="font-medium text-xs">Gas Sponsored</span>
</div>
);
}
@@ -1,166 +1,100 @@
import { ChevronRight, ShieldCheckIcon } from "lucide-react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { ArrowRightIcon, CircleAlertIcon } from "lucide-react";
import Link from "next/link";
import {
type SortBy,
type TimeRange,
fetchTopContracts,
} from "../../../../../lib/search";
import { type SortBy, fetchTopContracts } from "../../../../../lib/search";
import { TrendingContractSection } from "../../../trending/components/trending-table";
import { getChain } from "../../utils";
import { InfoCard } from "./components/server/info-card";
import twPublisherImage from "./tw-publisher.png";
type ContractCardInfo = {
name: string;
description: string;
href: string;
};
// currently the metadata is hardcoded - TODO:fetch it
const popularContracts: ContractCardInfo[] = [
{
name: "NFT Drop",
description: "Release collection of unique NFTs for a set price",
href: "/thirdweb.eth/DropERC721",
},
{
name: "NFT Collection",
description: "Create a collection of unique NFTs",
href: "/thirdweb.eth/TokenERC721",
},
{
name: "Edition Drop",
description: "Release ERC1155 tokens for a set price",
href: "/thirdweb.eth/DropERC1155",
},
{
name: "Token",
description: "Create cryptocurrency compliant with ERC20 standard",
href: "/thirdweb.eth/TokenERC20",
},
{
name: "Edition",
description: "Create editions of ERC1155 tokens",
href: "/thirdweb.eth/TokenERC1155",
},
];
import { getChain, getChainMetadata } from "../../utils";
import { BuyFundsSection } from "./components/server/BuyFundsSection";
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
import { ClaimChainSection } from "./components/server/ClaimChainSection";
import { FaucetSection } from "./components/server/FaucetSection";
import { SupportedProductsSection } from "./components/server/SupportedProductsSection";
import { ChainCTA } from "./components/server/cta-card";
import { ExplorersSection } from "./components/server/explorer-section";
export default async function Page(props: {
params: { chain_id: string };
searchParams: { timeRange?: TimeRange; page?: number; sortBy?: SortBy };
searchParams: { page?: number; sortBy?: SortBy };
}) {
const chain = await getChain(props.params.chain_id);
const chainMetadata = await getChainMetadata(chain.chainId);
const isDeprecated = chain.status === "deprecated";
const topContracts = await fetchTopContracts({
chainId: chain.chainId,
timeRange: props.searchParams.timeRange,
page: props.searchParams.page,
sortBy: props.searchParams.sortBy,
perPage: 3,
timeRange: "month",
});
return (
<div>
<div className="h-4" />
{topContracts.length > 0 && (
<>
<div className="flex flex-col gap-4">
<h3 className="text-foreground text-2xl tracking-tighter font-semibold">
Popular Contracts on {chain.name.split(" ")[0]}
</h3>
<TrendingContractSection
topContracts={topContracts}
chainId={chain.chainId}
searchParams={props.searchParams}
/>
</div>
<div className="h-10" />
</>
<div className="flex flex-col gap-10">
{/* Custom CTA */}
{(chainMetadata?.cta?.title || chainMetadata?.cta?.description) && (
<ChainCTA {...chainMetadata.cta} />
)}
<div className="flex items-center justify-between">
<h3 className="text-foreground text-2xl tracking-tighter font-semibold">
Deploy Contracts on {chain.name.split(" ")[0]}
</h3>
<Link
href="/explore"
className="text-link-foreground inline-flex items-center gap-1 text-base hover:text-foreground font-medium"
>
View All
<ChevronRight className="size-5" />
</Link>
</div>
{/* Deprecated Alert */}
{isDeprecated && (
<section className="bg-destructive rounded-lg text-destructive-foreground border border-destructive-foreground/20">
<div className="container py-4 flex flex-row items-center gap-4">
<CircleAlertIcon className="size-6 flex-shrink-0" />
<h3 className="font-medium">
This chain has been marked as deprecated. Some or all services may
no longer function.
</h3>
</div>
</section>
)}
<div className="h-3" />
{/* Buy Funds */}
{chain.services.find((c) => c.service === "pay" && c.enabled) && (
<BuyFundsSection chain={chain} />
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 md:gap-4">
{popularContracts.map((c) => {
return (
<ContractCard
key={c.name}
name={c.name}
description={c.description}
href={c.href}
/>
);
})}
</div>
{/* Faucet */}
{chain.testnet && <FaucetSection chain={chain} />}
<div className="h-10" />
{/* Chain Overview */}
<ChainOverviewSection chain={chain} />
<InfoCard
title="thirdweb Contracts"
links={[
{
label: "Learn More",
href: "https://portal.thirdweb.com/contracts",
},
]}
>
<p>End-to-end tools for smart contract development</p>
<p>
Trusted, modular smart contracts that can be deployed securely on any
EVM chain
</p>
</InfoCard>
</div>
);
}
function ContractCard(props: ContractCardInfo) {
return (
<div className="border bg-secondary rounded-xl p-4 hover:bg-muted relative flex flex-col h-full shadow-sm min-h-[200px]">
<div className="text-success-text flex items-center gap-1 mb-4 text-sm font-medium">
<ShieldCheckIcon className="size-4 text-success-text" />
Audited
</div>
<h3 className="text-lg tracking-tight font-semibold text-foreground mb-1">
<Link
href={props.href}
className="before:absolute before:inset-0 before:z-0"
>
{props.name}
</Link>
</h3>
<p className="text-base text-muted-foreground mb-5">
{props.description}
</p>
<Link
className="inline-flex items-center gap-1.5 z-10 relative mt-auto text-muted-foreground hover:text-foreground"
href="/thirdweb.eth"
>
<Image
src={twPublisherImage}
alt="thirdweb.eth"
className="size-4 rounded-full"
/>
<p className="text-sm">thirdweb.eth</p>
</Link>
{/* Explorers */}
{chain.explorers && chain.explorers.length > 0 && (
<ExplorersSection explorers={chain.explorers} />
)}
{topContracts.length > 0 && (
<section>
<div className="mb-3 flex items-center justify-between">
<h2 className="text-xl tracking-tight font-semibold">
Popular Contracts
</h2>
<Button asChild variant="outline">
<Link
href={`/${chain.slug}/popular`}
className="flex gap-2 items-center"
>
See All
<ArrowRightIcon className="size-4" />
</Link>
</Button>
</div>
<TrendingContractSection
topContracts={topContracts}
chainId={chain.chainId}
searchParams={props.searchParams}
showPagination={false}
/>
</section>
)}
{chain.services.filter((s) => s.enabled).length > 0 && (
<SupportedProductsSection services={chain.services} />
)}
{/* Claim Chain */}
{!chainMetadata && <ClaimChainSection />}
</div>
);
}
@@ -1,26 +0,0 @@
"use client";
import { thirdwebClient } from "@/constants/client";
import { useTheme } from "next-themes";
import { useMemo } from "react";
import { defineChain } from "thirdweb";
import { PayEmbed } from "thirdweb/react";
export function CustomPayEmbed(props: { chainId: number }) {
const { resolvedTheme } = useTheme();
const theme = resolvedTheme === "dark" ? "dark" : "light";
const chain = useMemo(() => defineChain(props.chainId), [props.chainId]);
return (
<PayEmbed
client={thirdwebClient}
connectOptions={{ chain }}
theme={theme}
payOptions={{
prefillBuy: {
chain,
allowEdits: { chain: false, amount: true, token: true },
},
}}
/>
);
}
@@ -1,34 +0,0 @@
import { redirect } from "next/navigation";
import { getChain } from "../../../utils";
import { InfoCard } from "../components/server/info-card";
import { CustomPayEmbed } from "./components/pay-embed.client";
export default async function Page(props: { params: { chain_id: string } }) {
const chain = await getChain(props.params.chain_id);
const enabled = chain.services.find((s) => s.service === "pay")?.enabled;
if (!enabled) {
redirect(`/${props.params.chain_id}`);
}
return (
<div className="pb-20 flex flex-col xl:flex-row gap-8">
<CustomPayEmbed chainId={chain.chainId} />
<InfoCard
title="thirdweb Pay"
links={[
{
label: "Get Started",
href: "https://portal.thirdweb.com/connect/pay/overview",
},
]}
>
<p>
thirdweb Pay allows your users to purchase cryptocurrencies and
execute transactions with their credit/debit card, or with any token
via cross-chain routing.
</p>
</InfoCard>
</div>
);
}
@@ -0,0 +1,51 @@
import { CircleAlertIcon } from "lucide-react";
import React from "react";
import { type SortBy, fetchTopContracts } from "../../../../../../lib/search";
import { TrendingContractSection } from "../../../../trending/components/trending-table";
import { getChain } from "../../../utils";
export default async function Page(props: {
params: { chain_id: string };
searchParams: { page?: number; sortBy?: SortBy };
}) {
const chain = await getChain(props.params.chain_id);
const topContracts = await fetchTopContracts({
chainId: chain.chainId,
page: props.searchParams.page,
sortBy: props.searchParams.sortBy,
perPage: 15,
timeRange: "month",
});
return (
<section className="mt-2">
<h2 className="text-2xl tracking-tighter font-semibold mb-2">
Popular Contracts
</h2>
<p className="text-sm text-secondary-foreground">
Explore contracts on Ethereum and sort them by your preferred metrics
</p>
<div className="h-8" />
{topContracts.length > 0 && (
<TrendingContractSection
topContracts={topContracts}
chainId={chain.chainId}
searchParams={props.searchParams}
showPagination={true}
/>
)}
{topContracts.length === 0 && (
<div className="text-secondary-foreground text-lg flex border rounded-lg h-[200px] border-border items-center justify-center">
<div className="flex items-center gap-2">
<CircleAlertIcon className="size-5 text-destructive-text" />
No contracts found
</div>
</div>
)}
</section>
);
}
@@ -1,43 +0,0 @@
import { redirect } from "next/navigation";
import { getChain } from "../../../utils";
import { InfoCard } from "../components/server/info-card";
import { ChainLiveStats } from "./components/client/live-stats";
export default async function Page(props: { params: { chain_id: string } }) {
const chain = await getChain(props.params.chain_id);
const enabled = chain.services.find((s) => s.service === "rpc-edge")?.enabled;
if (!enabled) {
redirect(`/${props.params.chain_id}`);
}
return (
<div className="pb-20">
<InfoCard
title="thirdweb RPC Edge"
links={[
{
label: "Learn More",
href: "https://portal.thirdweb.com/infrastructure/rpc-edge/overview",
},
]}
>
<p>
Remote Procedure Call (RPC) Edge provides reliable access to querying
data and interacting with the blockchain through global edge RPCs
</p>
<p>
By default, thirdweb provides publicly available RPCs for over 1000+
EVM Networks
</p>
</InfoCard>
{chain.rpc[0] && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mt-10 gap-4">
<ChainLiveStats rpc={chain.rpc[0]} />
</div>
)}
</div>
);
}
@@ -0,0 +1,8 @@
// handle deprecated route
// redirect /<chain_id>/faucet to /<chain_id>
import { redirect } from "next/navigation";
export default function Page(props: { params: { chain_id: string } }) {
redirect(`/${props.params.chain_id}`);
}
@@ -35,7 +35,6 @@ export async function ChainListRow({
iconUrl,
}: ChainListRowProps) {
const chainMetadata = await getChainMetadata(chainId);
const productsWithoutFaucet = products.filter((p) => p.id !== "faucet");
return (
<tr className="border-b relative hover:bg-secondary">
<TableData>{favoriteButton}</TableData>
@@ -82,7 +81,7 @@ export async function ChainListRow({
<TableData>
<div className="flex flex-row gap-14 items-center w-[520px] ">
<div className="flex items-center gap-7 z-10">
{productsWithoutFaucet.map((p) => {
{products.map((p) => {
return (
<ProductIcon
key={p.name}
@@ -9,7 +9,7 @@ import { createThirdwebClient } from "thirdweb";
import { resolveScheme } from "thirdweb/storage";
const fallbackChainIcon =
"data:image/svg+xml;charset=UTF-8,%3csvg width='15' height='14' viewBox='0 0 15 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M7 8.04238e-07C5.1435 8.04238e-07 3.36301 0.737501 2.05025 2.05025C0.7375 3.36301 0 5.1435 0 7C0 7.225 -1.52737e-07 7.445 0.0349998 7.665C0.16385 9.0151 0.68213 10.2988 1.52686 11.3598C2.37158 12.4209 3.50637 13.2137 4.79326 13.642C6.0801 14.0702 7.4637 14.1153 8.7758 13.7719C10.0879 13.4285 11.2719 12.7113 12.184 11.7075C13.0961 10.7038 13.6969 9.4567 13.9135 8.1178C14.1301 6.7789 13.9531 5.406 13.4039 4.16587C12.8548 2.92574 11.9573 1.87184 10.8204 1.13228C9.6835 0.392721 8.3563 -0.000649196 7 8.04238e-07ZM7 1C8.581 1.00137 10.0975 1.62668 11.22 2.74V3.24C9.2438 2.55991 7.0956 2.56872 5.125 3.265C4.96758 3.1116 4.76997 3.00586 4.555 2.96H4.43C4.37 2.75 4.315 2.54 4.27 2.325C4.225 2.11 4.2 1.92 4.175 1.715C5.043 1.24658 6.0137 1.00091 7 1ZM5.5 3.935C7.3158 3.32693 9.2838 3.34984 11.085 4C10.8414 5.2703 10.3094 6.4677 9.53 7.5C9.312 7.4077 9.0707 7.3855 8.8395 7.4366C8.6083 7.4877 8.3988 7.6094 8.24 7.785C8.065 7.685 7.89 7.585 7.74 7.47C6.7307 6.7966 5.8877 5.9023 5.275 4.855C5.374 4.73221 5.4461 4.58996 5.4866 4.43749C5.5271 4.28502 5.5351 4.12575 5.51 3.97L5.5 3.935ZM3.5 2.135C3.5 2.24 3.53 2.35 3.55 2.455C3.595 2.675 3.655 2.89 3.715 3.105C3.52353 3.21838 3.36943 3.38531 3.2717 3.58522C3.17397 3.78513 3.13688 4.00927 3.165 4.23C2.37575 4.7454 1.67078 5.3795 1.075 6.11C1.19455 5.3189 1.47112 4.55966 1.88843 3.87701C2.30575 3.19437 2.85539 2.60208 3.505 2.135H3.5ZM3.5 9.99C3.30481 10.0555 3.13037 10.1714 2.9943 10.3259C2.85822 10.4804 2.76533 10.6681 2.725 10.87H2.405C1.59754 9.9069 1.1146 8.7136 1.025 7.46L1.08 7.365C1.70611 6.3942 2.52463 5.562 3.485 4.92C3.62899 5.0704 3.81094 5.179 4.01162 5.2345C4.2123 5.2899 4.42423 5.2901 4.625 5.235C5.2938 6.3652 6.208 7.3306 7.3 8.06C7.505 8.195 7.715 8.32 7.925 8.44C7.9082 8.6312 7.9391 8.8237 8.015 9C7.1 9.7266 6.0445 10.256 4.915 10.555C4.78401 10.3103 4.57028 10.1201 4.31199 10.0184C4.05369 9.9167 3.76766 9.9102 3.505 10L3.5 9.99ZM7 12.99C5.9831 12.9903 4.98307 12.7304 4.095 12.235L4.235 12.205C4.43397 12.1397 4.61176 12.0222 4.74984 11.8648C4.88792 11.7074 4.98122 11.5158 5.02 11.31C6.2985 10.984 7.4921 10.3872 8.52 9.56C8.7642 9.7027 9.0525 9.75 9.3295 9.6927C9.6064 9.6355 9.8524 9.4778 10.02 9.25C10.7254 9.4334 11.4511 9.5275 12.18 9.53H12.445C11.9626 10.5673 11.1938 11.4451 10.2291 12.0599C9.2643 12.6747 8.144 13.0009 7 13V12.99ZM10.255 8.54C10.2545 8.3304 10.1975 8.1249 10.09 7.945C10.9221 6.8581 11.5012 5.5991 11.785 4.26C12.035 4.37667 12.2817 4.50667 12.525 4.65C13.0749 5.9495 13.1493 7.4012 12.735 8.75C11.9049 8.8142 11.0698 8.7484 10.26 8.555L10.255 8.54Z' fill='%23646D7A'/%3e%3c/svg%3e";
"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTYiIGhlaWdodD0iOTYiIHZpZXdCb3g9IjAgMCA5NiA5NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTY4LjE1MTkgNzUuNzM3MkM2Mi4yOTQzIDc5Ljk5MyA1NS4yMzk3IDgyLjI4NTIgNDcuOTk5MyA4Mi4yODUyQzQwLjc1ODkgODIuMjg1MiAzMy43MDQzIDc5Ljk5MyAyNy44NDY2IDc1LjczNzJNNjMuMDI5MSAxNy4xODM3QzY5LjUzNjggMjAuMzU3NyA3NC44NzI2IDI1LjUxMDQgNzguMjcxOCAzMS45MDMzQzgxLjY3MDkgMzguMjk2MiA4Mi45NTkgNDUuNjAxMiA4MS45NTEzIDUyLjc3MTFNMTQuMDQ3NiA1Mi43NzA4QzEzLjAzOTkgNDUuNjAwOCAxNC4zMjggMzguMjk1OSAxNy43MjcxIDMxLjkwM0MyMS4xMjYzIDI1LjUxMDEgMjYuNDYyMSAyMC4zNTczIDMyLjk2OTggMTcuMTgzM000Ni4wNTk4IDI5LjM2NzVMMjkuMzY3MyA0Ni4wNkMyOC42ODg1IDQ2LjczODkgMjguMzQ5IDQ3LjA3ODMgMjguMjIxOCA0Ny40Njk3QzI4LjExIDQ3LjgxNCAyOC4xMSA0OC4xODQ5IDI4LjIyMTggNDguNTI5MkMyOC4zNDkgNDguOTIwNiAyOC42ODg1IDQ5LjI2MDEgMjkuMzY3MyA0OS45MzlMNDYuMDU5OCA2Ni42MzE0QzQ2LjczODcgNjcuMzEwMyA0Ny4wNzgxIDY3LjY0OTcgNDcuNDY5NSA2Ny43NzY5QzQ3LjgxMzggNjcuODg4OCA0OC4xODQ3IDY3Ljg4ODggNDguNTI5IDY3Ljc3NjlDNDguOTIwNCA2Ny42NDk3IDQ5LjI1OTkgNjcuMzEwMyA0OS45Mzg4IDY2LjYzMTRMNjYuNjMxMiA0OS45MzlDNjcuMzEwMSA0OS4yNjAxIDY3LjY0OTUgNDguOTIwNiA2Ny43NzY3IDQ4LjUyOTJDNjcuODg4NiA0OC4xODQ5IDY3Ljg4ODYgNDcuODE0IDY3Ljc3NjcgNDcuNDY5N0M2Ny42NDk1IDQ3LjA3ODMgNjcuMzEwMSA0Ni43Mzg5IDY2LjYzMTIgNDYuMDZMNDkuOTM4OCAyOS4zNjc1QzQ5LjI1OTkgMjguNjg4NyA0OC45MjA0IDI4LjM0OTIgNDguNTI5IDI4LjIyMkM0OC4xODQ3IDI4LjExMDIgNDcuODEzOCAyOC4xMTAyIDQ3LjQ2OTUgMjguMjIyQzQ3LjA3ODEgMjguMzQ5MiA0Ni43Mzg3IDI4LjY4ODcgNDYuMDU5OCAyOS4zNjc1WiIgc3Ryb2tlPSIjNDA0MDQwIiBzdHJva2Utd2lkdGg9IjYuODU3MTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K";
export async function ChainIcon(props: {
iconUrl?: string;
@@ -11,39 +11,48 @@ export const products = [
name: "Contracts",
id: "contracts",
icon: ContractIcon,
description: "Pre-built contracts & deployment tools",
link: "https://portal.thirdweb.com/contracts",
},
{
name: "Connect SDK",
id: "connect-sdk",
icon: ConnectSDKIcon,
description: "Client-side SDKs for wallets and blockchain interactions",
link: "https://portal.thirdweb.com/connect",
},
{
name: "RPC Edge",
id: "rpc-edge",
icon: RPCIcon,
description: "Performant and scalable RPC service",
link: "https://portal.thirdweb.com/infrastructure/rpc-edge/overview",
},
{
name: "Engine",
id: "engine",
icon: EngineIcon,
description: "Backend server that reads, writes, and deploys contracts",
link: "https://portal.thirdweb.com/engine",
},
{
name: "Account Abstraction",
id: "account-abstraction",
icon: SmartAccountIcon,
description: "Account factory contracts, bundler & paymaster",
link: "https://portal.thirdweb.com/connect/account-abstraction/overview",
},
{
name: "Pay",
id: "pay",
icon: PayIcon,
},
{
name: "Faucet",
id: "faucet",
icon: PayIcon,
description: "Point of sale solution for bridging, onramping & swapping",
link: "https://portal.thirdweb.com/connect/pay/overview",
},
] satisfies Array<{
name: string;
id: ChainSupportedService;
icon: React.FC<{ className?: string }>;
description: string;
link: string;
}>;
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

@@ -7,12 +7,19 @@ import ancient8Banner from "./temp-assets/ancient8-banner.png";
import baseBanner from "./temp-assets/base-banner.jpeg";
import superchainCTABG from "./temp-assets/cta-bg-superchain.png";
import xaiCTABg from "./temp-assets/cta-bg-xai-connect.png";
import thirdwebCTA from "./temp-assets/cta-thirdweb.png";
import lumiaCTA from "./temp-assets/lumia-cta.png";
import lumiaBanner from "./temp-assets/lumiaBanner.png";
import mantleBanner from "./temp-assets/mantle-banner.jpeg";
import rootStockBanner from "./temp-assets/rootstock-banner.png";
import rootStockCTABG from "./temp-assets/rootstock-cta.png";
import superpositionBanner from "./temp-assets/superpositionBanner.png";
import superpositionCTA from "./temp-assets/superpositionCTA.png";
import thirdwebBanner from "./temp-assets/thirdweb-banner.png";
import vanarBanner from "./temp-assets/vanar-banner.png";
import vanarCTABG from "./temp-assets/vanar-cta.png";
import xaiBanner from "./temp-assets/xai-banner.jpg";
import type { ChainMetadataWithServices } from "./types/chain";
// END TEMPORARY
@@ -166,6 +173,151 @@ const chainMetaRecord = {
title: "Fully EVM compatible L1 for Entertainment",
},
},
// lumia Mainnet
994873017: {
headerImgUrl: lumiaBanner.src,
about:
"Lumia is a groundbreaking modular layer 2 blockchain designed to integrate real-world assets (RWA) with cross-chain liquidity. A seamless, efficient, and scalable aggregation layer that unifies liquidity from both centralized (CEX) and decentralized exchanges (DEX) to provide builders with immediate access to the biggest markets in Web3.",
cta: {
backgroundImageUrl: lumiaCTA.src,
title:
"Lumia is a groundbreaking modular layer 2 blockchain designed to integrate real-world assets (RWA) with cross-chain liquidity. A seamless, efficient, and scalable aggregation layer that unifies liquidity from both centralized (CEX) and decentralized exchanges (DEX) to provide builders with immediate access to the biggest markets in Web3.",
buttonLink: "https://docs.lumia.org/",
buttonText: "Learn more",
},
},
// Superposition Testnet
98985: {
headerImgUrl: superpositionBanner.src,
about:
"Superposition is the first blockchain that pays you to use it. It is a DeFi native Layer-3 that focuses on novel incentives and order-flow for growth and value capture for developers and users alike.\n\nSuperpositions includes a native on-chain orderbook with faster execution speeds through Stylus, providing shared and permissionless liquidity for all apps onchain, and Super Assets, which pay yield when you both hold and use them.",
cta: {
backgroundImageUrl: superpositionCTA.src,
title: "The Blockchain that pays you to use it",
buttonLink: "https://superposition.so/",
buttonText: "Learn more",
},
},
// Aleph 0 Mainnet
41455: {
headerImgUrl: thirdwebBanner.src,
about:
"Deploy Web3 apps that are as fast as legacy solutions. Aleph Zero is a layer 1 that enables teams to deploy scalable, secure, low-cost, and ZK privacy-enhanced products across multiple verticals—from DeFi and gaming to enterprise.",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://alephzero.org/",
buttonText: "Learn more",
},
},
// B3 Mainnet
8333: {
headerImgUrl: thirdwebBanner.src,
about:
"Created by a team of Base/Coinbase alumni and OG ETH contributors, B3 is a horizontally scaled gaming ecosystem built on Base. \n\nOur rollup network settles on Base, ensuring B3 transactions inherit the security of the EVM.",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://b3.fun/",
buttonText: "Learn more",
},
},
//CampNetwork Testnet v2
325000: {
headerImgUrl: thirdwebBanner.src,
about:
"Camp Network is the user identity layer across blockchains. Camp aggregates online user data, enabling users to own and monetize their identity while helping teams better understand user behavior.",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://www.campnetwork.xyz/",
buttonText: "Learn more",
},
},
//Donatuz
42026: {
headerImgUrl: thirdwebBanner.src,
about:
"Monetize your passion, your way. Discover the freedom to grow with our creator-focused platform.",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://www.donatuz.org/",
buttonText: "Learn more",
},
},
//Fuse
122: {
headerImgUrl: thirdwebBanner.src,
about:
"Build and grow on the leading web3 platform for business and finance, powered by a performant zkEVM tailor made to get millions of consumers on-chain.",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://www.fuse.io/",
buttonText: "Learn more",
},
},
//Gemuchain
1903648807: {
headerImgUrl: thirdwebBanner.src,
about:
"Is a new Layer 2 integrating Al, Cybersecurity, ZK and a new Consensus layer - Proof Of Longevity. It will protect projects from bad actors and practices including hacks, manipulations, MEV bots and more, penalize bad actors and reward good actors and long term holders, so unlike anything out there today.",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://gemuchain.io/",
buttonText: "Learn more",
},
},
//0G-Newton-Testnet
16600: {
headerImgUrl: thirdwebBanner.src,
about:
"0G is an infinitely scalable data availability layer and data storage system that provides the necessary infrastructure to scale Web3 and bring novel use cases on-chain.",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://0g.ai/",
buttonText: "Learn more",
},
},
//Plume
161221135: {
headerImgUrl: thirdwebBanner.src,
about:
"Plume Network is the premier RWA chain built for crypto natives. Venture into RWA land with us while earning Plume Miles!",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://www.plumenetwork.xyz/",
buttonText: "Learn more",
},
},
//zkCandy
302: {
headerImgUrl: thirdwebBanner.src,
about:
"Explore our cutting-edge Layer2 Ethereum scaling ZK Chain built for gaming and entertainment with the latest zk-proof technology.",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://zkcandy.io/",
buttonText: "Learn more",
},
},
//zkSync
324: {
headerImgUrl: thirdwebBanner.src,
about:
"ZKsync is an ever expanding verifiable blockchain network, secured by math.",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://zksync.io/",
buttonText: "Learn more",
},
},
} satisfies Record<number, ChainMetadata>;
// END TEMPORARY
@@ -8,9 +8,7 @@ import {
} from "@/components/ui/pagination";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import React from "react";
export function TablePagination(props: { isLandingPage?: boolean }) {
export function TablePagination() {
const router = useRouter();
const path = usePathname();
const searchParams = useSearchParams();
@@ -22,7 +20,6 @@ export function TablePagination(props: { isLandingPage?: boolean }) {
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className={`${props.isLandingPage && "bg-black/50"}`}
disabled={page <= 1}
onClick={() => {
if (page === 1) return;
@@ -35,10 +32,11 @@ export function TablePagination(props: { isLandingPage?: boolean }) {
}}
/>
</PaginationItem>
<PaginationItem className="px-4">{page}</PaginationItem>
<PaginationItem className="px-4 border rounded-lg h-full flex items-center bg-secondary">
{page}
</PaginationItem>
<PaginationItem>
<PaginationNext
className={`${props.isLandingPage && "bg-black/50"}`}
onClick={() => {
router.replace(
`${path}?page=${Number(page) + 1}${range?.length ? `&timeRange=${range}` : ""}${currentSort ? `&sortBy=${currentSort}` : ""}`,
@@ -18,10 +18,11 @@ export function SortingHeader(props: SortingHeaderProps) {
const isCurrentSort = currentSort === props.sortBy;
const justify =
props.sortBy === "transactionCountChange" ? "justify-start" : "justify-end";
return (
<div className={`flex flex-row ${justify}`}>
<Button
className={"flex flex-row items-center gap-2 px-0 cursor-pointer"}
className={"flex flex-row items-center gap-1 px-0 cursor-pointer"}
variant={"link"}
onClick={() => {
router.replace(
@@ -32,7 +33,7 @@ export function SortingHeader(props: SortingHeaderProps) {
);
}}
>
{isCurrentSort && <ChevronDown className="size-sm" />}
{isCurrentSort && <ChevronDown className="size-4 shrink-0" />}
{props.title}
</Button>
</div>
@@ -1,15 +1,18 @@
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { CircleArrowDown, CircleArrowUp, DotIcon } from "lucide-react";
import Link from "next/link";
import type { ChainMetadata } from "thirdweb/chains";
import { shortenAddress } from "thirdweb/utils";
import { ChainIcon } from "../../(chain)/components/server/chain-icon";
import { ScrollShadow } from "../../../../@/components/ui/ScrollShadow/ScrollShadow";
import { Badge } from "../../../../@/components/ui/badge";
import type {
SortBy,
TimeRange,
@@ -23,120 +26,121 @@ export async function TrendingContractSection(props: {
chainId?: number;
perPage?: number;
searchParams: { timeRange?: TimeRange; page?: number; sortBy?: SortBy };
showPagination: boolean;
}) {
const isLandingPage = props.chainId === undefined;
const { page, timeRange } = props.searchParams;
const { page } = props.searchParams;
const firstIndex = Math.max(0, ((page || 1) - 1) * (props.perPage || 10));
const showChainColumn = !props.chainId;
return (
props.topContracts.length > 0 && (
<div className="flex flex-col gap-4 w-full">
<Table
className={`border-white ${isLandingPage && "border-b-[1px] font-mono text-xs md:text-sm mb-4"}`}
>
<TableHeader>
<TableRow
className={`bg-secondary ${isLandingPage && "border-white"}`}
>
<TableHead className="text-left">RANK</TableHead>
<TableHead>CONTRACT</TableHead>
<TableHead>
<SortingHeader
title={`CHANGE (
${
timeRange === "month"
? "30d"
: timeRange === "week"
? "7d"
: "24h"
}
)`}
sortBy="transactionCountChange"
/>
</TableHead>
{props.chainId ? null : <TableHead>CHAIN</TableHead>}
<TableHead className="text-center">TYPE</TableHead>
<TableHead>
<SortingHeader title="TRANSACTIONS" sortBy="transactionCount" />
</TableHead>
<TableHead>
<SortingHeader title="WALLETS" sortBy="walletCount" />
</TableHead>
<TableHead>
<SortingHeader title="GAS USAGE" sortBy="gasUsage" />
</TableHead>
<TableHead>
<SortingHeader title="VALUE MOVED" sortBy="totalValueMoved" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{props.topContracts.map((contract, index) => (
<TableRow
key={`${contract.chainMetadata.chainId}${contract.contractAddress}${index}`}
className={`${isLandingPage && "border-white !border-x-[1px]"}`}
style={{
backgroundColor: isLandingPage
? `rgb(200, 72, 0, ${0.5 - index * 0.05})`
: isLandingPage
? "rgba(0, 0, 0, 0.5)"
: undefined,
}}
>
<TableCell className="text-left">
{index + 1 + firstIndex}
</TableCell>
<TableCell className="font-medium">
<a
href={`/${contract.chainMetadata.chainId}/${contract.contractAddress}`}
target="_blank"
rel="noreferrer"
className={`${isLandingPage ? "underline" : "text-link-foreground"}`}
>
{getContractName(contract)}
</a>
</TableCell>
<TableCell className="text-left flex justify-start">
<ChangeCell
displayCount={contract.transactionCount}
change={contract.transactionCountChange}
<div>
<ScrollShadow scrollableClassName="rounded-lg border">
<Table className="w-full">
<TableHeader>
<TableRow className="bg-secondary">
<TableHead className="text-left">Rank</TableHead>
<TableHead>Contract</TableHead>
<TableHead>Change</TableHead>
{showChainColumn && <TableHead>Chain</TableHead>}
<TableHead className="text-center">Type</TableHead>
<TableHead>
<SortingHeader
title="Transactions"
sortBy="transactionCount"
/>
</TableCell>
{props.chainId ? null : (
<TableCell>
<ChainCell chainMetadata={contract.chainMetadata} />
</TableCell>
)}
<TableCell className="text-center">
{contract.type ? contract.type : null}
</TableCell>
<TableCell className="text-right">
{contract.transactionCount}
</TableCell>
<TableCell className="text-right">
{contract.walletCount}
</TableCell>
<TableCell className="text-right">
{contract.gasUsage}
</TableCell>
<TableCell className="text-right">
{contract.valueMoved}
</TableCell>
</TableHead>
<TableHead>
<SortingHeader title="Wallets" sortBy="walletCount" />
</TableHead>
<TableHead>
<SortingHeader title="Gas Usage" sortBy="gasUsage" />
</TableHead>
<TableHead>
<SortingHeader title="Value Moved" sortBy="totalValueMoved" />
</TableHead>
</TableRow>
))}
</TableBody>
{isLandingPage && (
<TableCaption className="text-left text-primary-foreground">
built by{" "}
<a href="https://thirdweb.com" className="underline">
thirdweb
</a>
</TableCaption>
)}
</Table>
<div className="w-full flex flex-row gap-4 justify-end">
<TablePagination isLandingPage={isLandingPage} />
</div>
</TableHeader>
<TableBody>
{props.topContracts.map((contract, index) => (
<TableRow
className="relative hover:bg-muted even:bg-secondary/30 "
key={`${contract.chainMetadata.chainId}${contract.contractAddress}${index}`}
>
{/* Rank */}
<TableCell className="text-left">
{index + 1 + firstIndex}
</TableCell>
{/* Contract name */}
<TableCell className="font-medium">
<Link
href={`/${contract.chainMetadata.chainId}/${contract.contractAddress}`}
target="_blank"
className="before:absolute before:inset-0 before:z-0 static before:content-[''] "
>
{getContractName(contract)}
</Link>
</TableCell>
{/* % Change */}
<TableCell className="text-left flex justify-start">
<ChangeCell
displayCount={contract.transactionCount}
change={contract.transactionCountChange}
/>
</TableCell>
{/* Chain */}
{showChainColumn && (
<TableCell>
<ChainCell chainMetadata={contract.chainMetadata} />
</TableCell>
)}
{/* Type */}
<TableCell className="text-center">
{contract.type ? (
<Badge variant="outline" className="px-2 py-1 font-mono">
{contract.type}
</Badge>
) : (
<DotIcon className="size-6 inline text-secondary-foreground" />
)}
</TableCell>
{/* Tx count */}
<TableCell className="text-right">
{contract.transactionCount}
</TableCell>
{/* Wallet Count */}
<TableCell className="text-right">
{contract.walletCount}
</TableCell>
{/* Gas Usage */}
<TableCell className="text-right">
{contract.gasUsage}
</TableCell>
{/* Value Moved */}
<TableCell className="text-right">
{contract.valueMoved || (
<DotIcon className="size-6 inline text-secondary-foreground" />
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</ScrollShadow>
{props.showPagination && (
<div className="w-full flex flex-row gap-4 justify-end mt-10">
<TablePagination />
</div>
)}
</div>
)
);
@@ -146,12 +150,18 @@ function ChangeCell(props: { displayCount: string; change: number }) {
if (!props.displayCount) {
return null;
}
const color = props.change > 0 ? "text-success-text" : "text-red-400";
const badge = (
<span className={`${color}`}>
{props.change > 0 ? "▲" : "▼"}{" "}
<Badge
variant={props.change > 0 ? "success" : "destructive"}
className="gap-1.5 px-2 py-1 min-w-[80px] flex justify-center"
>
{props.change > 0 ? (
<CircleArrowUp className="size-4" />
) : (
<CircleArrowDown className="size-4" />
)}
{props.change < 10 ? props.change.toFixed(1) : props.change.toFixed(0)}%
</span>
</Badge>
);
return (
<div className="flex flex-row items-center gap-2 justify-end group">
@@ -163,17 +173,14 @@ function ChangeCell(props: { displayCount: string; change: number }) {
function ChainCell(props: { chainMetadata: ChainMetadata }) {
const chainMetadata = props.chainMetadata;
return (
<div className="flex flex-row items-center gap-3">
<Link
href={`/${chainMetadata.chainId}`}
target="_blank"
className="flex flex-row items-center gap-3 hover:underline relative z-1"
>
<ChainIcon iconUrl={chainMetadata.icon?.url} className="size-6" />
<a
href={`/${chainMetadata.chainId}`}
target="_blank"
rel="noreferrer"
className="underline"
>
{chainMetadata.name.split(" ")[0]}
</a>
</div>
{chainMetadata.name.split(" ")[0]}
</Link>
);
}
@@ -7,10 +7,10 @@ import {
import { TrendingContractSection } from "./components/trending-table";
export const metadata: Metadata = {
title: "🔥 HOT Contracts",
title: "Trending Contracts",
description: "See what's hot onchain right now",
openGraph: {
title: "🔥 HOT Contracts",
title: "Trending Contracts",
description: "See what's hot onchain right now",
},
};
@@ -21,32 +21,20 @@ export default async function DashboardContractTrendingPage(props: {
const topContracts = await fetchTopContracts({
...props.searchParams,
timeRange: "month",
perPage: 20,
});
return (
<div>
<div className="container px-0 flex flex-col gap-8 py-6">
<div className="flex flex-col gap-6 justify-between items-start">
<div className="w-full flex flex-row justify-center items-center gap-4 mt-8">
<h1 className="text-5xl md:text-7xl tracking-tight font-bold font-mono mb-2">
🔥
</h1>
<div className="text-left">
<h1 className="text-3xl md:text-5xl tracking-tight font-bold font-mono mb-2">
HOT Contracts
</h1>
<p className="text-sm md:text-base text-secondary-foreground leading-7 w-full font-mono">
See what's hot onchain right now
</p>
</div>
</div>
<div className="flex my-8 mx-4 gap-8 w-full">
<TrendingContractSection
topContracts={topContracts}
searchParams={props.searchParams}
/>
</div>
</div>
</div>
<div className="container px-4 py-10">
<h1 className="text-3xl md:text-4xl tracking-tight font-semibold mb-5">
Trending Contracts
</h1>
<TrendingContractSection
topContracts={topContracts}
searchParams={props.searchParams}
showPagination={true}
/>
</div>
);
}
@@ -11,7 +11,7 @@ import { headerLinks } from "./headerLinks";
export function DashboardHeader() {
return (
// the "h-24" avoids layout shift when connecting wallet (connected wallet button is taller than disconnected...)
<header className="flex flex-row items-center justify-between flex-shrink-0 h-20 border-b border-border bg-background md:h-24 py-4 px-4 md:px-6">
<header className="flex flex-row items-center justify-between flex-shrink-0 h-20 border-b border-border bg-background md:h-24 py-4 px-4 lg:px-6">
{/* Left */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
@@ -27,8 +27,8 @@ export function DashboardHeader() {
key={link.href}
href={link.href}
target={link.href.startsWith("https") ? "_blank" : undefined}
className="text-foreground font-medium py-2 px-[10px] hover:underline"
activeClassName="font-semibold"
className="text-secondary-foreground font-medium py-2 px-[10px] hover:text-foreground"
activeClassName="font-medium text-foreground"
>
{link.name}
</NavLink>
@@ -45,7 +45,7 @@ export function DashboardHeader() {
key={link.href}
href={link.href}
target={link.href.startsWith("https") ? "_blank" : undefined}
className="text-foreground font-medium py-2 px-[10px] hover:underline"
className="text-secondary-foreground font-medium py-2 px-[10px] hover:text-foreground"
>
{link.name}
</Link>
@@ -18,6 +18,10 @@ export const headerLinks = {
},
],
right: [
{
name: "Chainlist",
href: "/chainlist",
},
{
name: "Docs",
href: "https://portal.thirdweb.com",
@@ -0,0 +1,32 @@
import {
type Theme,
type ThemeOverrides,
darkTheme,
lightTheme,
} from "thirdweb/react";
export function getSDKTheme(theme: "light" | "dark"): Theme {
const overrides: ThemeOverrides = {
colors: {
modalBg: "hsl(var(--background))",
borderColor: "hsl(var(--border))",
connectedButtonBg: "hsl(var(--background))",
connectedButtonBgHover: "hsl(var(--muted))",
tertiaryBg: "hsl(var(--muted))",
secondaryButtonBg: "hsl(var(--muted))",
secondaryButtonHoverBg: "hsl(var(--accent))",
inputAutofillBg: "hsl(var(--muted))",
secondaryIconHoverBg: "hsl(var(--accent))",
separatorLine: "hsl(var(--border))",
skeletonBg: "hsl(var(--accent))",
// scrollbarBg: "hsl(var(--muted))",
// secondaryText: "hsl(var(--secondary-foreground))",
// primaryText: "hsl(var(--foreground))",
// accentButtonBg: "hsl(var(--primary))",
// accentButtonText: "hsl(var(--foreground))",
// accentText: "hsl(var(--link-foreground))",
},
};
return theme === "light" ? lightTheme(overrides) : darkTheme(overrides);
}
+1 -1
View File
@@ -69,7 +69,7 @@ export default function RootLayout({
>
<PostHogPageView />
<AppRouterProviders>{children}</AppRouterProviders>
<Toaster />
<Toaster richColors />
<NextTopLoader
color="hsl(var(--primary))"
height={2}
+13 -1
View File
@@ -131,12 +131,24 @@ export async function doLogin(
});
// redirect to the nextPath (if set)
if (nextPath) {
if (nextPath && isValidRedirectPath(nextPath)) {
return redirect(nextPath);
}
// if we do not have a next path, redirect to dashboard home
return redirect("/dashboard");
}
function isValidRedirectPath(encodedPath: string): boolean {
try {
// Decode the URI component
const decodedPath = decodeURIComponent(encodedPath);
// ensure the path always starts with a _single_ slash
// dobule slash could be interpreted as `//example.com` which is not allowed
return decodedPath.startsWith("/") && !decodedPath.startsWith("//");
} catch (e) {
// If decoding fails, return false
return false;
}
}
export async function doLogout() {
const cookieStore = cookies();
+3 -24
View File
@@ -5,8 +5,9 @@ import { thirdwebClient } from "@/constants/client";
import { useTheme } from "next-themes";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { ConnectEmbed, darkTheme, lightTheme } from "thirdweb/react";
import { ConnectEmbed } from "thirdweb/react";
import { ThirdwebMiniLogo } from "../components/ThirdwebMiniLogo";
import { getSDKTheme } from "../components/sdk-component-theme";
import { doLogin, doLogout, getLoginPayload, isLoggedIn } from "./auth-actions";
export default function LoginPage() {
@@ -48,29 +49,7 @@ function CustomConnectEmmbed() {
}}
client={thirdwebClient}
modalSize={isLG ? "wide" : "compact"}
theme={
theme === "light"
? lightTheme({
colors: {
primaryButtonBg: "hsl(var(--primary))",
modalBg: "hsl(var(--background))",
borderColor: "hsl(var(--border))",
secondaryButtonBg: "hsl(var(--secondary))",
secondaryButtonHoverBg: "hsl(var(--accent))",
tertiaryBg: "hsl(var(--accent))",
},
})
: darkTheme({
colors: {
primaryButtonBg: "hsl(var(--primary))",
modalBg: "hsl(var(--background))",
borderColor: "hsl(var(--border))",
secondaryButtonBg: "hsl(var(--secondary))",
secondaryButtonHoverBg: "hsl(var(--accent))",
tertiaryBg: "hsl(var(--accent))",
},
})
}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
/>
);
}
@@ -263,7 +263,7 @@ const NoFundsNotice: React.FC<NoFundsNoticeProps> = ({ symbol }) => {
if (chainInfo.chainId === localhost.id) {
await sdk.wallet.requestFunds(10);
} else if (chainInfo?.testnet) {
router.push(`/${chainInfo.chainId}/faucet`);
router.push(`/${chainInfo.chainId}`);
}
}
};
@@ -1,6 +1,6 @@
import { useDashboardStorageUpload } from "@3rdweb-sdk/react/hooks/useDashboardStorageUpload";
import { Icon } from "@chakra-ui/react";
import { useQueryClient } from "@tanstack/react-query";
import { useStorageUpload } from "@thirdweb-dev/react";
import { FileInput } from "components/shared/FileInput";
import { PINNED_FILES_QUERY_KEY_ROOT } from "components/storage/your-files";
import { useErrorHandler } from "contexts/error-handler";
@@ -11,7 +11,7 @@ export const IconUpload: React.FC<{ onUpload: (url: string) => void }> = ({
onUpload,
}) => {
const errorHandler = useErrorHandler();
const storageUpload = useStorageUpload();
const storageUpload = useDashboardStorageUpload();
const queryClient = useQueryClient();
const handleIconUpload = (file: File) => {
@@ -21,19 +21,16 @@ export const IconUpload: React.FC<{ onUpload: (url: string) => void }> = ({
return;
}
storageUpload.mutate(
{ data: [file] },
{
onSuccess([uri]) {
onUpload(uri);
// also refetch the files list
queryClient.invalidateQueries([PINNED_FILES_QUERY_KEY_ROOT]);
},
onError(error) {
errorHandler.onError(error, "Failed to upload file");
},
storageUpload.mutate([file], {
onSuccess([uri]) {
onUpload(uri);
// also refetch the files list
queryClient.invalidateQueries([PINNED_FILES_QUERY_KEY_ROOT]);
},
);
onError(error) {
errorHandler.onError(error, "Failed to upload file");
},
});
};
return (
@@ -41,10 +41,7 @@ export const ContractMetadataFieldset: React.FC<
shouldTouch: true,
})
}
border="1px solid"
borderColor="gray.200"
borderRadius="md"
transition="all 200ms ease"
className="border border-border rounded transition-all duration-200"
/>
<FormErrorMessage>
{
@@ -7,9 +7,6 @@ import {
Divider,
Flex,
FormControl,
HStack,
Icon,
Tooltip,
} from "@chakra-ui/react";
import { TransactionButton } from "components/buttons/TransactionButton";
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
@@ -28,8 +25,6 @@ import { useActiveAccount } from "thirdweb/react";
import { encodeAbiParameters } from "thirdweb/utils";
import invariant from "tiny-invariant";
import {
Card,
Checkbox,
FormHelperText,
FormLabel,
Heading,
@@ -37,6 +32,9 @@ import {
TrackedLink,
} from "tw-components";
import { Spinner } from "../../../@/components/ui/Spinner/Spinner";
import { Checkbox, CheckboxWithLabel } from "../../../@/components/ui/checkbox";
import { ToolTipLabel } from "../../../@/components/ui/tooltip";
import { TrackedLinkTW } from "../../../@/components/ui/tracked-link";
import {
useConstructorParamsFromABI,
useContractEnabledExtensions,
@@ -647,54 +645,45 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
</Text>
</Flex>
<Checkbox
{...form.register("addToDashboard")}
isChecked={form.watch("addToDashboard")}
>
<Text>
Import so I can find it in the list of my contracts at{" "}
<TrackedLink
href="/dashboard"
isExternal
category="custom-contract"
label="visit-dashboard"
color="blue.500"
>
/dashboard
</TrackedLink>
.
</Text>
</Checkbox>
<CheckboxWithLabel>
<Checkbox
{...form.register("addToDashboard")}
checked={form.watch("addToDashboard")}
onCheckedChange={(checked) =>
form.setValue("addToDashboard", !!checked)
}
/>
Import so I can find it in the list of my contracts at{" "}
<TrackedLinkTW
className="text-link-foreground hover:text-foreground"
href="/dashboard"
target="_blank"
category="custom-contract"
label="visit-dashboard"
>
/dashboard
</TrackedLinkTW>
</CheckboxWithLabel>
{fullPublishMetadata.data?.deployType === "standard" && (
<Flex gap={4} flexDir="column">
<Checkbox
{...form.register("deployDeterministic")}
isChecked={form.watch("deployDeterministic")}
>
<Tooltip
label={
<Card py={2} px={4} bgColor="backgroundHighlight">
<Text fontSize="small" lineHeight={6}>
Allows having the same contract address on multiple
chains. You can control the address by specifying a salt
for create2 deployment below.
</Text>
</Card>
<CheckboxWithLabel>
<Checkbox
{...form.register("deployDeterministic")}
checked={form.watch("deployDeterministic")}
onCheckedChange={(c) =>
form.setValue("deployDeterministic", !!c)
}
isDisabled={false}
p={0}
bg="transparent"
boxShadow="none"
>
<HStack>
<Heading as="label" size="label.md">
/>
<ToolTipLabel label="Allows having the same contract address on multiple chains. You can control the address by specifying a salt for create2 deployment below">
<div className="inline-flex gap-1.5 items-center">
<span className="tex-sm">
Deploy at a deterministic address
</Heading>
<Icon as={FiHelpCircle} />
</HStack>
</Tooltip>
</Checkbox>
</span>
<FiHelpCircle className="size-4" />
</div>
</ToolTipLabel>
</CheckboxWithLabel>
{isCreate2Deployment && (
<FormControl>
@@ -712,16 +701,17 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
solidityType={"string"}
{...form.register("saltForCreate2")}
/>
<Flex alignItems="center" gap={3}>
<div className="h-2" />
<CheckboxWithLabel>
<Checkbox
{...form.register("signerAsSalt")}
isChecked={form.watch("signerAsSalt")}
checked={form.watch("signerAsSalt")}
onCheckedChange={(c) => form.setValue("signerAsSalt", !!c)}
/>
<Text mt={1}>
<span className="text-sm">
Include deployer wallet address in salt (recommended)
</Text>
</Flex>
</span>
</CheckboxWithLabel>
</FormControl>
)}
</Flex>
@@ -96,11 +96,7 @@ export const LandingFieldset: React.FC<LandingFieldsetProps> = ({
accept={{ "image/*": [] }}
value={logoUrl}
setValue={(file) => form.setValue("logo", file)}
border="1px solid"
borderColor="gray.200"
borderRadius="md"
transition="all 200ms ease"
_hover={{ shadow: "sm" }}
className="border border-border rounded transition-all duration-200"
renderPreview={(fileUrl) => (
<Image
alt=""
@@ -147,11 +147,7 @@ export const EditProfile: React.FC<EditProfileProps> = ({
value={imageUrl}
showUploadButton
setValue={(file) => setValue("avatar", file)}
border="1px solid"
borderColor="gray.200"
borderRadius="md"
transition="all 200ms ease"
_hover={{ shadow: "sm" }}
className="border border-border rounded transition-all"
renderPreview={(fileUrl) => (
<MaskedAvatar w="100%" h="100%" src={fileUrl} />
)}
@@ -1,4 +1,4 @@
import type { NFTMetadata } from "@thirdweb-dev/sdk";
import type { NFTMetadata } from "thirdweb/utils";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
interface MediaCellProps {
@@ -20,13 +20,10 @@ export const IpfsUploadButton: ComponentWithChildren<IpfsUploadButtonProps> = ({
}) => {
const { onError } = useErrorHandler();
const handleUpload = (file: File) => {
storageUpload.mutate(
{ data: [file] },
{
onSuccess: ([uri]) => onUpload(uri),
onError: (error) => onError(error, "Failed to upload file"),
},
);
storageUpload.mutate([file], {
onSuccess: ([uri]) => onUpload(uri),
onError: (error) => onError(error, "Failed to upload file"),
});
};
return (
@@ -1,4 +1,5 @@
import { thirdwebClient } from "@/constants/client";
import { useDashboardStorageUpload } from "@3rdweb-sdk/react/hooks/useDashboardStorageUpload";
import {
AspectRatio,
Box,
@@ -16,8 +17,6 @@ import {
useToast,
} from "@chakra-ui/react";
import { useQueryClient } from "@tanstack/react-query";
import { useStorageUpload } from "@thirdweb-dev/react";
import type { UploadProgressEvent } from "@thirdweb-dev/storage";
import { PINNED_FILES_QUERY_KEY_ROOT } from "components/storage/your-files";
import { useErrorHandler } from "contexts/error-handler";
import { useTrack } from "hooks/analytics/useTrack";
@@ -159,19 +158,19 @@ const filesPerPage = 20;
const FileUpload: React.FC<FileUploadProps> = ({ files, updateFiles }) => {
const trackEvent = useTrack();
const address = useActiveAccount()?.address;
const [progress, setProgress] = useState<UploadProgressEvent>({
progress: 0,
total: 100,
});
// const [progress, setProgress] = useState<UploadProgressEvent>({
// progress: 0,
// total: 100,
// });
const [uploadWithoutDirectory, setUploadWithoutDirectory] = useState(
files.length === 1,
);
const uploadToAFolder = !uploadWithoutDirectory;
const storageUpload = useStorageUpload({
const storageUpload = useDashboardStorageUpload({
uploadWithoutDirectory,
onProgress: setProgress,
// onProgress: setProgress,
metadata: {
address,
address: address ?? "",
uploadedAt: new Date().toISOString(),
uploadedFrom: "thirdweb-dashboard",
},
@@ -182,7 +181,7 @@ const FileUpload: React.FC<FileUploadProps> = ({ files, updateFiles }) => {
const [page, setPage] = useState(0);
const progressPercent = (progress.progress / progress.total) * 100;
// const progressPercent = (progress.progress / progress.total) * 100;
const mainIpfsUri = useMemo(() => {
if (ipfsHashes.length === 0) {
@@ -192,8 +191,9 @@ const FileUpload: React.FC<FileUploadProps> = ({ files, updateFiles }) => {
return replaceIpfsUrl(ipfsHashes[0]);
}
// get the folder
// return replaceIpfsUrl(ipfsHashes[0].split("/").slice(0, -1).join("/"));
return `https://ipfs.io/ipfs/${ipfsHashes[0].split("ipfs://")[1]}`;
const uri = ipfsHashes[0].split("ipfs://")[1];
const folderPath = uri.split("/")[0]; // "Qma.../image.png" -> "Qma..."
return `https://ipfs.io/ipfs/${folderPath}`;
}, [ipfsHashes]);
const filesToShow = useMemo(() => {
@@ -429,11 +429,11 @@ const FileUpload: React.FC<FileUploadProps> = ({ files, updateFiles }) => {
>
<Progress
colorScheme="green"
value={progress.progress ? progressPercent : undefined}
// value={progress.progress ? progressPercent : undefined}
size={{ base: "xs", md: "lg" }}
w="100%"
borderRadius="full"
isIndeterminate={progress.progress === progress.total}
isIndeterminate={storageUpload.isLoading}
position="relative"
/>
<Center
@@ -444,7 +444,7 @@ const FileUpload: React.FC<FileUploadProps> = ({ files, updateFiles }) => {
top={0}
bottom={0}
>
<Text
{/* <Text
mt={0.5}
size="label.xs"
fontSize={11}
@@ -469,7 +469,7 @@ const FileUpload: React.FC<FileUploadProps> = ({ files, updateFiles }) => {
transition="color 0.2s"
>
{Math.round(progressPercent)}%
</Text>
</Text> */}
</Center>
</Flex>
)}
@@ -515,49 +515,34 @@ const FileUpload: React.FC<FileUploadProps> = ({ files, updateFiles }) => {
action: "upload",
label: "start",
});
storageUpload.mutate(
{
data: files,
storageUpload.mutate(files, {
onError: (error) => {
trackEvent({
category: TRACKING_CATEGORY,
action: "upload",
label: "error",
error,
});
onError(error, "Failed to upload file");
},
{
onError: (error) => {
trackEvent({
category: TRACKING_CATEGORY,
action: "upload",
label: "error",
error,
});
onError(error, "Failed to upload file");
setProgress({
progress: 0,
total: 100,
});
},
onSuccess: (uris) => {
trackEvent({
category: TRACKING_CATEGORY,
action: "upload",
label: "success",
address,
uri:
uris.length === 1
? uris[0]
: uris[0].split("/").slice(0, -1).join("/"),
});
setIpfsHashes(uris);
// also refetch the files list
queryClient.invalidateQueries([
PINNED_FILES_QUERY_KEY_ROOT,
]);
},
onSettled: () => {
setProgress({
progress: 0,
total: 100,
});
},
onSuccess: (uris) => {
trackEvent({
category: TRACKING_CATEGORY,
action: "upload",
label: "success",
address,
uri:
uris.length === 1
? uris[0]
: uris[0].split("/").slice(0, -1).join("/"),
});
setIpfsHashes(uris);
// also refetch the files list
queryClient.invalidateQueries([
PINNED_FILES_QUERY_KEY_ROOT,
]);
},
);
});
}}
>
Start Upload
@@ -1,9 +1,10 @@
import { Box, type BoxProps, DarkMode, Flex } from "@chakra-ui/react";
import { Box, type BoxProps, Flex } from "@chakra-ui/react";
import { HomepageFooter } from "components/footer/Footer";
import { NewsletterSection } from "components/homepage/sections/NewsletterSection";
import { HomepageTopNav } from "components/product-pages/common/Topnav";
import { NextSeo, type NextSeoProps } from "next-seo";
import type { ComponentWithChildren } from "types/component-with-children";
import { useForceDarkTheme } from "../../@/components/theme-provider";
interface LandingLayoutProps {
seo: NextSeoProps;
@@ -17,8 +18,10 @@ export const LandingLayout: ComponentWithChildren<LandingLayoutProps> = ({
children,
py,
}) => {
useForceDarkTheme();
return (
<DarkMode>
<>
<NextSeo {...seo} />
<Flex
sx={{
@@ -44,6 +47,6 @@ export const LandingLayout: ComponentWithChildren<LandingLayoutProps> = ({
<NewsletterSection />
<HomepageFooter />
</Flex>
</DarkMode>
</>
);
};
@@ -117,6 +117,19 @@ const AppHeader: React.FC = () => {
<CreditsButton />
<UpgradeButton />
</Flex>
<Button
as={TrackedLink}
variant="link"
href="/chainlist"
category="header"
label="chainlist"
color="bgBlack"
display={{ base: "none", md: "block" }}
size="sm"
mx={1.5}
>
Chainlist
</Button>
<Button
as={TrackedLink}
variant="link"
@@ -1,10 +1,11 @@
import { Box, DarkMode, Flex } from "@chakra-ui/react";
import { Box, Flex } from "@chakra-ui/react";
import { HomepageFooter } from "components/footer/Footer";
import { GetStartedSection } from "components/homepage/sections/GetStartedSection";
import { NewsletterSection } from "components/homepage/sections/NewsletterSection";
import { HomepageTopNav } from "components/product-pages/common/Topnav";
import { NextSeo, type NextSeoProps } from "next-seo";
import type { PropsWithChildren } from "react";
import { useForceDarkTheme } from "../../../@/components/theme-provider";
interface IProductPage {
seo: NextSeoProps;
@@ -16,8 +17,9 @@ export const ProductPage: React.FC<PropsWithChildren<IProductPage>> = ({
accentColor,
children,
}) => {
useForceDarkTheme();
return (
<DarkMode>
<>
<NextSeo {...seo} />
<Flex
sx={{
@@ -41,6 +43,6 @@ export const ProductPage: React.FC<PropsWithChildren<IProductPage>> = ({
<NewsletterSection />
<HomepageFooter />
</Flex>
</DarkMode>
</>
);
};
@@ -1,18 +1,10 @@
import {
AspectRatio,
Box,
type BoxProps,
Center,
Flex,
Icon,
Image,
type LayoutProps,
Link,
Stack,
} from "@chakra-ui/react";
/* eslint-disable @next/next/no-img-element */
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { AiFillEye } from "@react-icons/all-files/ai/AiFillEye";
import { AiOutlineFileAdd } from "@react-icons/all-files/ai/AiOutlineFileAdd";
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
import Link from "next/link";
import { useCallback } from "react";
import {
type Accept,
@@ -21,20 +13,21 @@ import {
useDropzone,
} from "react-dropzone";
import { FiImage, FiUpload } from "react-icons/fi";
import { Button, Text } from "tw-components";
interface IFileInputProps extends BoxProps {
interface IFileInputProps {
accept?: Accept;
setValue: (file: File) => void;
isDisabled?: boolean;
value?: string | File;
showUploadButton?: true;
maxContainerWidth?: LayoutProps["maxW"];
previewMaxWidth?: string;
renderPreview?: (fileUrl: string) => React.ReactNode;
helperText?: string;
selectOrUpload?: "Select" | "Upload";
isDisabledText?: string;
showPreview?: boolean;
children?: React.ReactNode;
className?: string;
}
export const FileInput: React.FC<IFileInputProps> = ({
@@ -44,13 +37,13 @@ export const FileInput: React.FC<IFileInputProps> = ({
showUploadButton,
value,
children,
maxContainerWidth,
renderPreview,
helperText,
selectOrUpload = "Select",
isDisabledText = "Upload Disabled",
showPreview = true,
...restBoxProps
className,
previewMaxWidth,
}) => {
const onDrop = useCallback<
<T extends File>(
@@ -107,111 +100,99 @@ export const FileInput: React.FC<IFileInputProps> = ({
if (children) {
return (
<Box {...restBoxProps} cursor="pointer" {...getRootProps()}>
<div className={cn("cursor-pointer", className)} {...getRootProps()}>
{children}
<input {...getInputProps()} />
</Box>
</div>
);
}
return (
<Stack
spacing={4}
direction="row"
align="center"
{...getRootProps()}
// onClick={(e) => e.stopPropagation()}
>
<div className="flex gap-4 items-center" {...getRootProps()}>
<input {...getInputProps()} />
{showPreview && (
<AspectRatio w="100%" maxW={maxContainerWidth} ratio={1}>
<>
{isDisabled ? (
<Center
{...restBoxProps}
cursor="not-allowed"
opacity={0.5}
bg="inputBg"
_hover={{
bg: "inputBgHover",
borderColor: "primary.500",
<div
style={{
aspectRatio: 1,
maxWidth: previewMaxWidth,
}}
borderColor="inputBorder"
borderWidth="1px"
className={cn(
"opacity-50 cursor-not-allowed bg-muted",
"w-full flex items-center justify-center hover:bg-accent border border-border hover:ring-2 hover:ring-ring",
className,
)}
>
<Stack align="center" color="gray.600">
<Icon boxSize={6} as={FiUpload} />
<Text color="gray.500" textAlign="center">
{isDisabledText}
</Text>
</Stack>
</Center>
<div className="flex flex-col items-center text-secondary-foreground gap-2">
<FiUpload className="size-6" />
<p className="text-sm text-center">{isDisabledText}</p>
</div>
</div>
) : (
<Center
{...restBoxProps}
cursor="pointer"
bg={fileUrl ? "transparent" : "inputBg"}
_hover={{
bg: "inputBgHover",
borderColor: "primary.500",
<div
style={{
aspectRatio: 1,
maxWidth: previewMaxWidth,
}}
borderColor="inputBorder"
borderWidth="1px"
position="relative"
overflow="hidden"
className={cn(
"cursor-pointer relative overflow-hidden",
"w-full flex items-center justify-center hover:bg-accent border border-border hover:ring-2 hover:ring-ring",
fileUrl ? "bg-transparent" : "bg-muted",
className,
)}
>
{noDisplay ? (
<Stack align="center" color="gray.600">
<Icon boxSize={6} as={FiImage} />
<Text color="gray.600">{fileType} uploaded</Text>
</Stack>
<div className="flex flex-col items-center text-secondary-foreground gap-2">
<FiImage className="size-6" />
<p className="text-sm">{fileType} uploaded</p>
</div>
) : fileUrl ? (
renderPreview ? (
renderPreview(fileUrl)
) : (
<Image
<img
alt=""
top={0}
left={0}
position="absolute"
w="100%"
h="100%"
className="top-0 left-0 absolute w-full h-full object-contain"
src={fileUrl}
objectFit="contain"
/>
)
) : (
<Stack align="center" color="gray.600">
<Icon boxSize={6} as={FiUpload} />
<Text color="gray.600" textAlign="center">
<div className="flex flex-col items-center text-secondary-foreground gap-2">
<FiUpload className="size-6" />
<p className="text-sm text-center">
Upload {helperTextOrFile}
</Text>
</Stack>
</p>
</div>
)}
</Center>
</div>
)}
</AspectRatio>
</>
)}
{showUploadButton || noDisplay ? (
<Flex direction="column" borderStyle="none">
<div className="flex flex-col">
{showUploadButton && (
<Button
leftIcon={<Icon as={AiOutlineFileAdd} />}
onClick={open}
variant="outline"
isDisabled={isDisabled}
disabled={isDisabled}
className="gap-2"
>
<AiOutlineFileAdd className="size-4" />
{selectOrUpload} {helperTextOrFile}
</Button>
)}
{noDisplay && (
<Link href={fileUrl} isExternal textDecor="none !important">
<Button leftIcon={<Icon as={AiFillEye} />} variant="outline">
<Link href={fileUrl} target="_blank" className="no-underline">
<Button variant="outline" className="gap-2">
<AiFillEye className="size-4" />
View File
</Button>
</Link>
)}
</Flex>
</div>
) : null}
</Stack>
</div>
);
};
@@ -1,6 +1,6 @@
import { useDashboardStorageUpload } from "@3rdweb-sdk/react/hooks/useDashboardStorageUpload";
import { Box, Input, InputGroup, InputRightElement } from "@chakra-ui/react";
import { useQueryClient } from "@tanstack/react-query";
import { useStorageUpload } from "@thirdweb-dev/react";
import { IpfsUploadButton } from "components/ipfs-upload/button";
import { PINNED_FILES_QUERY_KEY_ROOT } from "components/storage/your-files";
import type { SolidityInputWithTypeProps } from ".";
@@ -11,7 +11,7 @@ export const SolidityStringInput: React.FC<SolidityInputWithTypeProps> = ({
functionName,
...inputProps
}) => {
const storageUpload = useStorageUpload();
const storageUpload = useDashboardStorageUpload();
const queryClient = useQueryClient();
const { name, ...restOfInputProps } = inputProps;
const inputName = name as string;
@@ -13,20 +13,8 @@ import {
Tooltip,
useModalContext,
} from "@chakra-ui/react";
import {
type UseContractResult,
useContract,
useContractType,
type useCreateAuctionListing,
type useCreateDirectListing,
useOwnedNFTs,
} from "@thirdweb-dev/react";
import type {
Marketplace,
MarketplaceV3,
NewAuctionListing,
NewDirectListing,
} from "@thirdweb-dev/sdk";
import { useContract, useOwnedNFTs } from "@thirdweb-dev/react";
import type { NewAuctionListing, NewDirectListing } from "@thirdweb-dev/sdk";
import { CurrencySelector } from "components/shared/CurrencySelector";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { formatEther, parseEther } from "ethers/lib/utils";
@@ -39,7 +27,14 @@ import type { WalletNFT } from "lib/wallet/nfts/types";
import { useMemo } from "react";
import { useForm } from "react-hook-form";
import { FiInfo } from "react-icons/fi";
import { NATIVE_TOKEN_ADDRESS } from "thirdweb";
import type { UseMutateFunction } from "react-query-v5";
import {
NATIVE_TOKEN_ADDRESS,
type PreparedTransaction,
type ThirdwebContract,
} from "thirdweb";
import type { TransactionReceipt } from "thirdweb/dist/types/transaction/types";
import { createAuction, createListing } from "thirdweb/extensions/marketplace";
import { useActiveAccount } from "thirdweb/react";
import {
FormErrorMessage,
@@ -64,13 +59,15 @@ interface ListForm
}
type NFTMintForm = {
contractQuery:
| UseContractResult<Marketplace>
| UseContractResult<MarketplaceV3>;
directList: ReturnType<typeof useCreateDirectListing>;
auctionList: ReturnType<typeof useCreateAuctionListing>;
contract: ThirdwebContract;
formId: string;
type?: "direct-listings" | "english-auctions";
mutate: UseMutateFunction<
TransactionReceipt,
Error,
PreparedTransaction,
unknown
>;
};
const auctionTimes = [
@@ -84,11 +81,10 @@ const auctionTimes = [
];
export const CreateListingsForm: React.FC<NFTMintForm> = ({
contractQuery,
directList,
auctionList,
contract,
formId,
type,
mutate,
}) => {
const trackEvent = useTrack();
const network = useEVMContractInfo()?.chain;
@@ -100,10 +96,6 @@ export const CreateListingsForm: React.FC<NFTMintForm> = ({
isAlchemySupported(chainId) ||
isMoralisSupported(chainId));
const { data: contractType } = useContractType(
contractQuery?.contract?.getAddress(),
);
const { data: walletNFTs, isLoading: isWalletNFTsLoading } = useWalletNFTs();
const form = useForm<ListForm>({
@@ -122,12 +114,15 @@ export const CreateListingsForm: React.FC<NFTMintForm> = ({
},
});
const { contract } = useContract(form.watch("contractAddress"));
const { contract: selectedContract } = useContract(
form.watch("contractAddress"),
);
const address = useActiveAccount()?.address;
const { data: ownedNFTs, isLoading: isOwnedNFTsLoading } = useOwnedNFTs(
contract,
selectedContract,
address,
);
const isSelected = (nft: WalletNFT) => {
return (
form.watch("selected")?.tokenId === nft.tokenId &&
@@ -155,6 +150,7 @@ export const CreateListingsForm: React.FC<NFTMintForm> = ({
"NFT listed successfully",
"Failed to list NFT",
);
return (
<Stack
spacing={6}
@@ -165,80 +161,69 @@ export const CreateListingsForm: React.FC<NFTMintForm> = ({
return;
}
if (formData.listingType === "direct") {
directList.mutate(
{
assetContractAddress: formData.selected.contractAddress,
tokenId: formData.selected.tokenId,
currencyContractAddress: formData.currencyContractAddress,
quantity: formData.quantity,
startTimestamp: formData.startTimestamp,
// Hard code to year 2100 for now
pricePerToken: formData.buyoutPricePerToken,
endTimestamp: new Date(4102444800000),
// Marketplace v1 only params
buyoutPricePerToken: formData.buyoutPricePerToken,
// Hard code to 100 years for now
listingDurationInSeconds: (60 * 60 * 24 * 365 * 100).toString(),
},
{
onSuccess: () => {
onSuccess();
modalContext.onClose();
},
onError,
},
// Hard code to 100 years for now
const endTimestamp = new Date(
new Date().setFullYear(new Date().getFullYear() + 100),
);
const transaction = createListing({
contract,
assetContractAddress: formData.selected.contractAddress,
tokenId: BigInt(formData.selected.tokenId),
currencyContractAddress: formData.currencyContractAddress,
quantity: BigInt(formData.quantity),
startTimestamp: formData.startTimestamp,
pricePerToken: String(formData.buyoutPricePerToken),
endTimestamp,
});
mutate(transaction, {
onSuccess: () => {
onSuccess();
modalContext.onClose();
},
onError,
});
} else if (formData.listingType === "auction") {
auctionList.mutate(
{
assetContractAddress: formData.selected.contractAddress,
tokenId: formData.selected.tokenId,
quantity: formData.quantity,
startTimestamp: formData.startTimestamp,
currencyContractAddress: formData.currencyContractAddress,
minimumBidAmount: mulDecimalByQuantity(
formData.reservePricePerToken,
formData.quantity,
),
buyoutBidAmount: mulDecimalByQuantity(
formData.buyoutPricePerToken,
formData.quantity,
),
// Create endTimestamp with the current date + listingDurationInSeconds
endTimestamp: new Date(
new Date().getTime() +
Number.parseInt(formData.listingDurationInSeconds) * 1000,
),
// Marketplace v1 only params
reservePricePerToken: formData.reservePricePerToken,
buyoutPricePerToken: formData.buyoutPricePerToken,
listingDurationInSeconds: formData.listingDurationInSeconds,
const transaction = createAuction({
contract,
assetContractAddress: formData.selected.contractAddress,
tokenId: BigInt(formData.selected.tokenId),
startTimestamp: formData.startTimestamp,
currencyContractAddress: formData.currencyContractAddress,
endTimestamp: new Date(
new Date().getTime() +
Number.parseInt(formData.listingDurationInSeconds) * 1000,
),
minimumBidAmount: mulDecimalByQuantity(
formData.reservePricePerToken,
formData.quantity,
),
buyoutBidAmount: mulDecimalByQuantity(
formData.buyoutPricePerToken,
formData.quantity,
),
});
mutate(transaction, {
onSuccess: () => {
onSuccess();
trackEvent({
category: "marketplace",
action: "add-listing",
label: "success",
network,
});
modalContext.onClose();
},
{
onSuccess: () => {
onSuccess();
trackEvent({
category: "marketplace",
action: "add-listing",
label: "success",
network,
});
modalContext.onClose();
},
onError: (error) => {
trackEvent({
category: "marketplace",
action: "add-listing",
label: "error",
network,
error,
});
onError(error);
},
onError: (error) => {
trackEvent({
category: "marketplace",
action: "add-listing",
label: "error",
network,
error,
});
onError(error);
},
);
});
}
})}
>
@@ -369,21 +354,20 @@ export const CreateListingsForm: React.FC<NFTMintForm> = ({
</Stack>
) : null}
</FormControl>
{contractType === "marketplace" ? (
<FormControl isDisabled={noNfts}>
<Heading as={FormLabel} size="label.lg">
Listing Type
</Heading>
<Select {...form.register("listingType")}>
<option value="direct">Direct</option>
<option value="auction">Auction</option>
</Select>
<FormHelperText>
The type of listing you want to create, either an auction or direct
listing.
</FormHelperText>
</FormControl>
) : null}
{/* Just don't support Marketplace v2 anymore */}
{/* <FormControl isDisabled={noNfts}>
<Heading as={FormLabel} size="label.lg">
Listing Type
</Heading>
<Select {...form.register("listingType")}>
<option value="direct">Direct</option>
<option value="auction">Auction</option>
</Select>
<FormHelperText>
The type of listing you want to create, either an auction or direct
listing.
</FormHelperText>
</FormControl> */}
<FormControl isRequired isDisabled={noNfts}>
<Heading as={FormLabel} size="label.lg">
Listing Currency
@@ -8,56 +8,53 @@ import {
useTotalCount,
} from "@thirdweb-dev/react";
import type { UploadProgressEvent } from "@thirdweb-dev/sdk";
import { extensionDetectedState } from "components/buttons/ExtensionDetectButton";
import { detectFeatures } from "components/contract-components/utils";
import { BatchLazyMint } from "core-ui/batch-upload/batch-lazy-mint";
import { ProgressBox } from "core-ui/batch-upload/progress-box";
import { BigNumber } from "ethers";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { thirdwebClient } from "lib/thirdweb-client";
import { defineDashboardChain } from "lib/v5-adapter";
import { useState } from "react";
import { RiCheckboxMultipleBlankLine } from "react-icons/ri";
import { getContract } from "thirdweb";
import { Button, Drawer } from "tw-components";
interface BatchLazyMintButtonProps {
contractQuery: ReturnType<typeof useContract>;
isRevealable: boolean;
}
export const BatchLazyMintButton: React.FC<BatchLazyMintButtonProps> = ({
contractQuery,
isRevealable,
...restButtonProps
}) => {
const contractV4 = contractQuery.contract;
const contract = contractV4
? getContract({
address: contractV4.getAddress(),
chain: defineDashboardChain(contractV4.chainId),
client: thirdwebClient,
})
: null;
const trackEvent = useTrack();
const { isOpen, onOpen, onClose } = useDisclosure();
const nextTokenIdToMint = useTotalCount(contractQuery.contract);
const nextTokenIdToMint = useTotalCount(contractV4);
const [progress, setProgress] = useState<UploadProgressEvent>({
progress: 0,
total: 100,
});
const detectedState = extensionDetectedState({
contractQuery,
feature: [
"ERC721LazyMintable",
"ERC1155LazyMintableV1",
"ERC1155LazyMintableV2",
],
});
const isRevealable = detectFeatures(contractQuery.contract, [
"ERC721Revealable",
"ERC1155Revealable",
]);
const mintBatchMutation = useLazyMint(
contractQuery.contract,
contractV4,
(event: UploadProgressEvent) => {
setProgress(event);
},
);
const mintDelayedRevealBatchMutation = useDelayedRevealLazyMint(
contractQuery.contract as RevealableContract,
contractV4 as RevealableContract,
(event: UploadProgressEvent) => {
setProgress(event);
},
@@ -68,13 +65,11 @@ export const BatchLazyMintButton: React.FC<BatchLazyMintButtonProps> = ({
"Error uploading batch",
contractQuery.contract,
);
if (detectedState !== "enabled") {
if (!contract) {
return null;
}
return (
<MinterOnly contract={contractQuery?.contract}>
<MinterOnly contract={contract}>
<Drawer
allowPinchZoom
preserveScrollBarGap
@@ -1,36 +1,20 @@
import { Icon, useDisclosure } from "@chakra-ui/react";
import { GiDiamondHard } from "@react-icons/all-files/gi/GiDiamondHard";
import type { useContract } from "@thirdweb-dev/react";
import { extensionDetectedState } from "components/buttons/ExtensionDetectButton";
import { Button, Drawer } from "tw-components";
import { NFTClaimForm } from "./claim-form";
interface NFTClaimButtonProps {
contractQuery: ReturnType<typeof useContract>;
contractAddress: string;
chainId: number;
}
export const NFTClaimButton: React.FC<NFTClaimButtonProps> = ({
contractQuery,
contractAddress,
chainId,
...restButtonProps
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const detectedState = extensionDetectedState({
contractQuery,
feature: [
// erc 721
"ERC721ClaimPhasesV1",
"ERC721ClaimPhasesV2",
"ERC721ClaimConditionsV1",
"ERC721ClaimConditionsV2",
"ERC721ClaimCustom",
],
});
if (detectedState !== "enabled" || !contractQuery.contract) {
return null;
}
return (
<>
<Drawer
@@ -40,7 +24,7 @@ export const NFTClaimButton: React.FC<NFTClaimButtonProps> = ({
onClose={onClose}
isOpen={isOpen}
>
<NFTClaimForm contract={contractQuery.contract} />
<NFTClaimForm contractAddress={contractAddress} chainId={chainId} />
</Drawer>
<Button
colorScheme="primary"
@@ -7,13 +7,15 @@ import {
Stack,
useModalContext,
} from "@chakra-ui/react";
import { type DropContract, useClaimNFT } from "@thirdweb-dev/react";
import { TransactionButton } from "components/buttons/TransactionButton";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { thirdwebClient } from "lib/thirdweb-client";
import { defineDashboardChain } from "lib/v5-adapter";
import { useForm } from "react-hook-form";
import { ZERO_ADDRESS } from "thirdweb";
import { useActiveAccount } from "thirdweb/react";
import { ZERO_ADDRESS, getContract } from "thirdweb";
import { claimTo } from "thirdweb/extensions/erc721";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import {
FormErrorMessage,
FormHelperText,
@@ -23,12 +25,20 @@ import {
const CLAIM_FORM_ID = "nft-claim-form";
interface NFTClaimFormProps {
contract: DropContract;
contractAddress: string;
chainId: number;
}
export const NFTClaimForm: React.FC<NFTClaimFormProps> = ({ contract }) => {
export const NFTClaimForm: React.FC<NFTClaimFormProps> = ({
contractAddress,
chainId,
}) => {
const contract = getContract({
address: contractAddress,
chain: defineDashboardChain(chainId),
client: thirdwebClient,
});
const trackEvent = useTrack();
const claim = useClaimNFT(contract);
const address = useActiveAccount()?.address;
const {
register,
@@ -42,7 +52,7 @@ export const NFTClaimForm: React.FC<NFTClaimFormProps> = ({ contract }) => {
"Failed to claim NFT",
contract,
);
const { mutate, isPending } = useSendAndConfirmTransaction();
return (
<>
<DrawerHeader>
@@ -70,7 +80,7 @@ export const NFTClaimForm: React.FC<NFTClaimFormProps> = ({ contract }) => {
<TransactionButton
transactionCount={1}
form={CLAIM_FORM_ID}
isLoading={claim.isLoading}
isLoading={isPending}
type="submit"
colorScheme="primary"
onClick={handleSubmit((d) => {
@@ -79,29 +89,36 @@ export const NFTClaimForm: React.FC<NFTClaimFormProps> = ({ contract }) => {
action: "claim",
label: "attempt",
});
claim.mutate(
{ quantity: d.amount, to: d.to },
{
onSuccess: () => {
trackEvent({
category: "nft",
action: "claim",
label: "success",
});
onSuccess();
modalContext.onClose();
},
onError: (error) => {
trackEvent({
category: "nft",
action: "claim",
label: "error",
error,
});
onError(error);
},
if (!d.to) {
return onError(
new Error("Please enter the address that will receive the NFT"),
);
}
const transaction = claimTo({
contract,
to: d.to,
quantity: BigInt(d.amount),
});
mutate(transaction, {
onSuccess: () => {
trackEvent({
category: "nft",
action: "claim",
label: "success",
});
onSuccess();
modalContext.onClose();
},
);
onError: (error) => {
trackEvent({
category: "nft",
action: "claim",
label: "error",
error,
});
onError(error);
},
});
})}
>
Claim NFT
@@ -1,8 +1,10 @@
import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
import { Icon, useDisclosure } from "@chakra-ui/react";
import { type useContract, useLazyMint } from "@thirdweb-dev/react";
import { extensionDetectedState } from "components/buttons/ExtensionDetectButton";
import { thirdwebClient } from "lib/thirdweb-client";
import { defineDashboardChain } from "lib/v5-adapter";
import { FiPlus } from "react-icons/fi";
import { getContract } from "thirdweb";
import { Button, Drawer } from "tw-components";
import { NFTMintForm } from "./mint-form";
@@ -14,24 +16,21 @@ export const NFTLazyMintButton: React.FC<NFTLazyMintButtonProps> = ({
contractQuery,
...restButtonProps
}) => {
const contractV4 = contractQuery.contract;
const contract = contractV4
? getContract({
address: contractV4.getAddress(),
chain: defineDashboardChain(contractV4.chainId),
client: thirdwebClient,
})
: null;
const { isOpen, onOpen, onClose } = useDisclosure();
const mutation = useLazyMint(contractQuery.contract);
const detectedState = extensionDetectedState({
contractQuery,
feature: [
"ERC721LazyMintable",
"ERC1155LazyMintableV1",
"ERC1155LazyMintableV2",
],
});
if (detectedState !== "enabled" || !contractQuery.contract) {
const mutation = useLazyMint(contractV4);
if (!contract) {
return null;
}
return (
<MinterOnly contract={contractQuery.contract}>
<MinterOnly contract={contract}>
<Drawer
allowPinchZoom
preserveScrollBarGap
@@ -39,10 +38,7 @@ export const NFTLazyMintButton: React.FC<NFTLazyMintButtonProps> = ({
onClose={onClose}
isOpen={isOpen}
>
<NFTMintForm
contract={contractQuery.contract}
lazyMintMutation={mutation}
/>
<NFTMintForm contract={contractV4} lazyMintMutation={mutation} />
</Drawer>
<Button
colorScheme="primary"
@@ -1,8 +1,10 @@
import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
import { Icon, useDisclosure } from "@chakra-ui/react";
import { type useContract, useMintNFT } from "@thirdweb-dev/react";
import { extensionDetectedState } from "components/buttons/ExtensionDetectButton";
import { thirdwebClient } from "lib/thirdweb-client";
import { defineDashboardChain } from "lib/v5-adapter";
import { FiPlus } from "react-icons/fi";
import { getContract } from "thirdweb";
import { Button, Drawer } from "tw-components";
import { NFTMintForm } from "./mint-form";
@@ -14,20 +16,21 @@ export const NFTMintButton: React.FC<NFTMintButtonProps> = ({
contractQuery,
...restButtonProps
}) => {
const contractV4 = contractQuery.contract;
const contract = contractV4
? getContract({
address: contractV4.getAddress(),
chain: defineDashboardChain(contractV4.chainId),
client: thirdwebClient,
})
: null;
const { isOpen, onOpen, onClose } = useDisclosure();
const mutation = useMintNFT(contractQuery.contract);
const detectedState = extensionDetectedState({
contractQuery,
feature: ["ERC721Mintable", "ERC1155Mintable"],
});
if (detectedState !== "enabled" || !contractQuery.contract) {
const mutation = useMintNFT(contractV4);
if (!contract) {
return null;
}
return (
<MinterOnly contract={contractQuery.contract}>
<MinterOnly contract={contract}>
<Drawer
allowPinchZoom
preserveScrollBarGap
@@ -35,10 +38,7 @@ export const NFTMintButton: React.FC<NFTMintButtonProps> = ({
onClose={onClose}
isOpen={isOpen}
>
<NFTMintForm
contract={contractQuery.contract}
mintMutation={mutation}
/>
<NFTMintForm contract={contractV4} mintMutation={mutation} />
</Drawer>
<Button
colorScheme="primary"
@@ -23,7 +23,6 @@ import type {
useSetSharedMetadata,
useUpdateNFTMetadata,
} from "@thirdweb-dev/react";
import type { NFTMetadataInput } from "@thirdweb-dev/sdk";
import { OpenSeaPropertyBadge } from "components/badges/opensea";
import { TransactionButton } from "components/buttons/TransactionButton";
import { detectFeatures } from "components/contract-components/utils";
@@ -36,6 +35,7 @@ import { useMemo } from "react";
import { useForm } from "react-hook-form";
import type { NFT } from "thirdweb";
import { useActiveAccount } from "thirdweb/react";
import type { NFTInput } from "thirdweb/utils";
import {
Button,
FormErrorMessage,
@@ -64,7 +64,7 @@ type NFTMintForm =
lazyMintMutation: UseMutationResult<
unknown,
unknown,
{ metadatas: NFTMetadataInput[] }
{ metadatas: NFTInput[] }
>;
mintMutation?: undefined;
sharedMetadataMutation?: undefined;
@@ -112,8 +112,8 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({
background_color: nft?.metadata.background_color || "",
attributes: nft?.metadata.attributes || [],
// We override these in the submit if they haven't been changed
image: nft?.metadata.image || null,
animation_url: nft?.metadata.animation_url || null,
image: nft?.metadata.image || undefined,
animation_url: nft?.metadata.animation_url || undefined,
// No need for these, but we need to pass them to the form
supply: 0,
customImage: "",
@@ -192,7 +192,7 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({
}
};
const imageUrl = useImageFileOrUrl(watch("image"));
const imageUrl = useImageFileOrUrl(watch("image") as File | string);
const mediaFileUrl =
watch("animation_url") instanceof File
? watch("animation_url")
@@ -413,18 +413,14 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({
)}
<Box>
<FileInput
maxContainerWidth={"200px"}
value={mediaFileUrl}
previewMaxWidth="200px"
value={mediaFileUrl as File | string}
showUploadButton
showPreview={nft?.metadata ? !!mediaFileUrl : true}
setValue={setFile}
border="1px solid"
borderColor="gray.200"
borderRadius="md"
transition="all 200ms ease"
className="border border-border rounded transition-all duration-200"
selectOrUpload="Upload"
helperText={nft?.metadata ? "New Media" : "Media"}
_hover={{ shadow: "sm" }}
/>
</Box>
<FormHelperText>
@@ -439,16 +435,12 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({
<FormControl isInvalid={!!errors.image}>
<FormLabel>Cover Image</FormLabel>
<FileInput
maxContainerWidth={"200px"}
previewMaxWidth="200px"
accept={{ "image/*": [] }}
value={imageUrl}
showUploadButton
setValue={(file) => setValue("image", file)}
border="1px solid"
borderColor="gray.200"
borderRadius="md"
transition="all 200ms ease"
_hover={{ shadow: "sm" }}
className="border border-border rounded transition-all"
/>
<FormHelperText>
You can optionally upload an image as the cover of your NFT.
@@ -1,8 +1,10 @@
import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
import { Icon, useDisclosure } from "@chakra-ui/react";
import { useBatchesToReveal, type useContract } from "@thirdweb-dev/react";
import { extensionDetectedState } from "components/buttons/ExtensionDetectButton";
import { thirdwebClient } from "lib/thirdweb-client";
import { defineDashboardChain } from "lib/v5-adapter";
import { FiEye } from "react-icons/fi";
import { getContract } from "thirdweb";
import { Button, Drawer } from "tw-components";
import { NFTRevealForm } from "./reveal-form";
@@ -15,20 +17,20 @@ export const NFTRevealButton: React.FC<NFTRevealButtonProps> = ({
...restButtonProps
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const detectedState = extensionDetectedState({
contractQuery,
feature: ["ERC721Revealable", "ERC1155Revealable"],
});
const { data: batchesToReveal } = useBatchesToReveal(contractQuery.contract);
if (detectedState !== "enabled" || !contractQuery.contract) {
const contractV4 = contractQuery.contract;
const contract = contractV4
? getContract({
address: contractV4.getAddress(),
chain: defineDashboardChain(contractV4.chainId),
client: thirdwebClient,
})
: null;
const { data: batchesToReveal } = useBatchesToReveal(contractV4);
if (!contract || !contractV4) {
return null;
}
return batchesToReveal?.length ? (
<MinterOnly contract={contractQuery.contract}>
<MinterOnly contract={contract}>
<Drawer
allowPinchZoom
preserveScrollBarGap
@@ -36,7 +38,7 @@ export const NFTRevealButton: React.FC<NFTRevealButtonProps> = ({
onClose={onClose}
isOpen={isOpen}
>
<NFTRevealForm contract={contractQuery.contract} />
<NFTRevealForm contract={contractV4} />
</Drawer>
<Button
colorScheme="primary"
@@ -1,8 +1,10 @@
import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
import { Icon, useDisclosure } from "@chakra-ui/react";
import { type useContract, useSetSharedMetadata } from "@thirdweb-dev/react";
import { extensionDetectedState } from "components/buttons/ExtensionDetectButton";
import { thirdwebClient } from "lib/thirdweb-client";
import { defineDashboardChain } from "lib/v5-adapter";
import { FiPlus } from "react-icons/fi";
import { getContract } from "thirdweb";
import { Button, Drawer } from "tw-components";
import { NFTMintForm } from "./mint-form";
@@ -13,19 +15,21 @@ interface NFTSharedMetadataButtonProps {
export const NFTSharedMetadataButton: React.FC<
NFTSharedMetadataButtonProps
> = ({ contractQuery, ...restButtonProps }) => {
const contractV4 = contractQuery.contract;
const contract = contractV4
? getContract({
address: contractV4.getAddress(),
chain: defineDashboardChain(contractV4.chainId),
client: thirdwebClient,
})
: null;
const { isOpen, onOpen, onClose } = useDisclosure();
const mutation = useSetSharedMetadata(contractQuery.contract);
const detectedState = extensionDetectedState({
contractQuery,
feature: ["ERC721SharedMetadata"],
});
if (detectedState !== "enabled" || !contractQuery.contract) {
const mutation = useSetSharedMetadata(contractV4);
if (!contract) {
return null;
}
return (
<MinterOnly contract={contractQuery.contract}>
<MinterOnly contract={contract}>
<Drawer
allowPinchZoom
preserveScrollBarGap
@@ -33,10 +37,7 @@ export const NFTSharedMetadataButton: React.FC<
onClose={onClose}
isOpen={isOpen}
>
<NFTMintForm
contract={contractQuery.contract}
sharedMetadataMutation={mutation}
/>
<NFTMintForm contract={contractV4} sharedMetadataMutation={mutation} />
</Drawer>
<Button
colorScheme="primary"
@@ -75,10 +75,9 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
{
Header: "Media",
accessor: (row) => row.metadata,
Cell: (
cell: CellProps<NFT, NFT["metadata"]>,
// @ts-expect-error - types are not compatible yet until we have NFTRenderer in v5
) => <MediaCell cell={cell} />,
Cell: (cell: CellProps<NFT, NFT["metadata"]>) => (
<MediaCell cell={cell} />
),
},
{
Header: "Name",
@@ -1,5 +1,6 @@
import { Box, Flex } from "@chakra-ui/react";
import { useContract } from "@thirdweb-dev/react";
import { extensionDetectedState } from "components/buttons/ExtensionDetectButton";
import { detectFeatures } from "components/contract-components/utils";
import { useRouter } from "next/router";
import type { ThirdwebContract } from "thirdweb";
@@ -28,26 +29,10 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
contract,
}) => {
const contractQuery = useContract(contractAddress);
const router = useRouter();
const tokenId = router.query?.paths?.[2];
const detectedState = detectFeatures(contractQuery?.contract, [
"ERC721Enumerable",
"ERC1155Enumerable",
"ERC721Supply",
]);
const isErc721 = detectFeatures(contractQuery?.contract, ["ERC721"]);
const isErc721Claimable = detectFeatures(contractQuery?.contract, [
"ERC721ClaimPhasesV1",
"ERC721ClaimPhasesV2",
"ERC721ClaimConditionsV1",
"ERC721ClaimConditionsV2",
"ERC721ClaimCustom",
]);
if (tokenId && isOnlyNumbers(tokenId)) {
return (
<TokenIdPage
@@ -64,17 +49,100 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
return <div>Loading...</div>;
}
const detectedClaimState = extensionDetectedState({
contractQuery,
feature: [
"ERC721ClaimPhasesV1",
"ERC721ClaimPhasesV2",
"ERC721ClaimConditionsV1",
"ERC721ClaimConditionsV2",
"ERC721ClaimCustom",
],
});
const isErc721Claimable = detectFeatures(contractQuery?.contract, [
"ERC721ClaimPhasesV1",
"ERC721ClaimPhasesV2",
"ERC721ClaimConditionsV1",
"ERC721ClaimConditionsV2",
"ERC721ClaimCustom",
]);
const detectedState = detectFeatures(contractQuery?.contract, [
"ERC721Enumerable",
"ERC1155Enumerable",
"ERC721Supply",
]);
const detectedLzyMintState = extensionDetectedState({
contractQuery,
feature: [
"ERC721LazyMintable",
"ERC1155LazyMintableV1",
"ERC1155LazyMintableV2",
],
});
const isRevealable = detectFeatures(contractQuery.contract, [
"ERC721Revealable",
"ERC1155Revealable",
]);
const detectedRevealableState = extensionDetectedState({
contractQuery,
feature: ["ERC721Revealable", "ERC1155Revealable"],
});
const detectedMintableState = extensionDetectedState({
contractQuery,
feature: ["ERC721Mintable", "ERC1155Mintable"],
});
const detectedLazyMintableState = extensionDetectedState({
contractQuery,
feature: [
"ERC721LazyMintable",
"ERC1155LazyMintableV1",
"ERC1155LazyMintableV2",
],
});
const detectedSharedMetadataState = extensionDetectedState({
contractQuery,
feature: ["ERC721SharedMetadata"],
});
return (
<Flex direction="column" gap={6}>
<Flex direction="row" justify="space-between" align="center">
<Heading size="title.sm">Contract NFTs</Heading>
<Flex gap={2} flexDir={{ base: "column", md: "row" }}>
<NFTRevealButton contractQuery={contractQuery} />
<NFTClaimButton contractQuery={contractQuery} />
<NFTMintButton contractQuery={contractQuery} />
<NFTSharedMetadataButton contractQuery={contractQuery} />
<NFTLazyMintButton contractQuery={contractQuery} />
<BatchLazyMintButton contractQuery={contractQuery} />
{detectedRevealableState === "enabled" && contractQuery?.contract && (
<NFTRevealButton contractQuery={contractQuery} />
)}
{detectedClaimState && contractQuery?.contract && (
<NFTClaimButton
contractAddress={contractQuery.contract.getAddress()}
chainId={contractQuery.contract.chainId}
/>
)}
{detectedMintableState === "enabled" && contractQuery?.contract && (
<NFTMintButton contractQuery={contractQuery} />
)}
{detectedSharedMetadataState === "enabled" &&
contractQuery?.contract && (
<NFTSharedMetadataButton contractQuery={contractQuery} />
)}
{detectedLazyMintableState === "enabled" &&
contractQuery?.contract && (
<NFTLazyMintButton contractQuery={contractQuery} />
)}
{detectedLzyMintState === "enabled" && contractQuery?.contract && (
<BatchLazyMintButton
contractQuery={contractQuery}
isRevealable={isRevealable}
/>
)}
</Flex>
</Flex>
{!detectedState ? (
@@ -1,3 +1,4 @@
import { thirdwebClient } from "@/constants/client";
import {
useIsAdmin,
useIsMinter,
@@ -16,6 +17,8 @@ import { detectFeatures } from "components/contract-components/utils";
import { StepsCard } from "components/dashboard/StepsCard";
import { useTabHref } from "contract-ui/utils";
import { BigNumber } from "ethers";
import { defineDashboardChain } from "lib/v5-adapter";
import { getContract } from "thirdweb";
import { Link, Text } from "tw-components";
interface ContractChecklistProps {
@@ -31,6 +34,11 @@ type Step = {
export const ContractChecklist: React.FC<ContractChecklistProps> = ({
contract,
}) => {
const contractv5 = getContract({
address: contract.getAddress(),
chain: defineDashboardChain(contract.chainId),
client: thirdwebClient,
});
const nftHref = useTabHref("nfts");
const tokenHref = useTabHref("tokens");
const accountsHref = useTabHref("accounts");
@@ -53,7 +61,7 @@ export const ContractChecklist: React.FC<ContractChecklistProps> = ({
];
const isAdmin = useIsAdmin(contract);
const isMinter = useIsMinter(contract);
const isMinter = useIsMinter(contractv5);
if (!isAdmin) {
return null;
@@ -223,10 +223,7 @@ export const SettingsMetadata = <
shouldDirty: true,
})
}
border="1px solid"
borderColor="gray.200"
borderRadius="md"
transition="all 200ms ease"
className="rounded border border-border transition-all duration-200"
/>
<FormErrorMessage>
{getFieldState("image", formState).error?.message}
@@ -1,14 +1,13 @@
import { ListerOnly } from "@3rdweb-sdk/react/components/roles/lister-only";
import { Icon, useDisclosure } from "@chakra-ui/react";
import {
type UseContractResult,
useCreateAuctionListing,
useCreateDirectListing,
} from "@thirdweb-dev/react";
import type { UseContractResult } from "@thirdweb-dev/react";
import type { Marketplace, MarketplaceV3 } from "@thirdweb-dev/sdk";
import { TransactionButton } from "components/buttons/TransactionButton";
import { thirdwebClient } from "lib/thirdweb-client";
import { defineDashboardChain } from "lib/v5-adapter";
import { FiPlus } from "react-icons/fi";
import { useActiveAccount } from "thirdweb/react";
import { getContract } from "thirdweb";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { Button, Drawer } from "tw-components";
import { CreateListingsForm } from "../listings/components/list-form";
@@ -30,11 +29,17 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
}) => {
const address = useActiveAccount()?.address;
const { isOpen, onOpen, onClose } = useDisclosure();
const directList = useCreateDirectListing(contractQuery.contract);
const auctionList = useCreateAuctionListing(contractQuery.contract);
const { mutate, isPending } = useSendAndConfirmTransaction();
if (!contractQuery?.contract) {
return null;
}
const contract = getContract({
address: contractQuery.contract.getAddress(),
chain: defineDashboardChain(contractQuery.contract.chainId),
client: thirdwebClient,
});
return (
<ListerOnly contract={contractQuery?.contract}>
<ListerOnly contract={contract}>
<Drawer
allowPinchZoom
preserveScrollBarGap
@@ -46,7 +51,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
children: (
<>
<Button
isDisabled={directList.isLoading || auctionList.isLoading}
isDisabled={isPending}
variant="outline"
mr={3}
onClick={onClose}
@@ -54,7 +59,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
Cancel
</Button>
<TransactionButton
isLoading={directList.isLoading || auctionList.isLoading}
isLoading={isPending}
transactionCount={2}
form={LIST_FORM_ID}
type="submit"
@@ -67,11 +72,10 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
}}
>
<CreateListingsForm
contractQuery={contractQuery}
directList={directList}
auctionList={auctionList}
contract={contract}
formId={LIST_FORM_ID}
type={type}
mutate={mutate}
/>
</Drawer>
<Button

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