Compare commits

..
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
9e46b5b94e Version Packages (#4121)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-18 12:37:18 +12:00
gregfromstl e0dcc64256 Fix connect button border in firefox (#4160)
Corrects an issue with the border visibility in firefox by explicitly specifying the div height

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing the connect button border issue on Firefox browser.

### Detailed summary
- Added `height: "50px"` to the button style in `Details.tsx` for proper display on Firefox.

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

<!-- end pr-codex -->
2024-08-18 00:03:46 +00:00
gregfromstl f0d6e343ee Adds EIP-1271 Signature Validation Fallback (#4156)
This PR EIP-1271 fallback for smart contract validation since not all chains support the deployless call used in 6792 flow.

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to fix smart contract wallet signature validation on older chains.

### Detailed summary
- Updated the prompt text for signing in on the wallet UI
- Added a fallback to EIP1271 validation for chains not supporting eth_call simulation
- Introduced a new function `verifyEip1271Signature` for EIP1271 signature validation

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

<!-- end pr-codex -->
2024-08-17 23:43:45 +00:00
gregfromstl cf4443a9eb Improve Blobbie Algo (#4159)
![image](https://github.com/user-attachments/assets/cfc34112-3b94-4125-8aba-4a065d7c9ec2)
![image](https://github.com/user-attachments/assets/9daf26b0-fde8-4a77-bebf-a77f94d444f2)
![image](https://github.com/user-attachments/assets/91c4ceb1-c3c0-4be6-a654-d233b699495a)
![image](https://github.com/user-attachments/assets/7a298ad2-7fa4-4967-a7ea-7e9ec2fcf7be)

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to improve the Blobbie algorithm for generating unique gradient avatars based on provided addresses.

### Detailed summary
- Updated color calculation logic for better color pairing
- Removed unused `nearestColor` function
- Refactored color and position calculations for Blobbie avatar
- Adjusted ellipse positions and sizes for better visual representation

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

<!-- end pr-codex -->
2024-08-17 23:12:15 +00:00
gregfromstl 355795a2d4 Fix text wrapping on connect button (#4157)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing text wrapping on the Connect Button in the `Details.tsx` file.

### Detailed summary
- Added `whiteSpace: "nowrap"` to prevent text wrapping
- Updated styles for better button display

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

<!-- end pr-codex -->
2024-08-17 19:37:43 +00:00
kien-ngo 5fda794569 [SDK] Add more VoteERC20 extensions (for Dashboard) (#4139)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR exposes ERC20Vote extensions and adds more Vote contract extensions.

### Detailed summary
- Exposed ERC20Vote extensions
- Added more Vote contract extensions
- Updated types for vote and proposal states
- Implemented functions for proposal existence, execution, and voting counts

> The following files were skipped due to too many changes: `packages/thirdweb/src/extensions/vote/read/getAll.ts`

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

<!-- end pr-codex -->
2024-08-17 12:28:02 +00:00
kien-ngo 93b3bad783 [SDK] Expose thirdweb extensions (for Dashboard) (#4149)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to export contracts related to Publisher, Multichain Registry, and Contract Factory functionalities.

### Detailed summary
- Added Publisher contract exports for reading and writing operations
- Added Multichain Registry exports for reading and writing operations
- Added Contract Factory export for deploying proxies

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

<!-- end pr-codex -->
2024-08-17 12:02:02 +00:00
Joaquim VergesandGitHub f5952621ab feat(wallet): handle eth_sendTransaction when doing zksync direct deploys (#4153) 2024-08-17 23:52:10 +12:00
kien-ngo 1d9f4e5e3e [Dashboard] MismatchButton to v5 (#4152)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview

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

<!-- end pr-codex -->
2024-08-17 10:01:56 +00:00
gregfromstl 2e99057f90 feat(wallet-ui): initial app (#3478)
1. Created a new `.env.example` file specifying the `NEXT_PUBLIC_ROOT_DOMAIN` variable.
2. Updated `.gitignore` to include `.env*.local` instead of `.env*`.
3. Modified the `Layout` component in `[ecosystem]/layout.tsx` to accept `children` and `params` properties and display the ecosystem and children elements.
4. Updated the `Page` component in `[ecosystem]/page.tsx` to return a simple `div` with the text `Page`.
5. Added a new `middleware.ts` file to handle dynamic routing based on the request host, matching all paths except for specific routes (e.g., `/api`, `/_next`, `/_static`). Rewrites the URL to point to either the root application or the appropriate ecosystem dynamic route.

---

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding ecosystem-related features and improving UI components.

### Detailed summary
- Added `Ecosystem` and token types
- Updated `biome.json` schema
- Implemented revalidate function
- Added NFT loading and card components
- Updated environment variables
- Improved layout and metadata handling
- Added new UI components like `Skeleton`, `Label`, and `Input`
- Enhanced client and ecosystem API integration

> The following files were skipped due to too many changes: `apps/wallet-ui/src/lib/chains.ts`, `apps/wallet-ui/src/components/ui/sonner.tsx`, `apps/wallet-ui/src/components/ConnectButton.tsx`, `apps/wallet-ui/src/app/[ecosystem]/login/page.tsx`, `apps/wallet-ui/src/app/[ecosystem]/(ui)/layout.tsx`, `apps/wallet-ui/src/components/ui/badge.tsx`, `apps/wallet-ui/src/components/ui/popover.tsx`, `apps/wallet-ui/src/middleware.ts`, `apps/wallet-ui/src/lib/assets/erc20.ts`, `apps/wallet-ui/src/components/ui/button.tsx`, `apps/wallet-ui/src/components/NftCard.tsx`, `apps/wallet-ui/src/components/NftGallery.tsx`, `apps/wallet-ui/src/lib/auth.ts`, `apps/wallet-ui/src/lib/assets/erc721.ts`, `apps/wallet-ui/src/app/[ecosystem]/(ui)/wallet/[address]/layout.tsx`, `apps/wallet-ui/src/util/simplehash.ts`, `apps/wallet-ui/src/components/ChainCombobox.tsx`, `apps/wallet-ui/src/components/TransactionToastProvider.tsx`, `apps/wallet-ui/src/app/globals.css`, `apps/wallet-ui/src/components/ChainIcon.tsx`, `apps/wallet-ui/src/components/ui/dialog.tsx`, `apps/wallet-ui/src/components/ui/form.tsx`, `apps/wallet-ui/src/components/ui/command.tsx`, `apps/wallet-ui/src/components/NftModal.tsx`, `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2024-08-17 08:08:48 +00:00
gregfromstl 13764f1abb Connect UI Profile Image Improvements (#4126)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on cosmetic improvements to the Connect UI, introduces Blobbie component for unique avatars, and updates button styles and sizes.

### Detailed summary
- Increased `minWidth` of `TransactionButton` to 165px
- Updated `defaultButtonTitle` in `en.ts`
- Added `Blobbie` component for unique avatars
- Updated button styles and sizes in `ConnectButton.tsx`
- Added `shortenAddress` import in `wallet.ts`
- Updated styles in `Details.tsx` and `DetailsModal`

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

<!-- end pr-codex -->
2024-08-17 08:03:28 +00:00
iuwqyir 997c3945a7 export usageEventSchema and UsageEvent (#4147)
## Problem solved

Almost the same as https://github.com/thirdweb-dev/js/pull/4122. Made a new PR, because hadn't setup commit signing for graphite, so it failed to merge

Exports the Zod schema for usage events. This way it can be used for validate the message before sending it to the usage tracker queue.
Moved it under core because it is not tied to worker logic and can be used outside workers.
Also exported `UsageEvent` and `usageEventSchema` under node, because I think core is not exported

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to export `usageEventSchema` for Cloudflare worker and define `UsageEvent` schema for node.

### Detailed summary
- Exported `usageEventSchema` for Cloudflare worker and defined `UsageEvent` schema for node
- Added detailed schema for usage events with various optional fields

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

<!-- end pr-codex -->
2024-08-16 21:15:34 +00:00
MananTank 2bf2e1eec4 Update Engine page header (#4125)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces UI improvements to the Engine page layout, including new components, styling updates, and a modal for updating the Engine version.

### Detailed summary
- Added `CopyTextButton`, `Button`, and `Separator` components
- Updated styling and structure of the Engine page layout
- Implemented a modal for updating the Engine version
- Added new icons and components for UI enhancements

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

<!-- end pr-codex -->
2024-08-16 20:37:12 +00:00
MananTank 9b3725a56e Migrate Engine list UI to shadcn/tailwind (#4123)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances UI components in the dashboard app.

### Detailed summary
- Updated tooltip styling in `tooltip.tsx`.
- Refactored dropdown menu in `dropdown-menu.tsx`.
- Improved table UI with dropdown menus in `TWTable.tsx`.
- Implemented modals for editing and removing engine instances in `engine-instances-table.tsx`.

> The following files were skipped due to too many changes: `apps/dashboard/src/components/engine/engine-instances-table.tsx`

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

<!-- end pr-codex -->
2024-08-16 20:37:11 +00:00
MananTank beafe43f46 Move Engine import from Modal to /dashboard/engine/import (#4119)
## 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 Engine sidebar component and improve error handling in Engine pages.

### Detailed summary
- Refactored `EngineSidebar` component
- Improved error handling with `PageLoading` and `EngineErrorPage`
- Added import functionality for Engine instances

> The following files were skipped due to too many changes: `apps/dashboard/src/pages/dashboard/engine/import.tsx`

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

<!-- end pr-codex -->
2024-08-16 20:37:11 +00:00
Winston YeoandGitHub 214a74c36a chore: add authentication method to usage tracking (#4146) 2024-08-16 20:17:57 +00:00
MananTank d0e3c4defc Fix hydration error in markdown renderer + property and ts cleanup (#4141)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR refactors the `MarkdownRenderer` component in `published-contract`.

### Detailed summary
- Refactored heading components (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`) to remove explicit `any` type
- Refactored `a`, `code`, `p`, `table`, `th`, `td`, `ul`, `ol`, `li` components to use cleaned props without `node`

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

<!-- end pr-codex -->
2024-08-16 18:30:57 +00:00
MananTank 0cdf34ea98 Move Engine pages from client Router to Pages Router (#4113)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR upgrades Engine navigation links, adds new pages for Engine management, and refactors list styling.

### Detailed summary
- Updated Engine navigation links
- Added new pages for Engine management
- Refactored list styling in components

> The following files were skipped due to too many changes: `apps/dashboard/src/components/engine/engine-instances-table.tsx`, `apps/dashboard/src/pages/dashboard/engine/create.tsx`, `apps/dashboard/src/components/engine/EnginePageLayout.tsx`, `apps/dashboard/src/components/engine/engine-list.tsx`

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

<!-- end pr-codex -->
2024-08-16 15:45:43 +00:00
MananTank b1632a3df1 Add force-dynamic declaration for pages using cookies - fix sentry errors (#4140)
## 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 export `dynamic = "force-dynamic"` in three files due to the usage of `cookies()`.

### Detailed summary
- Added `export const dynamic = "force-dynamic"` in `page.tsx` under `dashboard/connect/pay`
- Added `export const dynamic = "force-dynamic"` in `page.tsx` under `dashboard/connect/ecosystem`
- Added `export const dynamic = "force-dynamic"` in `layout.tsx` under `dashboard/connect/ecosystem/[slug]/(active)`

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

<!-- end pr-codex -->
2024-08-16 15:34:13 +00:00
jnsdls d3f43a757b [Optimize] Add CI optimization step using Graphite action (#4115)
tl;dr: https://graphite.dev/docs/stacking-and-ci#reducing-ci-runs-for-github-actions

<!-- start pr-codex -->

---

## PR-Codex overview
This PR optimizes the CI workflow by adding a step to skip unnecessary processes based on a condition.

### Detailed summary
- Added `optimize_ci` job with skip condition
- Updated `build`, `lint`, `test`, `e2e`, and `size` jobs to depend on `optimize_ci` and skip if condition is met

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

<!-- end pr-codex -->
2024-08-15 21:22:24 +00:00
gregfromstl 825352476c fix: throw when no encryption key (#4120)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing security by throwing an error when no encryption key is provided for custom authentication.

### Detailed summary
- Added error throwing when encryption key is missing for custom JWT authentication
- Added error throwing when encryption key is missing for custom auth endpoint authentication

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

<!-- end pr-codex -->
2024-08-15 21:17:20 +00:00
arcoraven 8caa095a67 fix: update transaction drawer labels (#4118)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to enhance transaction details display in the dashboard by adding support for effective gas price calculation and showing additional information like account address and block number.

### Detailed summary
- Added `effectiveGasPrice` field to calculate transaction fee
- Displayed account address and linked to explorer
- Differentiated recipient address and contract address
- Showed transaction value in tokens and symbol
- Linked block number to explorer

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

<!-- end pr-codex -->
2024-08-15 19:22:51 +00:00
jnsdls 997e1197e9 analytics overhaul (#4107)
<!-- start pr-codex -->

## PR-Codex overview
This PR enhances blockchain analytics in the dashboard. It introduces support for analytics on specific chains and updates chart components for better visualization.

### Detailed summary
- Added `CHAINSAW_API_KEY` environment variable for contract analytics
- Updated chart components for better visualization
- Improved analytics support for specific chains
- Refactored chart styling for better UI/UX

> The following files were skipped due to too many changes: `apps/dashboard/src/contract-ui/tabs/overview/components/Analytics.tsx`, `apps/dashboard/src/contract-ui/tabs/analytics/page.tsx`

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

<!-- end pr-codex -->
2024-08-15 15:29:50 +00:00
Jonas DanielsandGitHub c9ac25fe3f Version Packages (#4114) 2024-08-15 07:38:54 -07:00
Wyatt MufsonandGitHub 79260c8cd0 Add new auth fields for redirect (#4099) 2024-08-15 07:30:59 -07:00
MananTank cf8badbc46 Move Network Configuration Form+Modal to shadcn/tailwind (#4105)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates various UI components for consistency and usability.

### Detailed summary
- Updated styling for `ClaimConditionsForm`, `CustomChainRenderer`, `ToolTipLabel`, `RpcInput`, `ChainIdInput`, `TooltipBox`, `ConfigureNetworkModal`, `ResetClaimEligibility`, `NetworkIdInput`, `IconUpload`, and `ConfigureNetworks`.
- Replaced `TooltipBox` with `ToolTipLabel` and `CircleHelpIcon`.
- Added `Input` from `@/components/ui/input`.
- Removed unnecessary imports.
- Replaced `Modal` components with `Dialog` and `DialogContent`.
- Used `toast` for error notifications.
- Updated button styling and functionality.

> The following files were skipped due to too many changes: `apps/dashboard/src/components/configure-networks/ConfigureNetworks.tsx`, `apps/dashboard/src/pages/[chain_id]/[...paths].tsx`, `apps/dashboard/src/components/configure-networks/ConfigureNetworkForm.tsx`

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

<!-- end pr-codex -->
2024-08-15 05:59:21 +00:00
MananTank 09e4d80b89 Show Top Progress Bar on navigation initiated using useRouter hook (#4097)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates Next.js navigation usage in the dashboard app to `useDashboardRouter` from `@/lib/DashboardRouter`.

### Detailed summary
- Replaces `useRouter` with `useDashboardRouter` in multiple files
- Adds `DashboardRouterTopProgressBar` component import in layout files
- Introduces `useDashboardRouter` import in various components
- Defines `createStore` function in `@/lib/reactive.ts`

> The following files were skipped due to too many changes: `apps/dashboard/src/@/lib/DashboardRouter.tsx`

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

<!-- end pr-codex -->
2024-08-15 05:48:22 +00:00
kien-ngo 1edd6c7e4c embed-setup.tsx to v5 (#4108)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to enhance the Contract Embed feature by integrating with `thirdwebClient` and `useV5DashboardChain`.

### Detailed summary
- Added `thirdwebClient` and `useV5DashboardChain` for contract setup
- Updated `contract` type to `ThirdwebContract`
- Refactored `buildIframeSrc` function to use `ThirdwebContract` properties

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

<!-- end pr-codex -->
2024-08-15 05:37:15 +00:00
kien-ngo 588f19639f [Dashboard] ContractMetadata settings to v5 (#4100)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the SettingsMetadata component in the contract UI to handle contract data more efficiently.

### Detailed summary
- Refactored the SettingsMetadata component to use ThirdwebContract type
- Updated schema imports and data handling
- Removed unnecessary dependencies and functions

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

<!-- end pr-codex -->
2024-08-15 05:26:34 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
f2c12781ef Version Packages (#4044)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-15 17:14:31 +12:00
kien-ngo 9d7d513d37 [Dashboard] PrimarySaleRecipient to v5 (#4098)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating the contract UI settings for primary sale recipient address handling.

### Detailed summary
- Updated handling of primary sale recipient address in contract settings UI
- Refactored usage of Thirdweb SDK functions
- Added error handling for invalid recipient address
- Improved transaction handling and form reset functionality

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

<!-- end pr-codex -->
2024-08-15 01:11:20 +00:00
kien-ngo 10de2712dd [Dashboard] Remove useClaimNFT; use erc1155/claimTo (#4102)
The component in this PR is meant for Edition Drop only

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update the `ClaimTab` component to support ERC1155 contracts and improve transaction handling.

### Detailed summary
- Renamed `ClaimTab` component to `ClaimTabERC1155`.
- Updated `ClaimTabERC1155` to work with ERC1155 contracts.
- Improved transaction handling in `ClaimTabERC1155`.
- Removed unnecessary imports and updated dependencies.

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

<!-- end pr-codex -->
2024-08-15 00:59:13 +00:00
gregfromstl 0ca1a79342 feat: cleanup ui (#4092)
- Reduced border radius on buttons
- Reduced connected button size
- Removed layout shift between connected and unconnected states
- Remove Rainbow from default wallets (replaced with Rabby)

https://github.com/user-attachments/assets/334f09d5-46d1-40e3-8b3a-5937771f277c

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on UI cosmetic improvements for wallet-related components.

### Detailed summary
- Added `io.rabby` wallet
- Adjusted padding and borderRadius in various components
- Modified button sizes and styles
- Updated wallet image sizes and text styles

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

<!-- end pr-codex -->
2024-08-15 00:44:18 +00:00
kien-ngo be344ac8c4 [Dashboard] Migration Royalties to v5 (#4051)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `SettingsRoyalties` component to use new contract and royalty information and improves error handling during royalty settings updates.

### Detailed summary
- Updated `SettingsRoyalties` component to use new contract and royalty information
- Improved error handling during royalty settings updates
- Replaced `thirdweb` imports with `sonner` and `thirdweb/react`
- Refactored default royalty info handling and transaction mutation logic

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

<!-- end pr-codex -->
2024-08-15 00:26:39 +00:00
754ad99cfd Feat: Transaction Simulator (#3715)
Co-authored-by: Kien Ngo <[email protected]>
Co-authored-by: Manan Tank <[email protected]>
2024-08-14 17:24:31 -07:00
MananTank d6e4898458 Add Pay success tracking (#4103)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances payment tracking in `MismatchButton` and `PayModalButton` components.

### Detailed summary
- Added tracking events for payment attempts and successes in `MismatchButton` and `PayModalButton`
- Utilized `useTrack` hook for event tracking
- Improved event tracking for different payment types and statuses

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

<!-- end pr-codex -->
2024-08-14 21:37:11 +00:00
YashandGitHub 191fb3a3a6 Modular contracts - rename extensions to modules (#3969) 2024-08-14 21:35:36 +00:00
MananTank 03a809a33f Add onPurchaseSuccess callback for Pay (#4101)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds `onPurchaseSuccess` callback to various components and hooks in the codebase, triggering when a user completes a purchase using thirdweb pay.

### Detailed summary
- Added `onPurchaseSuccess` callback to `PayEmbed`, `ConnectButton`, `TransactionButton`, and `useSendTransaction`
- Updated props in multiple components to include `onSuccess` callback parameter
- Added type definitions for `BuyWithCryptoStatus` and `BuyWithFiatStatus`
- Implemented logic to call `onSuccess` callback upon successful purchase completion

> The following files were skipped due to too many changes: `packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatFlow.tsx`

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

<!-- end pr-codex -->
2024-08-14 21:05:41 +00:00
kien-ngo 874ef7af4e [Dashboard] Hide ERC721 Claim button if viewing ERC1155 Drop (#4104)
For Edition Drop we have a dedicated ClaimTabERC1155 inside each Edition's page
2024-08-14 20:10:11 +00:00
joaquim-verges ef9c7df220 feat(native): add passkey support in connect UI (#4077)
## Problem solved

Added passkey UI in connect for react native

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces passkey login support in React Native.

### Detailed summary
- Updated colors in the design system
- Added passkey domain logic
- Improved wallet loading components
- Added passkey authentication screen
- Updated wallet icons and images

> The following files were skipped due to too many changes: `packages/thirdweb/src/react/native/ui/connect/InAppWalletUI.tsx`

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

<!-- end pr-codex -->
2024-08-14 19:44:37 +00:00
joaquim-verges a640a1e147 feat(inapp): add passkey authentication for native clients (#4022)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces support for `passkeyDomain` in in-app wallets, updates error handling in signup screens, and improves storage handling.

### Detailed summary
- Added `passkeyDomain` support in in-app wallets
- Updated error handling in signup screens
- Improved storage handling for in-app wallets

> The following files were skipped due to too many changes: `packages/thirdweb/src/wallets/in-app/web/utils/Storage/LocalStorage.ts`, `packages/thirdweb/src/utils/bytecode/cbor-decode.ts`, `packages/thirdweb/package.json`, `packages/thirdweb/src/wallets/wallet-connect/receiver/session-store.test.ts`, `pnpm-lock.yaml`, `packages/thirdweb/src/wallets/in-app/native/native-connector.ts`, `packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts`, `packages/thirdweb/src/wallets/in-app/core/authentication/passkeys.ts`, `packages/thirdweb/src/wallets/in-app/native/auth/passkeys.ts`, `packages/thirdweb/src/wallets/in-app/web/lib/auth/passkeys.ts`

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

<!-- end pr-codex -->
2024-08-14 19:44:35 +00:00
kien-ngo 52678e02a8 [Dashboard] use MediaRenderer for contract's avatar (#4095)
- [x] Contract metadata header now uses <MediaRenderer /> instead of <Image/>
This way is works with videos and it gets rid of that weird border-radius bug (see image)

<img width="324" alt="image" src="https://github.com/user-attachments/assets/a57f95c1-eb45-4e6f-8121-0d10358c7c6b">

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor the `MetadataHeader` component in the custom-contract folder by replacing the `resolveScheme` function with `MediaRenderer` for image rendering.

### Detailed summary
- Replaced `resolveScheme` with `MediaRenderer` for image rendering
- Updated image rendering logic with additional props
- Removed unused imports and variables

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

<!-- end pr-codex -->
2024-08-14 14:04:07 +00:00
MananTank b9ed7535a4 Fix Connection with Safe using WalletConnect Link (#4091)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing the connection with Safe using WalletConnect link.

### Detailed summary
- Added support for Safe supported chains in WalletConnect
- Updated chain selection logic based on walletId
- Included specific chains for Safe connection

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

<!-- end pr-codex -->
2024-08-14 11:39:29 +00:00
Joaquim VergesandGitHub 70d4a5a3db fix(dashboard): fix wrong 'version' prop import (#4096) 2024-08-14 21:51:56 +12:00
jnsdls ea28704a04 [Refactor] Remove unused functions and improve faucet validation (#4093) 2024-08-14 00:50:22 +00:00
jnsdls 814024ae24 [Chore] Remove unused 'amount' field from FaucetButton component and claim route (#4090)
## 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 the testnet faucet claim route to fix issues related to the amount handling.

### Detailed summary
- Removed `amount` from request payload
- Updated amount handling in the faucet claim route to always transfer 0.01
- Improved error handling and response consistency

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

<!-- end pr-codex -->
2024-08-13 23:11:24 +00:00
joaquim-verges 9d3fa73da7 feat(dashboard): bring zksync signer back to direct deploys (#4089)
## Problem solved

for direct deploys, right now go back to the zksync ethers signer wrapper - will implement it properly inside account v5
2024-08-13 22:14:05 +00:00
jnsdls fea4a4afe8 [Fix] Remove unused loginRequired prop from MismatchButton component (#4088)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the `MismatchButton.tsx` component in the dashboard app.

### Detailed summary
- Updated `CustomConnectWallet` component with new props
- Removed `loginRequired={false}` prop from `CustomConnectWallet` usage

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

<!-- end pr-codex -->
2024-08-13 22:00:38 +00:00
Phillip HoandGitHub 2e16f9ecef chore: Add queue metrics to dashboard (#4075) 2024-08-13 21:59:34 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1032bc7cc4 chore(deps): bump @stripe/react-stripe-js from 2.7.2 to 2.7.3 (#4063)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-13 13:02:34 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7e6d1decfc chore(deps-dev): bump react-native from 0.74.2 to 0.74.5 (#4061)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-13 12:48:51 -07:00
jnsdls 73b2c512ce [Chore] Add temporary redirect for /gas to /explore and remove GasPage component (#4087) 2024-08-13 18:14:18 +00:00
MananTank 13cda91d10 Remove sign-in requirement for TransactionButton (#4086)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `MismatchButton` component in the dashboard by adding the `loginRequired` prop to `CustomConnectWallet`.

### Detailed summary
- Added `loginRequired={false}` prop to `CustomConnectWallet` in `MismatchButton.tsx`

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

<!-- end pr-codex -->
2024-08-13 17:58:37 +00:00
MananTank e91421a252 Fix isLoggedin auth action (#4085)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the authentication logic in `auth-actions.ts` to check for a matching creatorWalletAddress in the JSON data.

### Detailed summary
- Updated condition to check for `creatorWalletAddress` in JSON data instead of `address`.

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

<!-- end pr-codex -->
2024-08-13 17:45:31 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
74c6bca538 chore(deps): bump @hookform/resolvers from 3.6.0 to 3.9.0 (#4064)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-13 10:45:02 -07:00
MananTank 23beb9d263 Add validation for NFT claim amount to be integer (#4080)
## Problem solved

Short description of the bug fixed or feature added
2024-08-13 17:21:15 +00:00
jnsdls 69657377f4 [Fix] Lower faucet amount to 0.01 (#4084) 2024-08-13 16:48:33 +00:00
MananTank 9db4b293a5 dynamically import onboarding components - reduce Stripe errors and bundle size (#4081)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the codebase by implementing lazy loading for onboarding components to improve performance.

### Detailed summary
- Implemented lazy loading for onboarding components
- Added `LazyOnboardingBilling` component for billing
- Updated multiple files to use lazy loading for onboarding components

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

<!-- end pr-codex -->
2024-08-13 16:37:26 +00:00
MananTank 867d2dae8a Fix useLayoutEffect warnings (#4079)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the usage of `useLayoutEffect` to `useIsomorphicLayoutEffect` for consistent server-side rendering compatibility.

### Detailed summary
- Replaces `useLayoutEffect` with `useIsomorphicLayoutEffect` in multiple files
- Ensures consistent rendering behavior across client and server side

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

<!-- end pr-codex -->
2024-08-13 16:26:34 +00:00
MananTank cb27dafca7 Do not show "No Funds" Modal when switch chain is required (#4083)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to handle a specific condition `networksMismatch` before executing the button's `onClick` functionality.

### Detailed summary
- Added a condition to check for `networksMismatch` before executing the button's `onClick` functionality.
- Stored the event reference in `eventRef.current` when `networksMismatch` is true.

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

<!-- end pr-codex -->
2024-08-13 16:21:33 +00:00
kien-ngo 6b7eedeecb [Dash] various small updates and fixes (#4076)
- [x] Fix clipboard value for the [copy button] inside /explorer page
- [x] Better handling for some edge cases where the contract's compilation target is a very long string
- [x] Instead of displaying "Custom" for non-thirdweb contracts, display the contract type based on the compilation target

<hr />

Before:
<img width="1241" alt="image" src="https://github.com/user-attachments/assets/397cc15f-5941-4a5c-ae11-cae5a7a7fbde">

After:
<img width="830" alt="image" src="https://github.com/user-attachments/assets/2a353e9c-04c8-4d6d-b15f-c26b7f423213">

<hr />

Before:
<img width="665" alt="image" src="https://github.com/user-attachments/assets/93465f35-d3c4-466c-b9cd-ab905c4fc6f1">

After:
<img width="667" alt="image" src="https://github.com/user-attachments/assets/24c59796-913d-42a4-95a2-2a63e6e726c3">

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to enhance how contract metadata is displayed in the dashboard.

### Detailed summary
- Improved handling of long contract names from the thirdweb endpoint
- Updated the display of contract type in the contract table
- Refactored the usage of contract metadata in different components

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

<!-- end pr-codex -->
2024-08-13 07:16:13 +00:00
MananTank 53da850397 Fix Various "couldn't be rendered statically" errors in Sentry (#4067)
TLDR; is - In the Chain landing page, we don't really care about filtering the popular contracts - so I removed so we can statically render the chain page,

Added force-dynamic for /popular and /trending pages where we allow filtering by search params

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to implement dynamic rendering using searchParams and improve sorting functionality in the dashboard pages.

### Detailed summary
- Implemented dynamic rendering using `searchParams`
- Updated sorting functionality in trending pages
- Refactored `searchParams` in components
- Removed unnecessary imports

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

<!-- end pr-codex -->
2024-08-13 04:11:48 +00:00
kien-ngo de1bd6e903 [Dashboard] Fix contract query key (#4073)
## 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 `useDashboardContractMetadata` hook to use a more descriptive query key and improve code readability.

### Detailed summary
- Refactored the query key to include contract metadata details
- Improved code readability by restructuring the Promise.all block
- Updated the return object format for consistency

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

<!-- end pr-codex -->
2024-08-13 03:56:15 +00:00
ff7f61c038 Dustin/treasure chain page update (#4072)
Co-authored-by: Jonas Daniels <[email protected]>
2024-08-12 20:26:37 -07:00
FirekeeperandGitHub 2408a7f9ed Docs - .NET 1.4.0 // Unity 5.0.0-beta.3 (#4071) 2024-08-12 20:25:49 -07:00
98bb37c1a1 chore(deps-dev): bump @swc-node/register from 1.10.0 to 1.10.9 (#4060)
Co-authored-by: dependabot <[email protected]>
2024-08-12 20:24:21 -07:00
gregfromstl 3c32b99b3a refactor(thirdweb): update siwe interface (#4045)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the SIWE interface to include a new `wallet` authentication option and refactors related methods for consistency.

### Detailed summary
- Added `wallet` as a new authentication option
- Updated interface methods to support `wallet` authentication
- Refactored SIWE-related functions for consistency

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

<!-- end pr-codex -->
2024-08-13 02:36:29 +00:00
kien-ngo e5a046ecdd [Dashboard] Fix missing contract metadata (#4070)
- [x] Remove duplicated formatCompilerMetadata method
- [x] Add getCompilerMetadata method to the SDK
- [x] Add a custom hook for Dashboard for fetching basic contract metadata (useDashboardContractMetadata)

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add a contract utility method `getCompilerMetadata()` and update contract metadata handling in the dashboard.

### Detailed summary
- Added `getCompilerMetadata()` utility method to fetch and format compiler metadata
- Updated contract metadata handling in the dashboard components
- Added `useDashboardContractMetadata` hook for fetching contract metadata in the dashboard

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

<!-- end pr-codex -->
2024-08-13 02:07:44 +00:00
kien-ngo 237f5faf42 [Dashboard] Migrate PlatformFees to v5 (#4050)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to migrate zod schemas from SDK v4 to a new location for form validations in the dashboard.

### Detailed summary
- Migrated zod schemas for PlatformFees and Royalties from SDK v4 to `schemas.ts`
- Updated `page.tsx` and `platform-fees.tsx` to use the new zod schemas and adapt to the changes in the SDK
- Refactored form validations and platform fee settings handling in `platform-fees.tsx`

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

<!-- end pr-codex -->
2024-08-13 01:50:49 +00:00
gregandGitHub ad2588b5de chore: fix deploy-split test (#4068) 2024-08-12 18:49:52 -07:00
kien-ngo db8695d0e8 [Dashboard]: Migrate major NFT features to v5 (#3998)
# High level changes
- `mint-form.tsx` is "over-abstracted" (not sure if that's the right word) - it's being used for too many different NFT features, making the code hard-to-migrate.
- solution: breaking it down to different component which makes it much easier to remove v4 code

# to test
- [x] UpdateMetadataForm
  - [x] Drop contracts
  - [x] Collection contracts
- [x] SharedMetadataForm
- [x] NFTMintForm
- [x] LazyMintForm

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces NFT extensions for ERC721 and ERC1155 contracts, adds a new `updateTokenURI` function, and updates components for NFT minting and metadata management.

### Detailed summary
- Added NFT extension with `updateTokenURI` function for ERC721 and ERC1155 contracts
- Updated components for NFT minting and metadata management

> The following files were skipped due to too many changes: `apps/dashboard/src/contract-ui/tabs/permissions/components/contract-permission.tsx`, `packages/thirdweb/src/extensions/erc721/write/updateTokenURI.ts`, `packages/thirdweb/src/extensions/erc1155/write/updateTokenURI.ts`, `apps/dashboard/src/contract-ui/tabs/nfts/components/shared-metadata-form.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/lazy-mint-form.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/update-metadata-form.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/mint-form.tsx`

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

<!-- end pr-codex -->
2024-08-13 00:27:22 +00:00
MananTank 2d4a5c3384 Update Deploy modal (#4057)
## 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 custom contract deployment process by introducing a new `DeployStatusModal` component.

### Detailed summary
- Added `DeployStatusModal` component for custom contract deployment status tracking
- Refactored `useCustomContractDeployMutation` to use `DeployStatusModal`
- Removed `DeployModalProvider` component and related context logic

> The following files were skipped due to too many changes: `apps/dashboard/src/components/contract-components/contract-deploy-form/deploy-context-modal.tsx`

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

<!-- end pr-codex -->
2024-08-13 00:06:43 +00:00
Joaquim VergesandGitHub dbf3873aaf feat(dashboard): add handle_all_urls relation to assetlinks.json for Android app link verification (#4069) 2024-08-13 12:06:09 +12:00
MananTank b15aa4cdff Add Deploy button link on the Contract Card (#4066)
## 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 deploy button with a rocket icon to the ContractCard component in the Explore section.

### Detailed summary
- Added `RocketIcon` and deploy button to `ContractCard`
- Adjusted styling of `ContractCard` component for better layout
- Linked deploy button to deployment page with `Link`

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

<!-- end pr-codex -->
2024-08-12 23:31:51 +00:00
gregfromstl 689b8dc160 test: fix split tests (#4065)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates test descriptions in the `split` extension to run only if a secret key is present, enhancing security.

### Detailed summary
- Updated test descriptions to run only if a secret key is present
- Renamed test suites for `getAllRecipientsAddresses`, `getAllRecipientsPercentages`, and `getRecipientSplitPercentage`

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

<!-- end pr-codex -->
2024-08-12 22:40:18 +00:00
gregfromstl 218355bf95 docs: fix multi auth jsdocs (#4056)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates documentation comments in `useProfiles.ts` and `profiles.ts` for clarity.

### Detailed summary
- Updated description comments in `profiles.ts` for clarity and consistency.
- Added `@wallet` tag to indicate in-app wallet-specific methods.
- Removed redundant descriptions to improve readability.

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

<!-- end pr-codex -->
2024-08-12 22:01:39 +00:00
9be0a2fe85 Update Startup program page layout (#4058)
Co-authored-by: Ginessi <[email protected]>
Co-authored-by: Manan Tank <[email protected]>
2024-08-12 14:49:03 -07:00
gregfromstl 22b335b1f2 chore: delete solutions page (#4059)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to restructure the directory layout by deleting specific files and updating URL redirects related to gaming solutions.

### Detailed summary
- Deleted `page.mdx`, `layout.tsx`, and `sidebar.tsx` in `solutions` directory
- Removed images `unreal.png` and `preview.png` in `gaming/assets`
- Deleted `page.mdx` in `gaming/overview` directory
- Updated URL redirects for `/gaming`, `/solutions`, and `/solutions/gaming`
- Redirected `/gaming` to `/`
- Redirected `/solutions` to `/`
- Redirected `/solutions/gaming` to `/`
- Updated redirect from `/signature-minting` to `/contracts/design-docs/signature-mint`
- Updated reference from `/references/typescript/v5/embeddedWallet` to `/references/typescript/v5/inAppWallet`

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

<!-- end pr-codex -->
2024-08-12 20:57:30 +00:00
MananTank dfbaeb1c3d [dashboard] Fix chains fetch failture in localhost (#4053)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to simplify the API request in the `useApi.ts` file by removing unnecessary headers and changing the request method to GET.

### Detailed summary
- Removed unnecessary headers from the API request
- Changed the request method to GET

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

<!-- end pr-codex -->
2024-08-12 20:46:03 +00:00
MananTank dc99b88cac Fix Chainlist can't be rendered statically error in sentry (#4054)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a `dynamic` export to force the page as dynamic for determining table/grid view based on viewport.

### Detailed summary
- Added `dynamic` export to force page as dynamic
- Updated comments to explain the purpose of the `dynamic` export

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

<!-- end pr-codex -->
2024-08-12 19:13:27 +00:00
Phillip HoandGitHub 66906dbef3 chore: Engine v2-beta in docs (#4049) 2024-08-12 10:04:29 -07:00
gregfromstl 7a68e3b8ed fix(react): switch chains for SIWE (#4047)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating authentication logic to switch to the proper chain before signing payloads.

### Detailed summary
- Updated `getLoginPayload` to include `chainId` parameter
- Added logic to switch to the correct chain before signing payloads
- Updated initialization of `thirdwebAuth` to handle chain switching
- Imported `getCachedChain` function in `useSiweAuth.ts`

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

<!-- end pr-codex -->
2024-08-12 05:34:36 +00:00
gregfromstl cb4b6fde85 fix(playground): do not attempt to decode nonexistent jwt (#4046)
## 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 how the authentication result is retrieved in the `GatedContentPreview` function.

### Detailed summary
- Refactored the authentication result retrieval logic to handle `jwt?.value` more efficiently.
- Updated the conditional check for `authResult` to account for the new retrieval logic.

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

<!-- end pr-codex -->
2024-08-12 05:01:02 +00:00
kien-ngo 141fd53759 [Dashboard/SDK] Add more Split extensions and remove Split v4 code from Dashboard (#4033)
<!-- start pr-codex -->

## PR-Codex overview
This PR adds more functionality to the `Split` contract extensions, including deploying the `Split` contract, reading recipient addresses and percentages, and testing these features.

### Detailed summary
- Added more Split contract extensions
- Updated `deploys.ts` with new exports for deploying Split contract
- Updated `split.ts` with new exports for reading recipient addresses and percentages
- Modified `page.tsx` to display contract balance using `getContract`
- Added new files for reading recipient addresses and percentages
- Updated `account-balance.tsx` to use `getContract` and `useSplitBalances`
- Added tests for getting all recipient addresses and percentages
- Added functionality to get recipient split percentage and corresponding tests

> The following files were skipped due to too many changes: `packages/thirdweb/src/extensions/split/__generated__/Split/read/totalShares.ts`, `apps/dashboard/src/contract-ui/tabs/split/components/distribute-button.tsx`, `packages/thirdweb/test/src/test-contracts.ts`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useSplit.ts`, `packages/thirdweb/src/extensions/prebuilts/deploy-split.ts`, `packages/thirdweb/src/extensions/prebuilts/__generated__/Split/write/initialize.ts`, `apps/dashboard/src/contract-ui/tabs/split/page.tsx`

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

<!-- end pr-codex -->
2024-08-11 22:17:38 +00:00
joaquim-verges 478d843975 feat(dashboard): add apple-app-site-association for iOS app link verification (#4043)
## Problem solved

for passkeys on ios

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `apple-app-site-association` and `assetlinks.json` files in the dashboard public directory to include specific app and web credentials information.

### Detailed summary
- Added `webcredentials` for specific app in `apple-app-site-association`
- Updated `sha256_cert_fingerprints` in `assetlinks.json`

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

<!-- end pr-codex -->
2024-08-11 22:11:13 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
386fe1ce68 Version Packages (#4042)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-11 12:38:36 -07:00
jnsdls 539bdab403 [Refactor] Remove unused Bun availability check in CLI bin file (#4040)
FIXES: https://github.com/thirdweb-dev/js/issues/3991
2024-08-11 19:13:39 +00:00
jnsdls 0e3cb1338b [Hotfix] Remove async modifier from TrendingContractSection in trending-table.tsx (#4041)
FIXES: DASH-128
2024-08-11 19:02:09 +00:00
34cdd4ce95 feat(dashboard): add assetlinks.json for Android app link verification (#4039)
Co-authored-by: joaquim-verges <[email protected]>
2024-08-11 11:18:22 -07:00
Jonas DanielsandGitHub c31617436c [Chore] Remove unused 'faucet' service from chain logic (#4038) 2024-08-11 01:31:34 -07:00
Jonas DanielsandGitHub 40fbe9abb9 [Feature] Refactor chain services handling in API calls (#4037) 2024-08-11 00:08:15 -07:00
Nick PapadakisandGitHub 74e005214d Update page.mdx (#4036)
Signed-off-by: Nick Papadakis <[email protected]>
2024-08-10 20:54:45 -07:00
LubovandGitHub 1c48c20785 Update page.mdx (#4035)
Signed-off-by: Lubov <[email protected]>
2024-08-10 21:28:38 +12:00
gregfromstl 7befc61b70 Unreal 1.1 Docs (#3917)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to enhance the Thirdweb Unreal SDK documentation by adding detailed information about Blueprint system, signers, runtime settings, and common enums.

### Detailed summary
- Added documentation for Blueprint Overview, ThirdwebSigner, ThirdwebRuntimeSettings, and ThirdwebCommon.
- Updated sidebar links and icons.
- Updated tsconfig.json with consistent casing.
- Added Getting Started guide with integration steps and images.

> The following files were skipped due to too many changes: `apps/portal/src/app/unreal/thirdweb-wallet-handle/page.mdx`, `apps/portal/src/app/unreal/thirdweb-subsystem/page.mdx`, `apps/portal/src/app/unreal/thirdweb/page.mdx`, `apps/portal/src/app/unreal/page.mdx`, `apps/portal/src/app/unreal/thirdweb-function-library/page.mdx`

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

<!-- end pr-codex -->
2024-08-10 03:42:49 +00:00
MananTank 6b4503a91c Deploy form layout updates, Fix faucet claim condition (#4030)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the logic for checking if a user can claim in the testnet faucet, adds a new React switch component, refactors common form fieldsets, and improves UI components.

### Detailed summary
- Updated `canClaim` logic in testnet faucet route
- Added `@radix-ui/react-switch` component
- Refactored common form fieldsets into `Fieldset` component
- Improved UI components and form field layouts

> The following files were skipped due to too many changes: `apps/dashboard/src/components/contract-components/contract-deploy-form/platform-fee-fieldset.tsx`, `pnpm-lock.yaml`, `apps/dashboard/src/components/contract-components/contract-deploy-form/trusted-forwarders-fieldset.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/split-fieldset.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx`

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

<!-- end pr-codex -->
2024-08-09 22:04:48 +00:00
507 changed files with 21040 additions and 12041 deletions
+22 -1
View File
@@ -19,7 +19,21 @@ env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
jobs:
optimize_ci:
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.check_skip.outputs.skip }}
steps:
- name: Optimize CI
id: check_skip
uses: withgraphite/graphite-ci-action@main
with:
graphite_token: ${{ secrets.GRAPHITE_OMTIMIZE_TOKEN }}
build:
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
runs-on: ubuntu-latest
name: Build Packages
steps:
@@ -33,6 +47,8 @@ jobs:
run: pnpm build:packages
lint:
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
timeout-minutes: 15
name: Lint Packages
runs-on: ubuntu-latest
@@ -51,6 +67,8 @@ jobs:
- run: pnpm lint
test:
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
timeout-minutes: 15
name: Unit Tests
runs-on: ubuntu-latest
@@ -77,6 +95,8 @@ jobs:
verbose: true
e2e:
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
timeout-minutes: 15
name: E2E Tests
runs-on: ubuntu-latest
@@ -144,8 +164,9 @@ jobs:
fi
size:
needs: optimize_ci
# only run on pull requests
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && needs.optimize_ci.outputs.skip == 'false'
timeout-minutes: 15
name: "Size"
runs-on: ubuntu-latest
+19
View File
@@ -27,7 +27,20 @@ env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
jobs:
optimize_ci:
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.check_skip.outputs.skip }}
steps:
- name: Optimize CI
id: check_skip
uses: withgraphite/graphite-ci-action@main
with:
graphite_token: ${{ secrets.GRAPHITE_OMTIMIZE_TOKEN }}
build:
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
runs-on: ubuntu-latest
name: Build Packages
steps:
@@ -41,6 +54,8 @@ jobs:
run: pnpm build:legacy_packages
lint:
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
timeout-minutes: 15
name: Lint Packages
runs-on: ubuntu-latest
@@ -54,6 +69,8 @@ jobs:
- run: pnpm lint:legacy
test:
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
timeout-minutes: 15
name: Unit Tests
runs-on: ubuntu-latest
@@ -74,6 +91,8 @@ jobs:
verbose: true
e2e:
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
timeout-minutes: 15
name: E2E Tests
runs-on: ubuntu-latest
+3
View File
@@ -18,5 +18,8 @@
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
+3
View File
@@ -40,6 +40,9 @@ NEXT_PUBLIC_STRIPE_KEY=
NEXT_PUBLIC_STRIPE_PAYMENT_METHOD_CFG_ID=
# Needed for contract analytics / blockchain data information
CHAINSAW_API_KEY=
#
# Private (server)
#
+6
View File
@@ -80,6 +80,12 @@ module.exports = {
message:
"Import from `@chakra-ui/react` instead of `@chakra-ui/menu`.",
},
{
name: "next/navigation",
importNames: ["useRouter"],
message:
'Use `import { useDashboardRouter } from "@/lib/DashboardRouter";` instead',
},
],
},
],
+4 -3
View File
@@ -27,7 +27,7 @@
"@coinbase/onchainkit": "^0.14.2",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5",
"@hookform/resolvers": "^3.6.0",
"@hookform/resolvers": "^3.9.0",
"@n8tb1t/use-scroll-position": "^2.0.3",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.0.3",
@@ -40,12 +40,13 @@
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "1.1.2",
"@react-icons/all-files": "^4.1.0",
"@sentry/nextjs": "^8.8.0",
"@stripe/react-stripe-js": "^2.7.2",
"@stripe/react-stripe-js": "^2.7.3",
"@stripe/stripe-js": "^3.5.0",
"@tanstack/query-async-storage-persister": "^4.36.1",
"@tanstack/react-query": "^4.36.1",
@@ -118,7 +119,7 @@
"@chakra-ui/cli": "^2.4.1",
"@next/bundle-analyzer": "^14.2.4",
"@next/eslint-plugin-next": "^14.2.4",
"@playwright/test": "^1.44.1",
"@playwright/test": "1.44.1",
"@types/color": "^3.0.6",
"@types/node": "20.14.9",
"@types/papaparse": "^5.3.14",
@@ -0,0 +1,7 @@
{
"applinks": {},
"webcredentials": {
"apps": ["XYHGSFAMHX.com.thirdweb.demo"]
},
"appclips": {}
}
@@ -0,0 +1,15 @@
[
{
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.thirdweb.demo",
"sha256_cert_fingerprints": [
"40:EB:0D:96:57:F3:D8:1F:BA:87:B8:E4:26:E0:3A:DB:C8:35:96:A8:A2:B2:55:F0:B1:64:F1:39:F8:6F:7E:EB"
]
}
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

+7
View File
@@ -282,6 +282,13 @@ function redirects() {
"https://support.thirdweb.com/troubleshooting-errors/7Y1BqKNvtLdBv5fZkRZZB3/issue-linking-device-on-the-authorization-page-via-thirdweb-cli/cn9LRA3ax7XCP6uxwRYdvx",
permanent: false,
},
// temporary redirect gas -> explore page
{
source: "/gas",
destination: "/explore",
permanent: false,
},
];
}
@@ -0,0 +1,109 @@
"use client";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { useMemo } from "react";
export type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
// metadata
title: string;
description?: string;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
showLegend?: boolean;
// chart className
chartClassName?: string;
};
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
props: ThirdwebAreaChartProps<TConfig>,
) {
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
return (
<Card>
<CardHeader>
<CardTitle>{props.title}</CardTitle>
{props.description && (
<CardDescription>{props.description}</CardDescription>
)}
</CardHeader>
<CardContent>
<ChartContainer config={props.config} className={props.chartClassName}>
<AreaChart
accessibilityLayer
data={props.data}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => new Date(value).toLocaleDateString()}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<defs>
{configKeys.map((key) => (
<linearGradient
key={key}
id={`fill_${key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor={`var(--color-${key})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${key})`}
stopOpacity={0.1}
/>
</linearGradient>
))}
</defs>
{configKeys.map((key) => (
<Area
key={key}
dataKey={key}
type="natural"
fill={`url(#fill_${key})`}
fillOpacity={0.4}
stroke={`var(--color-${key})`}
stackId="a"
/>
))}
{props.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
)}
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}
@@ -0,0 +1,91 @@
"use client";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { useMemo } from "react";
export type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
// metadata
title: string;
description?: string;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
showLegend?: boolean;
variant?: "stacked" | "grouped";
// chart className
chartClassName?: string;
};
export function ThirdwebBarChart<TConfig extends ChartConfig>(
props: ThirdwebBarChartProps<TConfig>,
) {
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
// if there are more than 4 keys then we should stack them by default
const variant =
props.variant || configKeys.length > 4 ? "stacked" : "grouped";
return (
<Card>
<CardHeader>
<CardTitle>{props.title}</CardTitle>
{props.description && (
<CardDescription>{props.description}</CardDescription>
)}
</CardHeader>
<CardContent>
<ChartContainer config={props.config} className={props.chartClassName}>
<BarChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={10}
tickFormatter={(value) => new Date(value).toLocaleDateString()}
/>
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
{props.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
)}
{configKeys.map((key, idx) => (
<Bar
key={key}
dataKey={key}
// if stacked then they should all be the same stackId
// if grouped then they should all be unique stackId (so the key works great)
stackId={variant === "stacked" ? "a" : key}
fill={`var(--color-${key})`}
// if stacked then we need to figure out the radius based on the index in the array
// if grouped then we can just use the same radius for all
radius={
variant === "stacked"
? idx === 0
? [0, 0, 4, 4]
: idx === configKeys.length - 1
? [4, 4, 0, 0]
: [0, 0, 0, 0]
: [4, 4, 4, 4]
}
/>
))}
</BarChart>
</ChartContainer>
</CardContent>
</Card>
);
}
@@ -0,0 +1,47 @@
import { Highlight } from "prism-react-renderer";
import React from "react";
import { cn } from "../../lib/utils";
import { CopyButton } from "./CopyButton";
import { Card } from "./card";
interface CodeBlockProps {
code: string;
language?: string;
className?: string;
canCopy?: boolean;
}
export const CodeBlock = ({
code,
language = "sh",
canCopy = true,
}: CodeBlockProps) => {
return (
<Highlight code={code} language={language}>
{({ className, tokens, getLineProps, getTokenProps }) => (
<Card className={className}>
{canCopy && (
<CopyButton text={code} className="float-right p-2 m-2" />
)}
<div
className={cn(
"block font-mono text-sm whitespace-pre-wrap p-4 overflow-auto",
className,
)}
>
{tokens.map((line, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: index IS the key here
<div {...getLineProps({ line, key: i })} key={i}>
{line.map((token, key) => (
// biome-ignore lint/suspicious/noArrayIndexKey: index IS the key here
<span {...getTokenProps({ token, key })} key={key} />
))}
</div>
))}
</div>
</Card>
)}
</Highlight>
);
};
@@ -1,7 +1,8 @@
"use client";
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
import type React from "react";
import { useLayoutEffect, useRef, useState } from "react";
import { useRef, useState } from "react";
export function DynamicHeight(props: {
children: React.ReactNode;
@@ -37,7 +38,7 @@ export function useHeightObserver() {
const elementRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<number | undefined>();
useLayoutEffect(() => {
useIsomorphicLayoutEffect(() => {
const element = elementRef.current;
if (!element) {
return;
@@ -0,0 +1,25 @@
.unorderedList,
.orderedList {
@apply pl-5 leading-relaxed list-none text-secondary-foreground;
}
.unorderedList > li {
@apply pb-1 pl-2 list-disc;
}
.orderedList > li {
@apply pb-1 pl-2 list-decimal;
}
.unorderedList > li::marker {
@apply text-muted-foreground !m-0 text-left;
}
.orderedList > li::marker {
@apply text-muted-foreground text-left;
}
.unorderedList > li:hover::marker,
.orderedList > li:hover::marker {
@apply text-foreground;
}
@@ -0,0 +1,9 @@
import styles from "./List.module.css";
export function UnorderedList(props: { children?: React.ReactNode }) {
return <ul className={styles.unorderedList}>{props.children}</ul>;
}
export function OrderedList(props: { children?: React.ReactNode }) {
return <ul className={styles.orderedList}>{props.children}</ul>;
}
@@ -1,7 +1,8 @@
"use client";
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
import { cn } from "@/lib/utils";
import { useLayoutEffect, useRef } from "react";
import { useRef } from "react";
import styles from "./ScrollShadow.module.css";
export function ScrollShadow(props: {
@@ -17,7 +18,7 @@ export function ScrollShadow(props: {
const shadowRightEl = useRef<HTMLDivElement>(null);
const wrapperEl = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
useIsomorphicLayoutEffect(() => {
const content = scrollableEl.current;
const shadowTop = shadowTopEl.current;
const shadowBottom = shadowBottomEl.current;
+1 -1
View File
@@ -10,7 +10,7 @@ const alertVariants = cva(
variant: {
default: "bg-background text-foreground",
destructive:
"bg-destructive/10 border-destructive/50 dark:border-destructive [&>svg]:text-destructive",
"bg-destructive/30 border-destructive/50 dark:border-destructive [&>svg]:text-destructive",
info: "bg-primary/10 border-primary/50 dark:border-primary [&>svg]:text-primary",
warning:
"bg-warning/10 border-warning/50 dark:border-warning [&>svg]:text-warning",
+1 -1
View File
@@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
+1 -1
View File
@@ -283,7 +283,7 @@ const ChartLegendContent = React.forwardRef<
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
"flex items-center justify-center gap-4 overflow-x-auto",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
@@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
@@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
@@ -0,0 +1,29 @@
"use client";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as React from "react";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-foreground data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
+5 -2
View File
@@ -1,6 +1,9 @@
"use client";
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { useCallback, useLayoutEffect, useRef, useState } from "react";
import { useCallback, useRef, useState } from "react";
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
import { Button } from "./button";
@@ -126,7 +129,7 @@ function useUnderline<El extends HTMLElement>() {
setActiveTabEl(el);
}, []);
useLayoutEffect(() => {
useIsomorphicLayoutEffect(() => {
if (activeTabEl && containerRef.current && lineRef.current) {
const containerRect = containerRef.current.getBoundingClientRect();
const lineEl = lineRef.current;
@@ -31,7 +31,7 @@ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
export function ToolTipLabel(props: {
children: React.ReactNode;
label: string | undefined;
label: React.ReactNode;
rightIcon?: React.ReactNode;
leftIcon?: React.ReactNode;
}) {
@@ -47,7 +47,7 @@ export function ToolTipLabel(props: {
</TooltipTrigger>
<TooltipContent
sideOffset={10}
className="max-w-[300px] leading-relaxed"
className="max-w-[400px] leading-relaxed whitespace-normal"
>
<div className="p-2 text-sm flex items-center gap-1.5">
{props.leftIcon}
@@ -0,0 +1,139 @@
"use client";
// eslint-disable-next-line no-restricted-imports
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import {
Suspense,
useEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
} from "react";
import { createStore } from "./reactive";
// Using useDashboardRouter instead of useRouter gives us a nice progress bar on top of the page when navigating using router.push or router.replace
// using a store instead of context to avoid triggering re-renders on root component
export const LoadingRouteHref = createStore<string | undefined>(undefined);
export function useDashboardRouter() {
const router = useRouter();
return useMemo(() => {
return {
...router,
replace(href: string, options?: { scroll?: boolean }) {
LoadingRouteHref.setValue(href);
router.replace(href, options);
},
push(href: string, options?: { scroll?: boolean }) {
LoadingRouteHref.setValue(href);
router.push(href, options);
},
};
}, [router]);
}
function useRouterLoadingStatus() {
const loadingHref = useSyncExternalStore(
LoadingRouteHref.subscribe,
LoadingRouteHref.getValue,
LoadingRouteHref.getValue,
);
const pathname = usePathname();
const searchParams = useSearchParams();
const searchParamsStr = searchParams?.toString();
const routerHref = pathname + (searchParamsStr ? `?${searchParamsStr}` : "");
const isLoading = loadingHref && loadingHref !== routerHref;
// reset loading on route load
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (!isLoading) {
LoadingRouteHref.setValue(undefined);
}
}, [isLoading]);
return isLoading;
}
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// Render this on root
function DashboardRouterTopProgressBarInner() {
const isLoading = useRouterLoadingStatus();
const [progress, setProgress] = useState(0);
const progressStartedRef = useRef(false);
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (!isLoading) {
setProgress(0);
progressStartedRef.current = false;
return;
}
if (progressStartedRef.current) {
return;
}
let isMounted = true;
progressStartedRef.current = true;
async function updateProgressBar(progress: number, delay: number) {
if (!isMounted) {
if (!isLoading) {
setProgress(100);
}
return;
}
setProgress(progress);
await wait(delay);
// increase progress by 10% of the remaining progress
// increase the delay by 5% to slow down number of updates for slower progress
updateProgressBar(progress + (100 - progress) / 10, delay * 1.05);
}
async function startEffect() {
// if the loading state remains for atleast 500ms start the progress bar
await wait(500);
if (isMounted) {
updateProgressBar(0, 100);
}
}
startEffect();
return () => {
isMounted = false;
};
}, [isLoading]);
const width = isLoading ? progress : 100;
return (
<span
className="block fixed top-0 h-[2px] bg-blue-500"
style={{
opacity: isLoading ? "100" : "0",
width: `${width}%`,
transition: width === 0 ? "none" : "width 0.2s ease, opacity 0.3s ease",
zIndex: "100000000",
pointerEvents: "none",
}}
/>
);
}
// need to wrap with suspense because of useSearchParams usage
export function DashboardRouterTopProgressBar() {
return (
<Suspense fallback={null}>
<DashboardRouterTopProgressBarInner />
</Suspense>
);
}
+47
View File
@@ -0,0 +1,47 @@
export type Store<T> = {
getValue(): T;
setValue(newValue: T): void;
subscribe(listener: () => void): () => void;
};
/**
* Create a reactive value store
* @param initialValue - The initial value to store
* @example
* ```ts
* const store = createStore(0);
* ```
* @returns A store object
* @internal
*/
export function createStore<T>(initialValue: T): Store<T> {
type Listener = () => void;
const listeners = new Set<Listener>();
let value = initialValue;
const notify = () => {
for (const listener of listeners) {
listener();
}
};
return {
getValue() {
return value;
},
setValue(newValue: T) {
if (newValue === value) {
return;
}
value = newValue;
notify();
},
subscribe(listener: Listener) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
},
};
}
@@ -0,0 +1,4 @@
import { useEffect, useLayoutEffect } from "react";
export const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
+11 -8
View File
@@ -94,19 +94,22 @@
/* 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%;
/* below is ai generated based on the initial */
--chart-6: 200 65% 50%;
--chart-7: 120 70% 45%;
--chart-8: 45 75% 55%;
--chart-9: 260 60% 60%;
--chart-10: 0 70% 50%;
--chart-11: 180 60% 50%;
--chart-12: 100 65% 55%;
--chart-13: 360 70% 55%;
--chart-14: 240 65% 50%;
--chart-15: 80 60% 50%;
}
}
@@ -90,8 +90,10 @@ export const engineKeys = {
health: (instance: string) =>
[...engineKeys.all, instance, "health"] as const,
latestVersion: () => [...engineKeys.all, "latestVersion"] as const,
metrics: (engineId: string) =>
[...engineKeys.all, engineId, "metrics"] as const,
systemMetrics: (engineId: string) =>
[...engineKeys.all, engineId, "systemMetrics"] as const,
queueMetrics: (engineId: string) =>
[...engineKeys.all, engineId, "queueMetrics"] as const,
};
export const contractKeys = {
@@ -913,12 +913,7 @@ export function useAuthorizedWallets() {
export async function fetchChainsFromApi() {
// always fetch from prod for chains for now
// TODO: re-visit this
const res = await fetch("https://api.thirdweb.com/v1/chains", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const res = await fetch("https://api.thirdweb.com/v1/chains");
const json = await res.json();
if (json.error) {
@@ -0,0 +1,54 @@
import { useQuery } from "@tanstack/react-query";
import type { ThirdwebContract } from "thirdweb";
import { getCompilerMetadata } from "thirdweb/contract";
import { getContractMetadata, name, symbol } from "thirdweb/extensions/common";
/**
* Fetch the contract metadata for Dashboard
*
* If the contract itself doesn't implement the metadata extension,
* we try to fall back to:
* 1. name(), symbol()
* 2. our API endpoint
*/
export function useDashboardContractMetadata(contract: ThirdwebContract) {
return useQuery(
["contract-metadata-header", contract.chain.id, contract.address],
async () => {
const [contractMetadata, _name, _symbol, compilerMetadata] =
await Promise.all([
getContractMetadata({ contract }).catch(() => ({
image: "",
name: "",
symbol: "",
})),
name({ contract }).catch(() => ""),
symbol({ contract }).catch(() => ""),
getCompilerMetadata(contract).catch(() => undefined),
]);
/**
* Handle the case where the contract.thirdweb.com endpoint returns a very long name
* example url: https://contract.thirdweb.com/metadata/324/0x66E298335c5992d599132582De8eaAF75348e6e6
* In that case, we split the solidity name on ":" and take the second part
*/
if (compilerMetadata?.name.includes(":")) {
const _name = compilerMetadata.name.split(":")[1];
if (_name) {
compilerMetadata.name = compilerMetadata.name.split(":")[1];
}
}
const contractName =
contractMetadata?.name || _name || compilerMetadata?.name || "";
const contractSymbol = contractMetadata?.symbol || _symbol || "";
return {
name: contractName,
symbol: contractSymbol,
image: contractMetadata.image || "",
contractType: compilerMetadata?.name || "",
};
},
);
}
@@ -126,9 +126,9 @@ export function useEngineSystemHealth(
instanceUrl: string,
pollInterval: number | false = false,
) {
return useQuery(
engineKeys.health(instanceUrl),
async () => {
return useQuery({
queryKey: engineKeys.health(instanceUrl),
queryFn: async () => {
const res = await fetch(`${instanceUrl}system/health`, {
headers: getEngineRequestHeaders(null),
});
@@ -138,23 +138,57 @@ export function useEngineSystemHealth(
const json = (await res.json()) as EngineSystemHealth;
return json;
},
{
enabled: !!instanceUrl,
refetchInterval: pollInterval,
enabled: !!instanceUrl,
refetchInterval: pollInterval,
});
}
export interface EngineSystemQueueMetrics {
result: {
queued: number;
pending: number;
latency?: {
msToSend: { p50: number; p90: number };
msToMine: { p50: number; p90: number };
};
};
}
export function useEngineQueueMetrics(
instanceUrl: string,
pollInterval: number | false = false,
) {
const token = useLoggedInUser().user?.jwt ?? null;
return useQuery({
queryKey: engineKeys.queueMetrics(instanceUrl),
queryFn: async () => {
const res = await fetch(`${instanceUrl}system/queue`, {
headers: getEngineRequestHeaders(token),
});
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
return (await res.json()) as EngineSystemQueueMetrics;
},
);
enabled: !!instanceUrl && !!token,
refetchInterval: pollInterval,
});
}
export function useEngineLatestVersion() {
return useQuery(engineKeys.latestVersion(), async () => {
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/latest-version`, {
method: "GET",
});
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return json.data.version as string;
return useQuery({
queryKey: engineKeys.latestVersion(),
queryFn: async () => {
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/latest-version`, {
method: "GET",
});
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return json.data.version as string;
},
});
}
@@ -299,6 +333,7 @@ export type Transaction = {
gasPrice?: string | null;
maxFeePerGas?: string | null;
maxPriorityFeePerGas?: string | null;
effectiveGasPrice?: string | null;
transactionType?: number | null;
transactionHash?: string | null;
queuedAt?: string | null;
@@ -1462,11 +1497,11 @@ export interface EngineResourceMetrics {
};
}
export function useEngineResourceMetrics(engineId: string) {
export function useEngineSystemMetrics(engineId: string) {
const [enabled, setEnabled] = useState(true);
return useQuery(
engineKeys.metrics(engineId),
engineKeys.systemMetrics(engineId),
async () => {
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/metrics`,
@@ -1,7 +1,8 @@
import { isLoginRequired } from "@/constants/auth";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useQuery } from "@tanstack/react-query";
import type { EnsureLoginResponse } from "app/api/auth/ensure-login/route";
import { usePathname, useRouter } from "next/navigation";
import { usePathname } from "next/navigation";
import { useRef } from "react";
import {
useActiveAccount,
@@ -20,7 +21,7 @@ export function useLoggedInUser(): {
isLoggedIn: boolean;
user: { address: string; jwt?: string } | null;
} {
const router = useRouter();
const router = useDashboardRouter();
const pathname = usePathname();
const connectedAddress = useActiveAccount()?.address;
const connectionStatus = useActiveWalletConnectionStatus();
@@ -1,29 +1,21 @@
import { useToast } from "@chakra-ui/react";
import { useSDKChainId } from "@thirdweb-dev/react";
import type { Split } from "@thirdweb-dev/sdk";
import type {
BalanceQueryRequest,
BalanceQueryResponse,
} from "pages/api/moralis/balances";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { distribute, distributeByToken } from "thirdweb/extensions/split";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import invariant from "tiny-invariant";
import { parseErrorToMessage } from "utils/errorParser";
import { splitsKeys } from "..";
import {
useMutationWithInvalidate,
useQueryWithNetwork,
} from "./query/useQueryWithNetwork";
export function useSplitData(contract?: Split) {
return useQueryWithNetwork(
splitsKeys.list(contract?.getAddress()),
async () => contract?.getAllRecipients(),
{
enabled: !!contract,
},
);
}
export function useSplitBalances(contractAddress?: string) {
const chainId = useSDKChainId();
export function useSplitBalances(contract: ThirdwebContract) {
const chainId = contract.chain.id;
const contractAddress = contract.address;
const currencies = useQueryWithNetwork(
splitsKeys.currencies(contractAddress),
async () => {
@@ -48,72 +40,31 @@ export function useSplitBalances(contractAddress?: string) {
return currencies;
}
export function useSplitDistributeFunds(contract?: Split) {
const contractAddress = contract?.getAddress();
const balances = useSplitBalances(contractAddress);
const toast = useToast();
export function useSplitDistributeFunds(contract: ThirdwebContract) {
const contractAddress = contract.address;
const balances = useSplitBalances(contract);
const { mutateAsync } = useSendAndConfirmTransaction();
return useMutationWithInvalidate(
async () => {
invariant(contract, "split contract is not ready");
invariant(balances.data, "No balances to distribute");
const distributions = (balances.data || [])
.filter((token) => token.display_balance !== "0.0")
.map(async (currency) => {
if (currency.name === "Native Token") {
await contract
.distribute()
.then(() => {
toast({
position: "bottom",
variant: "solid",
title: "Success",
description: `Successfully distributed ${currency.name}`,
status: "success",
duration: 5000,
isClosable: true,
const transaction =
currency.name === "Native Token"
? distribute({ contract })
: distributeByToken({
contract,
tokenAddress: currency.token_address,
});
return void 0;
})
.catch((err: unknown) => {
toast({
position: "bottom",
variant: "solid",
title: `Error distributing ${currency.name}`,
description: parseErrorToMessage(err),
status: "error",
duration: 9000,
isClosable: true,
});
});
} else {
await contract
.distributeToken(currency.token_address)
.then(() => {
toast({
position: "bottom",
variant: "solid",
title: "Success",
description: `Successfully distributed ${currency.name}`,
status: "success",
duration: 5000,
isClosable: true,
});
return void 0;
})
.catch((err: unknown) => {
toast({
position: "bottom",
variant: "solid",
title: `Error distributing ${currency.name}`,
description: parseErrorToMessage(err),
status: "error",
duration: 9000,
isClosable: true,
});
});
}
const promise = mutateAsync(transaction);
toast.promise(promise, {
success: `Successfully distributed ${currency.name}`,
error: `Error distributing ${currency.name}`,
loading: "Distributing funds",
});
});
return await Promise.all(distributions);
@@ -4,16 +4,15 @@ 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, useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
import { mapV4ChainToV5Chain } from "contexts/map-chains";
import { useTrack } from "hooks/analytics/useTrack";
import { thirdwebClient } from "lib/thirdweb-client";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { toUnits } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
import { mapV4ChainToV5Chain } from "../../../../../../../contexts/map-chains";
function formatTime(seconds: number) {
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
@@ -48,6 +47,9 @@ export function FaucetButton({
client: thirdwebClient,
});
const trackEvent = useTrack();
const queryClient = useQueryClient();
const claimMutation = useMutation({
mutationFn: async () => {
trackEvent({
@@ -64,7 +66,6 @@ export function FaucetButton({
body: JSON.stringify({
chainId: chainId,
toAddress: address,
amount,
}),
});
@@ -80,7 +81,6 @@ export function FaucetButton({
label: "success",
chain_id: chainId,
});
router.refresh();
},
onError: (error) => {
trackEvent({
@@ -91,6 +91,12 @@ export function FaucetButton({
errorMsg: error instanceof Error ? error.message : "Unknown error",
});
},
onSettled: () => {
return queryClient.invalidateQueries([
"testnet-faucet-can-claim",
chainId,
]);
},
});
const canClaimFaucetQuery = useQuery({
@@ -107,8 +113,6 @@ export function FaucetButton({
},
});
const router = useRouter();
const isFaucetEmpty =
faucetWalletBalanceQuery.data !== undefined &&
faucetWalletBalanceQuery.data.value < toUnits("1", 17);
@@ -2,6 +2,7 @@
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { thirdwebClient } from "@/constants/client";
import { useTrack } from "hooks/analytics/useTrack";
import { defineDashboardChain } from "lib/defineDashboardChain";
import { useTheme } from "next-themes";
import { PayEmbed } from "thirdweb/react";
@@ -9,10 +10,21 @@ import { getSDKTheme } from "../../../../../../components/sdk-component-theme";
export function PayModalButton(props: { chainId: number; label: string }) {
const { theme } = useTheme();
const trackEvent = useTrack();
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="primary" className="w-full">
<Button
variant="primary"
className="w-full"
onClick={() => {
trackEvent({
category: "pay",
action: "buy",
label: "attempt",
});
}}
>
{props.label}
</Button>
</DialogTrigger>
@@ -26,6 +38,34 @@ export function PayModalButton(props: { chainId: number; label: string }) {
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
className="!w-auto"
payOptions={{
onPurchaseSuccess(info) {
if (
info.type === "crypto" &&
info.status.status !== "NOT_FOUND"
) {
trackEvent({
category: "pay",
action: "buy",
label: "success",
type: info.type,
chainId: info.status.quote.toToken.chainId,
tokenAddress: info.status.quote.toToken.tokenAddress,
amount: info.status.quote.toAmount,
});
}
if (info.type === "fiat" && info.status.status !== "NOT_FOUND") {
trackEvent({
category: "pay",
action: "buy",
label: "success",
type: info.type,
chainId: info.status.quote.toToken.chainId,
tokenAddress: info.status.quote.toToken.tokenAddress,
amount: info.status.quote.estimatedToTokenAmount,
});
}
},
prefillBuy: {
// Do not include local chain overrides for chain pages
// eslint-disable-next-line no-restricted-syntax
@@ -4,7 +4,7 @@ import { FaucetButton } from "../client/FaucetButton";
import { GiftIcon } from "../icons/GiftIcon";
import { SectionTitle } from "./SectionTitle";
const amountToGive = "0.1";
const amountToGive = "0.01";
export async function FaucetSection(props: { chain: ChainMetadata }) {
const { chain } = props;
@@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button";
import { type SortBy, fetchTopContracts } from "lib/search";
import { fetchTopContracts } from "lib/search";
import { ArrowRightIcon, CircleAlertIcon } from "lucide-react";
import Link from "next/link";
import { TrendingContractSection } from "../../../trending/components/trending-table";
@@ -14,7 +14,6 @@ import { ExplorersSection } from "./components/server/explorer-section";
export default async function Page(props: {
params: { chain_id: string };
searchParams: { page?: number; sortBy?: SortBy };
}) {
const chain = await getChain(props.params.chain_id);
const chainMetadata = await getChainMetadata(chain.chainId);
@@ -22,8 +21,6 @@ export default async function Page(props: {
const topContracts = await fetchTopContracts({
chainId: chain.chainId,
page: props.searchParams.page,
sortBy: props.searchParams.sortBy,
perPage: 3,
timeRange: "month",
});
@@ -83,8 +80,8 @@ export default async function Page(props: {
<TrendingContractSection
topContracts={topContracts}
chainId={chain.chainId}
searchParams={props.searchParams}
showPagination={false}
searchParams={undefined}
/>
</section>
)}
@@ -4,6 +4,9 @@ import React from "react";
import { TrendingContractSection } from "../../../../trending/components/trending-table";
import { getChain } from "../../../utils";
// we're using searchParams here - use dynamic rendering
export const dynamic = "force-dynamic";
export default async function Page(props: {
params: { chain_id: string };
searchParams: { page?: number; sortBy?: SortBy };
@@ -10,8 +10,9 @@ import {
} from "@/components/ui/popover";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Separator } from "@/components/ui/separator";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { ChevronDownIcon, Filter, XIcon } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { usePathname, useSearchParams } from "next/navigation";
import type React from "react";
import { type PropsWithChildren, useCallback, useMemo } from "react";
import { products } from "../../../components/server/products";
@@ -73,7 +74,7 @@ type FilterResetButtonProps = {
const FilterResetButton: React.FC<
PropsWithChildren<FilterResetButtonProps>
> = ({ filters, children }) => {
const router = useRouter();
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const mutableSearchParams = useMemo(() => {
@@ -124,7 +125,7 @@ type ChainTypeFilterProps = { sectionOnly?: boolean };
export const ChainTypeFilter: React.FC<ChainTypeFilterProps> = ({
sectionOnly,
}) => {
const router = useRouter();
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
@@ -228,7 +229,7 @@ type ChainOptionsFilterProps = { sectionOnly?: boolean };
export const ChainOptionsFilter: React.FC<ChainOptionsFilterProps> = ({
sectionOnly,
}) => {
const router = useRouter();
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const mutableSearchParams = useMemo(() => {
@@ -332,7 +333,7 @@ type ChainServiceFilterProps = { sectionOnly?: boolean };
export const ChainServiceFilter: React.FC<ChainServiceFilterProps> = ({
sectionOnly,
}) => {
const router = useRouter();
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const mutableSearchParams = useMemo(() => {
@@ -1,7 +1,8 @@
"use client";
import { PaginationButtons } from "@/components/pagination-buttons";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { usePathname, useSearchParams } from "next/navigation";
import { useCallback } from "react";
type ChainlistPaginationProps = {
@@ -15,7 +16,7 @@ export const ChainlistPagination: React.FC<ChainlistPaginationProps> = ({
}) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
const router = useDashboardRouter();
const createPageURL = useCallback(
(pageNumber: number) => {
@@ -2,8 +2,9 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { SearchIcon, XCircleIcon } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { usePathname, useSearchParams } from "next/navigation";
import { useRef } from "react";
import { useDebouncedCallback } from "use-debounce";
@@ -15,7 +16,7 @@ function cleanUrl(url: string) {
}
export const SearchInput: React.FC = () => {
const router = useRouter();
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
@@ -1,8 +1,9 @@
"use client";
import { Button } from "@/components/ui/button";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { Grid2X2Icon, ListIcon } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { usePathname, useSearchParams } from "next/navigation";
import { useCallback } from "react";
type hainListViewProps = {
@@ -12,7 +13,7 @@ type hainListViewProps = {
export const ChainListView: React.FC<hainListViewProps> = ({ activeView }) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
const router = useDashboardRouter();
const createPageURL = useCallback(
(view: "grid" | "table") => {
@@ -96,12 +96,6 @@ async function getChainsToRender(params: SearchParams) {
// if the chain does not have all of the services in the filter, return false to filter it out
if (
!urlServiceArray.every((service) => {
// TODO improve this
// We don't have "faucet" added in services array, so for now we just check if it's testnet or not
if (service === "faucet") {
return chain.testnet;
}
return chain.services.find((s) => s.enabled && s.service === service);
})
) {
@@ -154,6 +148,10 @@ export const metadata: Metadata = {
"A list of EVM networks with RPCs, smart contracts, block explorers & faucets. Deploy smart contracts to all EVM chains with thirdweb.",
};
// we use headers() to determine if we should default to table or grid view by checking viewport
// so this page needs to be forced as dynamic
export const dynamic = "force-dynamic";
export default function ChainListPage(props: { searchParams: SearchParams }) {
const headersList = headers();
const viewportWithHint = Number(
Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

@@ -6,12 +6,18 @@ export type ChainSupportedService =
| "engine"
| "account-abstraction"
| "pay"
| "faucet"
| "rpc-edge";
| "rpc-edge"
| "chainsaw";
export type ChainService = {
service: ChainSupportedService;
enabled: boolean;
status: "enabled" | "disabled";
};
export type ChainServices = {
chainId: number;
services: Array<ChainService>;
};
export type ChainMetadataWithServices = ChainMetadata & {
@@ -20,48 +20,75 @@ 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 treasureBanner from "./temp-assets/treasureBanner.png";
import treasureCTA from "./temp-assets/treasureCta.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
import type { ChainMetadata } from "thirdweb/chains";
import type {
ChainMetadataWithServices,
ChainService,
ChainServices,
} from "./types/chain";
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
export async function getChains() {
const response = await fetch(
`${THIRDWEB_API_HOST}/v1/chains?includeServices=true`,
// revalidate every hour
{ next: { revalidate: 60 * 60 } },
);
const [chains, chainServices] = await Promise.all([
fetch(
`${THIRDWEB_API_HOST}/v1/chains`,
// revalidate every 60 minutes
{ next: { revalidate: 60 * 60 } },
).then((res) => res.json()) as Promise<{ data: ChainMetadata[] }>,
fetch(
`${THIRDWEB_API_HOST}/v1/chains/services`,
// revalidate every 60 minutes
{ next: { revalidate: 60 * 60 } },
).then((res) => res.json()) as Promise<{
data: Record<number, Array<ChainService>>;
}>,
]);
if (!response.ok) {
response.body?.cancel();
if (!chains.data.length) {
throw new Error("Failed to fetch chains");
}
return (await response.json()).data as ChainMetadataWithServices[];
return chains.data.map((c) => ({
...c,
services: chainServices.data[c.chainId] || [],
})) satisfies ChainMetadataWithServices[];
}
export async function getChain(
chainIdOrSlug: string,
): Promise<ChainMetadataWithServices> {
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/chains/${chainIdOrSlug}?includeServices=true`,
// revalidate every 15 minutes
{ next: { revalidate: 15 * 60 } },
);
const [chain, chainServices] = await Promise.all([
fetch(
`${THIRDWEB_API_HOST}/v1/chains/${chainIdOrSlug}`,
// revalidate every 15 minutes
{ next: { revalidate: 15 * 60 } },
).then((res) => res.json()) as Promise<{ data: ChainMetadata }>,
fetch(
`${THIRDWEB_API_HOST}/v1/chains/${chainIdOrSlug}/services`,
// revalidate every 15 minutes
{ next: { revalidate: 15 * 60 } },
).then((res) => res.json()) as Promise<{ data: ChainServices }>,
]);
const result = await res.json();
if (!result.data) {
if (!chain.data) {
redirect("/404");
}
return result.data as ChainMetadataWithServices;
return {
...chain.data,
services: chainServices.data.services,
} satisfies ChainMetadataWithServices;
}
type ChainMetadata = Partial<{
type ExtraChainMetadata = Partial<{
headerImgUrl: string;
about: string;
gasSponsored: boolean;
@@ -321,12 +348,24 @@ const chainMetaRecord = {
buttonText: "Learn more",
},
},
} satisfies Record<number, ChainMetadata>;
//treasure
978657: {
headerImgUrl: treasureBanner.src,
about:
'Treasure is the decentralized game console. Powered by $MAGIC, the Treasure L2 serves as the base layer for the best cryptonative games and projects. Treasure and its network of "Infinity Chains" L3s offers EVM compatibility, massive scale, and decentralized infrastructure enshrined throughout. Combined with a passionate community and builder support, developers on Treasure are equipped with all of the tools they need to not only build great games and products, but also distribute to the masses.',
cta: {
backgroundImageUrl: treasureCTA.src,
title: "Start building on Treasure!",
buttonLink: "https://portal.treasure.lol",
buttonText: "Learn more",
},
},
} satisfies Record<number, ExtraChainMetadata>;
// END TEMPORARY
export async function getChainMetadata(
chainId: number,
): Promise<ChainMetadata | null> {
): Promise<ExtraChainMetadata | null> {
// TODO: fetch this from the API
if (chainId in chainMetaRecord) {
return chainMetaRecord[chainId as keyof typeof chainMetaRecord];
@@ -51,3 +51,6 @@ export default async function Layout({
</div>
);
}
// because cookies() is used
export const dynamic = "force-dynamic";
@@ -15,11 +15,11 @@ import { ImageUpload } from "@/components/ui/image-upload";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItemButton } from "@/components/ui/radio-group";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -49,7 +49,7 @@ export function CreateEcosystemForm() {
>();
const { data: billingAccountInfo } = useAccount();
const router = useRouter();
const router = useDashboardRouter();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -113,3 +113,6 @@ export default async function Page() {
</main>
);
}
// because cookies() is used
export const dynamic = "force-dynamic";
@@ -79,3 +79,6 @@ export default async function DashboardConnectPayPage() {
</div>
);
}
// because cookies() is used
export const dynamic = "force-dynamic";
@@ -7,6 +7,7 @@ const links: {
name: string;
segment: string;
}[] = [
{ name: "Transaction Simulator", segment: "transaction-simulator" },
{ name: "Wei Converter", segment: "wei-converter" },
{ name: "Hex Converter", segment: "hex-converter" },
{ name: "Unix Time Converter", segment: "unixtime-converter" },
@@ -0,0 +1,387 @@
"use client";
import { CodeBlock } from "@/components/ui/CodeBlock";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { ToolTipLabel } from "@/components/ui/tooltip";
import type { Abi, AbiFunction } from "abitype";
import { thirdwebClient } from "lib/thirdweb-client";
import { useV5DashboardChain } from "lib/v5-adapter";
import { ArrowDown } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { MdOutlineAccountBalanceWallet } from "react-icons/md";
import {
getContract,
prepareContractCall,
resolveMethod,
simulateTransaction,
toSerializableTransaction,
toWei,
} from "thirdweb";
import { resolveContractAbi } from "thirdweb/contract";
import { useActiveAccount } from "thirdweb/react";
import { parseAbiParams } from "thirdweb/utils";
import { ShareButton } from "../../components/share";
export type SimulateTransactionForm = {
chainId: number;
from: string;
to: string;
functionName: string;
functionArgs: string;
value: string;
};
type State = {
success: boolean;
message: string;
codeExample: string;
shareUrl: string;
};
export const TransactionSimulator = (props: {
searchParams: Partial<SimulateTransactionForm>;
}) => {
const activeAccount = useActiveAccount();
const initialFormValues = props.searchParams;
const [isLoading, setIsLoading] = useState(false);
const [state, setState] = useState<State>({
success: false,
message: "",
codeExample: "",
shareUrl: "",
});
const form = useForm<SimulateTransactionForm>();
const chainId = form.watch("chainId");
const chain = useV5DashboardChain(
Number.isInteger(Number(chainId)) ? chainId : undefined,
);
async function handleSimulation(data: SimulateTransactionForm) {
try {
setIsLoading(true);
const { from, to, value, functionArgs, functionName } = data;
if (!chain) {
throw new Error("Invalid chainId");
}
const contract = getContract({
client: thirdwebClient,
chain,
address: to,
});
const abi = (await resolveContractAbi(contract)) as Abi;
const abiItem = abi.find(
(o) => o.type === "function" && o.name === functionName,
) as AbiFunction;
if (!abiItem) {
throw new Error(`Contract does not have method \`${functionName}]\``);
}
const inputParams = functionArgs
? functionArgs.split(/[\n,]+/).map((arg) => arg.trim())
: [];
if (abiItem.inputs.length !== inputParams.length) {
throw new Error(
`Param length mismatch. Expected ${abiItem.inputs.length} params. Received ${
inputParams.length
}`,
);
}
// Parse to proper types
const types = abiItem.inputs.map((o) => o.type);
const params = parseAbiParams(types, inputParams);
const codeExample = getCodeExample(
{
chainId: chain.id,
from,
to,
value,
functionArgs,
functionName,
},
params,
);
const shareUrl = getShareUrl({
chainId: chain.id,
from,
to,
value,
functionArgs,
functionName,
});
const transaction = prepareContractCall({
contract,
method: resolveMethod(functionName),
params,
value: value ? toWei(value) : undefined,
});
const [simulateResult, populatedTransaction] = await Promise.all([
simulateTransaction({
from,
transaction,
}),
toSerializableTransaction({
from,
transaction,
}),
]);
setState({
success: true,
message: `result: ${simulateResult.length > 0 ? simulateResult.join(",") : "Method did not return a result."}\n
${Object.keys(populatedTransaction)
.map((key) => {
let _val = populatedTransaction[key as keyof typeof populatedTransaction];
if (key === "value" && !_val) {
_val = 0;
}
return `${key}: ${_val}\n`;
})
.join("")}`,
codeExample,
shareUrl,
});
} catch (err) {
setState({
success: false,
message: `${err}`,
codeExample: "",
shareUrl: "",
});
}
setIsLoading(false);
}
return (
<div className="max-w-[800px] space-y-4">
<p>
Simulate a transaction on any EVM chain. Account Abstraction support is
coming soon.
</p>
<form
onSubmit={form.handleSubmit(handleSimulation)}
className="space-y-4 flex-col"
>
<Card className="flex flex-col gap-4 p-4">
<div className="flex gap-2 sm:items-center flex-col sm:flex-row">
<Label htmlFor="chainId" className="min-w-60">
Chain ID
</Label>
<Input
type="number"
required
placeholder="The chain ID of the sender wallet"
defaultValue={initialFormValues.chainId}
{...form.register("chainId", { required: true })}
// Hide spinner.
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
</div>
<div className="flex gap-2 sm:items-center flex-col sm:flex-row">
<Label htmlFor="from" className="min-w-60">
From Address
</Label>
<Input
required
placeholder="The sender wallet address"
defaultValue={initialFormValues.from}
{...form.register("from", { required: true })}
/>
{activeAccount && (
<ToolTipLabel label="Use current wallet address">
<Button
variant="ghost"
size="icon"
onClick={() => form.setValue("from", activeAccount.address)}
>
<MdOutlineAccountBalanceWallet />
</Button>
</ToolTipLabel>
)}
</div>
</Card>
<div className="flex justify-center">
<ArrowDown />
</div>
<Card className="flex flex-col gap-4 p-4">
<div className="flex gap-2 sm:items-center flex-col sm:flex-row">
<Label htmlFor="to" className="min-w-60">
Contract Address
</Label>
<Input
placeholder="The contract address to call"
defaultValue={initialFormValues.to}
{...form.register("to", { required: true })}
/>
</div>
<div className="flex gap-2 sm:items-center flex-col sm:flex-row">
<Label htmlFor="functionName" className="min-w-60">
Function Name
</Label>
<Input
placeholder="The contract function name (i.e. mintTo)"
defaultValue={initialFormValues.functionName}
{...form.register("functionName", { required: true })}
/>
</div>
<div className="flex gap-2 sm:items-center flex-col sm:flex-row">
<Label htmlFor="functionArgs" className="min-w-60">
Function Arguments
</Label>
<Textarea
placeholder="Comma-separated arguments to call the function"
rows={3}
className="bg-transparent"
{...form.register("functionArgs")}
defaultValue={
initialFormValues.functionArgs
? decodeURIComponent(initialFormValues.functionArgs)
: undefined
}
/>
</div>
<div className="flex gap-2 sm:items-center flex-col sm:flex-row">
<Label htmlFor="value" className="min-w-60">
Value
</Label>
<Input
{...form.register("value")}
placeholder={`The amount of native currency to send (e.g "0.01")`}
/>
</div>
<Button type="submit">
{isLoading ? (
<>
<Spinner className="w-4 h-4 mr-2" />
Simulating
</>
) : (
"Simulate Transaction"
)}
</Button>
</Card>
</form>
{state.message && (
<>
<div className="flex justify-center">
<ArrowDown />
</div>
<Card className="max-w-[800px] p-4">
<p className="text-sm font-mono whitespace-pre-wrap overflow-auto">
{state.success
? "--- ✅ Simulation succeeded ---\n"
: "--- ❌ Simulation failed ---\n"}
{state.message}
</p>
</Card>
</>
)}
<div className="flex justify-between">
{state.shareUrl && (
<CopyTextButton
textToShow="Copy Simulation Link"
tooltip="Copy Simulation Link"
textToCopy={state.shareUrl}
copyIconPosition="right"
/>
)}
<ShareButton
cta="Share on X"
href="https://twitter.com/intent/tweet?text=Easy-to-use%20transaction%20simulator%20by%20thirdweb%20%F0%9F%98%8D&url=https%3A%2F%2Fthirdweb.com%2Ftools%2Ftransaction-simulator"
/>
</div>
{state.codeExample && (
<div className="max-w-[800px] flex flex-col gap-2 pt-16 ">
<a
href="https://portal.thirdweb.com/references/typescript/v5/simulateTransaction"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Connect SDK example
</a>
<CodeBlock code={state.codeExample} language="typescript" />
</div>
)}
</div>
);
};
// Generate code example with input values.
const getCodeExample = (
parsedData: SimulateTransactionForm,
params: unknown[],
) => {
const displayParams = params.map((item) => {
if (typeof item === "bigint") {
return `${item.toString()}n`;
}
if (typeof item === "string") {
return `"${item}"`;
}
if (
typeof item === "object" &&
item !== null &&
!Array.isArray(item) &&
Object.prototype.toString.call(item) === "[object Object]"
) {
return JSON.stringify(item);
}
return item;
});
return `import {
getContract,
defineChain,
prepareContractCall,
createThirdwebClient
} from "thirdweb";
const client = createThirdwebClient({
// use secretKey instead of clientId in backend environment
clientId: "your-client-id",
});
const contract = getContract({
client,
chain: defineChain(${parsedData.chainId}),
address: "${parsedData.to}",
});
const transaction = prepareContractCall({
contract,
method: resolveMethod("${parsedData.functionName}"),
params: [${displayParams.join(", ")}],
value: ${parsedData.value ? `${parsedData.value}n` : ""},
});
const result = await simulateTransaction({
from: "${parsedData.from}",
transaction,
});`;
};
// Generate share link from input values.
const getShareUrl = (parsedData: SimulateTransactionForm) => {
const url = new URL(`${window.location.origin}/tools/transaction-simulator`);
url.searchParams.set("chainId", parsedData.chainId.toString());
url.searchParams.set("from", parsedData.from);
url.searchParams.set("to", parsedData.to);
url.searchParams.set("functionName", parsedData.functionName);
url.searchParams.set(
"functionArgs",
encodeURIComponent(parsedData.functionArgs),
);
url.searchParams.set("value", parsedData.value);
return url.href;
};
@@ -0,0 +1,17 @@
import type { Metadata } from "next";
import {
type SimulateTransactionForm,
TransactionSimulator,
} from "./components/TransactionSimulator";
export const metadata: Metadata = {
title: "thirdweb Transaction Simulator",
description:
"Simulate any EVM transaction. Get gas estimates and onchain error messages to debug your contract calls.",
};
export default async function Page(props: {
searchParams: Partial<SimulateTransactionForm>;
}) {
return <TransactionSimulator searchParams={props.searchParams} />;
}
@@ -6,10 +6,11 @@ import {
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { usePathname, useSearchParams } from "next/navigation";
export function TablePagination() {
const router = useRouter();
const router = useDashboardRouter();
const path = usePathname();
const searchParams = useSearchParams();
const page = Number(searchParams?.get("page") || 1);
@@ -1,24 +1,32 @@
"use client";
import { Button } from "@/components/ui/button";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { ChevronDown } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { usePathname } from "next/navigation";
import type { SortBy, TimeRange } from "../../../../lib/search";
export type SortingHeaderProps = {
sortBy: string;
title: string;
searchParams:
| { timeRange?: TimeRange; page?: number; sortBy?: SortBy }
| undefined;
};
export function SortingHeader(props: SortingHeaderProps) {
const router = useRouter();
const router = useDashboardRouter();
const path = usePathname();
const searchParams = useSearchParams();
const range = searchParams?.get("timeRange");
const page = searchParams?.get("page");
const currentSort = searchParams?.get("sortBy") || "transactionCount";
const enableSorting = !!props.searchParams;
const { timeRange, page, sortBy } = props.searchParams || {};
const currentSort = sortBy || "transactionCount";
const isCurrentSort = currentSort === props.sortBy;
const justify =
props.sortBy === "transactionCountChange" ? "justify-start" : "justify-end";
if (!enableSorting) {
return <div className={`flex flex-row ${justify}`}>{props.title}</div>;
}
return (
<div className={`flex flex-row ${justify}`}>
<Button
@@ -26,7 +34,7 @@ export function SortingHeader(props: SortingHeaderProps) {
variant={"link"}
onClick={() => {
router.replace(
`${path}?sortBy=${props.sortBy}${range?.length ? `&timeRange=${range}` : ""}${page?.length ? `&page=${page}` : ""}`,
`${path}?sortBy=${props.sortBy}${timeRange?.length ? `&timeRange=${timeRange}` : ""}${page ? `&page=${page}` : ""}`,
{
scroll: false,
},
@@ -1,10 +1,11 @@
"use client";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { TimeRange } from "lib/search";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { usePathname, useSearchParams } from "next/navigation";
export function TimeRangeSwitcher() {
const router = useRouter();
const router = useDashboardRouter();
const path = usePathname();
const searchParams = useSearchParams();
const currentSort = searchParams?.get("sortBy") || "transactionCount";
@@ -17,14 +17,16 @@ import { ChainIcon } from "../../(chain)/components/server/chain-icon";
import { TablePagination } from "./pagination.client";
import { SortingHeader } from "./sorting-header.client";
export async function TrendingContractSection(props: {
export function TrendingContractSection(props: {
topContracts: TrendingContract[];
chainId?: number;
perPage?: number;
searchParams: { timeRange?: TimeRange; page?: number; sortBy?: SortBy };
searchParams:
| { timeRange?: TimeRange; page?: number; sortBy?: SortBy }
| undefined;
showPagination: boolean;
}) {
const { page } = props.searchParams;
const { page } = props.searchParams || {};
const firstIndex = Math.max(0, ((page || 1) - 1) * (props.perPage || 10));
const showChainColumn = !props.chainId;
@@ -44,16 +46,29 @@ export async function TrendingContractSection(props: {
<SortingHeader
title="Transactions"
sortBy="transactionCount"
searchParams={props.searchParams}
/>
</TableHead>
<TableHead>
<SortingHeader title="Wallets" sortBy="walletCount" />
<SortingHeader
title="Wallets"
sortBy="walletCount"
searchParams={props.searchParams}
/>
</TableHead>
<TableHead>
<SortingHeader title="Gas Usage" sortBy="gasUsage" />
<SortingHeader
title="Gas Usage"
sortBy="gasUsage"
searchParams={props.searchParams}
/>
</TableHead>
<TableHead>
<SortingHeader title="Value Moved" sortBy="totalValueMoved" />
<SortingHeader
title="Value Moved"
sortBy="totalValueMoved"
searchParams={props.searchParams}
/>
</TableHead>
</TableRow>
</TableHeader>
@@ -11,6 +11,9 @@ export const metadata: Metadata = {
},
};
// we're using searchParams here - use dynamic rendering
export const dynamic = "force-dynamic";
export default async function DashboardContractTrendingPage(props: {
searchParams: { timeRange?: TimeRange; page?: number; sortBy?: SortBy };
}) {
@@ -1,4 +1,3 @@
import { getIpAddress } from "lib/ip";
import { cacheTtl } from "lib/redis";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
@@ -46,12 +45,21 @@ export const GET = async (req: NextRequest) => {
return NextResponse.json(res);
}
const ipAddress = getIpAddress();
// vercel provides this for us in the request object || fall back to the header
const ipAddress = req.ip || req.headers.get("X-Forwarded-For");
if (!ipAddress) {
return NextResponse.json(
{
error: "Could not validate elligibility.",
},
{ status: 400 },
);
}
const cacheKey = `testnet-faucet:${chainId}:${ipAddress}`;
const ttlSeconds = await cacheTtl(cacheKey);
const res: CanClaimResponseType = {
canClaim: ttlSeconds > 0,
canClaim: ttlSeconds <= 0,
ttlSeconds,
type: "throttle",
};
@@ -1,18 +1,16 @@
import { getIpAddress } from "lib/ip";
import { cacheDeleteKey, cacheExists, cacheSet } from "lib/redis";
import { NextResponse } from "next/server";
import { cacheGet, cacheSet } from "lib/redis";
import { type NextRequest, NextResponse } from "next/server";
import { ZERO_ADDRESS } from "thirdweb";
interface RequestTestnetFundsPayload {
chainId: number;
toAddress: string;
amount: string;
}
// Note: This handler cannot use "edge" runtime because of Redis usage.
export const POST = async (req: Request) => {
export const POST = async (req: NextRequest) => {
const requestBody = (await req.json()) as RequestTestnetFundsPayload;
const { chainId, toAddress, amount } = requestBody;
const { chainId, toAddress } = requestBody;
if (Number.isNaN(chainId)) {
throw new Error("Invalid chain ID.");
}
@@ -33,55 +31,59 @@ export const POST = async (req: Request) => {
);
}
const ipAddress = getIpAddress();
// vercel provides this for us in the request object || fall back to the header
const ipAddress = req.ip || req.headers.get("X-Forwarded-For");
if (!ipAddress) {
return NextResponse.json(
{
error: "Could not validate elligibility.",
},
{ status: 400 },
);
}
const cacheKey = `testnet-faucet:${chainId}:${ipAddress}`;
// Assert 1 request per IP/chain every 24 hours.
if (await cacheExists(cacheKey)) {
// get the cached value
const cachedValue = await cacheGet(cacheKey);
// if we have a cached value, return an error
if (cachedValue !== null) {
return NextResponse.json(
{ error: "Already requested funds on this chain in the past 24 hours." },
{ status: 429 },
);
}
// Store the claim request for 24 hours.
await cacheSet(cacheKey, "claimed", 24 * 60 * 60);
try {
// Store the claim request for 24 hours.
await cacheSet(cacheKey, "claimed", 24 * 60 * 60);
// then actually transfer the funds
const url = `${THIRDWEB_ENGINE_URL}/backend-wallet/${chainId}/transfer`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-backend-wallet-address": NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET,
Authorization: `Bearer ${THIRDWEB_ACCESS_TOKEN}`,
},
body: JSON.stringify({
to: toAddress,
currencyAddress: ZERO_ADDRESS,
amount: "0.01",
}),
});
// Assert max amount.
if (Number.parseFloat(amount) > 0.1) {
return NextResponse.json({ error: "Invalid amount." }, { status: 400 });
}
if (amount !== "0") {
try {
const url = `${THIRDWEB_ENGINE_URL}/backend-wallet/${chainId}/transfer`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-backend-wallet-address": NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET,
Authorization: `Bearer ${THIRDWEB_ACCESS_TOKEN}`,
},
body: JSON.stringify({
to: toAddress,
currencyAddress: ZERO_ADDRESS,
amount,
}),
});
if (!response.ok) {
const error = await response.json();
throw error.error;
}
} catch (error) {
// Remove user from cache for retry on transfer failure.
cacheDeleteKey(cacheKey);
return NextResponse.json(
{ error: `${(error as Error)?.message}` },
{ status: 500 },
);
if (!response.ok) {
const error = await response.json();
throw error.error;
}
} catch (error) {
return NextResponse.json(
{ error: `${(error as Error)?.message}` },
{ status: 500 },
);
}
return NextResponse.json({ amount: amount.toString() }, { status: 200 });
return NextResponse.json({ amount: "0.01" }, { status: 200 });
};
@@ -1,23 +1,25 @@
"use client";
import { Button } from "@/components/ui/button";
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
import { Menu, XIcon } from "lucide-react";
import Link from "next/link";
import { useLayoutEffect, useState } from "react";
import { useState } from "react";
import { headerLinks } from "./headerLinks";
export function MobileBurgerMenu() {
const [showBurgerMenu, setShowBurgerMenu] = useState(false);
useLayoutEffect(() => {
useIsomorphicLayoutEffect(() => {
if (showBurgerMenu) {
// eslint-disable-next-line react-compiler/react-compiler
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
}
}, [showBurgerMenu]);
useLayoutEffect(() => {
useIsomorphicLayoutEffect(() => {
return () => {
document.body.style.overflow = "auto";
};
+2
View File
@@ -1,5 +1,6 @@
import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner";
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import type { Metadata } from "next";
import PlausibleProvider from "next-plausible";
@@ -70,6 +71,7 @@ export default function RootLayout({
<PostHogPageView />
<AppRouterProviders>{children}</AppRouterProviders>
<Toaster richColors />
<DashboardRouterTopProgressBar />
<NextTopLoader
color="hsl(var(--primary))"
height={2}
+1 -1
View File
@@ -197,7 +197,7 @@ export async function isLoggedIn(address: string) {
return false;
}
// if the address matches what we expect, we're logged in
if (getAddress(json.address) === getAddress(address)) {
if (getAddress(json.data.creatorWalletAddress) === getAddress(address)) {
// set the active account cookie again
cookieStore.set(COOKIE_ACTIVE_ACCOUNT, getAddress(address), {
httpOnly: false,
@@ -129,13 +129,11 @@ export const BarChart = <
tickFormatter={(payload) =>
index.format
? index.format(payload)
: index.type === "date"
? new Date(payload).toLocaleDateString(undefined, {
day: "2-digit",
month: "2-digit",
year: "2-digit",
})
: payload
: new Date(payload).toLocaleDateString(undefined, {
day: "2-digit",
month: "2-digit",
year: "2-digit",
})
}
style={{
fontSize: "12px",
@@ -16,7 +16,6 @@ import {
} from "@tanstack/react-query-persist-client";
import { shouldNeverPersistQuery } from "@thirdweb-dev/react";
import { ConfigureNetworkModal } from "components/configure-networks/ConfigureNetworkModal";
import { DeployModalProvider } from "components/contract-components/contract-deploy-form/deploy-context-modal";
import { AppShell, type AppShellProps } from "components/layout/app-shell";
import { Onboarding as OnboardingModal } from "components/onboarding";
import { OpCreditsGrantedModalWrapper } from "components/onboarding/OpCreditsGrantedModalWrapper";
@@ -125,25 +124,23 @@ export const AppLayout: ComponentWithChildren<AppLayoutProps> = (props) => {
>
<Hydrate state={props.dehydratedState}>
<ErrorProvider>
<DeployModalProvider>
<AllChainsProvider>
<ChainsProvider>
<EVMContractInfoProvider value={props.contractInfo}>
<DashboardThirdwebProvider>
<SanctionedAddressesChecker>
<PosthogIdentifier />
<ConfigModal />
<AllChainsProvider>
<ChainsProvider>
<EVMContractInfoProvider value={props.contractInfo}>
<DashboardThirdwebProvider>
<SanctionedAddressesChecker>
<PosthogIdentifier />
<ConfigModal />
<OnboardingModal />
<OpCreditsGrantedModalWrapper />
<OnboardingModal />
<OpCreditsGrantedModalWrapper />
<AppShell {...props} />
</SanctionedAddressesChecker>
</DashboardThirdwebProvider>
</EVMContractInfoProvider>
</ChainsProvider>
</AllChainsProvider>
</DeployModalProvider>
<AppShell {...props} />
</SanctionedAddressesChecker>
</DashboardThirdwebProvider>
</EVMContractInfoProvider>
</ChainsProvider>
</AllChainsProvider>
</ErrorProvider>
</Hydrate>
</PersistQueryClientProvider>
@@ -25,11 +25,15 @@ import {
} from "@chakra-ui/react";
import { AiOutlineWarning } from "@react-icons/all-files/ai/AiOutlineWarning";
import { useQuery } from "@tanstack/react-query";
import { useSDK, useSDKChainId } from "@thirdweb-dev/react";
import { useSDKChainId } from "@thirdweb-dev/react";
import { FaucetButton } from "app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton";
import { GiftIcon } from "app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/icons/GiftIcon";
import type { ChainMetadataWithServices } from "app/(dashboard)/(chain)/types/chain";
import type {
ChainMetadataWithServices,
ChainServices,
} from "app/(dashboard)/(chain)/types/chain";
import { getSDKTheme } from "app/components/sdk-component-theme";
import { LOCAL_NODE_PKEY } from "constants/misc";
import { useTrack } from "hooks/analytics/useTrack";
import { useSupportedChain } from "hooks/chains/configureChains";
import { ExternalLinkIcon } from "lucide-react";
@@ -38,6 +42,7 @@ import Link from "next/link";
import { forwardRef, useCallback, useMemo, useRef, useState } from "react";
import { VscDebugDisconnect } from "react-icons/vsc";
import { toast } from "sonner";
import { prepareTransaction, sendTransaction, toWei } from "thirdweb";
import { type Chain, type ChainMetadata, localhost } from "thirdweb/chains";
import {
PayEmbed,
@@ -48,6 +53,7 @@ import {
useSwitchActiveWalletChain,
useWalletBalance,
} from "thirdweb/react";
import { privateKeyToAccount } from "thirdweb/wallets";
import { Button, type ButtonProps, Card, Heading, Text } from "tw-components";
import { THIRDWEB_API_HOST } from "../../constants/urls";
import { useV5DashboardChain } from "../../lib/v5-adapter";
@@ -74,7 +80,6 @@ export const MismatchButton = forwardRef<HTMLButtonElement, ButtonProps>(
const wallet = useActiveWallet();
const activeWalletChain = useActiveWalletChain();
const [dialog, setDialog] = useState<undefined | "no-funds" | "pay">();
const { theme } = useTheme();
const evmBalance = useWalletBalance({
address: account?.address,
@@ -119,6 +124,12 @@ export const MismatchButton = forwardRef<HTMLButtonElement, ButtonProps>(
loadingText={loadingText}
onClick={(e) => {
e.stopPropagation();
if (networksMismatch) {
eventRef.current = e;
return;
}
if (notEnoughBalance) {
trackEvent({
category: "no-funds",
@@ -129,11 +140,6 @@ export const MismatchButton = forwardRef<HTMLButtonElement, ButtonProps>(
return;
}
if (networksMismatch) {
eventRef.current = e;
return undefined;
}
if (onClick) {
return onClick(e);
}
@@ -196,7 +202,14 @@ export const MismatchButton = forwardRef<HTMLButtonElement, ButtonProps>(
{dialog === "no-funds" && (
<NoFundsDialogContent
chain={activeWalletChain}
openPayModal={() => setDialog("pay")}
openPayModal={() => {
trackEvent({
category: "pay",
action: "buy",
label: "attempt",
});
setDialog("pay");
}}
onCloseModal={() => setDialog(undefined)}
/>
)}
@@ -207,6 +220,37 @@ export const MismatchButton = forwardRef<HTMLButtonElement, ButtonProps>(
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
className="!w-auto"
payOptions={{
onPurchaseSuccess(info) {
if (
info.type === "crypto" &&
info.status.status !== "NOT_FOUND"
) {
trackEvent({
category: "pay",
action: "buy",
label: "success",
type: info.type,
chainId: info.status.quote.toToken.chainId,
tokenAddress: info.status.quote.toToken.tokenAddress,
amount: info.status.quote.toAmount,
});
}
if (
info.type === "fiat" &&
info.status.status !== "NOT_FOUND"
) {
trackEvent({
category: "pay",
action: "buy",
label: "success",
type: info.type,
chainId: info.status.quote.toToken.chainId,
tokenAddress: info.status.quote.toToken.tokenAddress,
amount: info.status.quote.estimatedToTokenAmount,
});
}
},
prefillBuy: {
chain: activeWalletChain,
},
@@ -229,20 +273,23 @@ function NoFundsDialogContent(props: {
onCloseModal: () => void;
}) {
const chainWithServiceInfoQuery = useQuery({
queryKey: ["chain", props.chain.id],
queryKey: ["chain-with-services", props.chain.id],
queryFn: async () => {
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/chains/${props.chain.id}?includeServices=true`,
);
const [chain, chainServices] = await Promise.all([
fetch(`${THIRDWEB_API_HOST}/v1/chains/${props.chain.id}`).then((res) =>
res.json(),
) as Promise<{ data: ChainMetadata }>,
fetch(`${THIRDWEB_API_HOST}/v1/chains/${props.chain.id}/services`).then(
(res) => res.json(),
) as Promise<{ data: ChainServices }>,
]);
const result = await res.json();
if (!result.data) {
throw new Error("Failed to fetch chain metadata with services");
}
return result.data as ChainMetadataWithServices;
return {
...chain.data,
services: chainServices.data.services,
} satisfies ChainMetadataWithServices;
},
enabled: !!props.chain.id,
});
return (
@@ -261,22 +308,23 @@ function NoFundsDialogContent(props: {
</DialogHeader>
{/* Get Funds content */}
{!chainWithServiceInfoQuery.data ? (
<div className="h-[300px] flex justify-center items-center">
<Spinner className="size-10" />
</div>
) : (
<div>
{props.chain.id === localhost.id && <GetLocalHostTestnetFunds />}
{props.chain.id !== localhost.id &&
chainWithServiceInfoQuery.data.testnet && (
<GetFundsFromFaucet chain={chainWithServiceInfoQuery.data} />
)}
{chainWithServiceInfoQuery.data.services.find(
(x) => x.enabled && x.service === "pay",
) && (
{props.chain.id === localhost.id ? (
// localhost case
<GetLocalHostTestnetFunds />
) : chainWithServiceInfoQuery.data.testnet ? (
// faucet case
<GetFundsFromFaucet chain={chainWithServiceInfoQuery.data} />
) : chainWithServiceInfoQuery.data.services.find(
(x) => x.enabled && x.service === "pay",
) ? (
// pay case
<ButtonShadcn
variant="primary"
className="w-full"
@@ -284,7 +332,8 @@ function NoFundsDialogContent(props: {
>
Buy Funds
</ButtonShadcn>
)}
) : // no funds options available
null}
</div>
)}
</div>
@@ -313,7 +362,7 @@ function GetFundsFromFaucet(props: {
chain: ChainMetadata;
}) {
// TODO - improvement for later -> estimate gas required for transaction, and use that as the amount to give
const amountToGive = "0.1";
const amountToGive = "0.01";
return (
<div className="border border-border px-4 py-6 rounded-lg flex justify-center">
@@ -439,16 +488,30 @@ const MismatchNotice: React.FC<{
};
const GetLocalHostTestnetFunds: React.FC = () => {
const sdk = useSDK();
const address = useActiveAccount()?.address;
const requestFunds = async () => {
if (sdk) {
try {
await sdk.wallet.requestFunds(10);
toast.success("Successfully got funds from localhost");
} catch (e) {
toast.error("Failed to get funds from localhost");
}
if (!address) {
return toast.error("No active account detected");
}
const faucet = privateKeyToAccount({
privateKey: LOCAL_NODE_PKEY,
client: thirdwebClient,
});
const transaction = prepareTransaction({
to: address,
chain: localhost,
client: thirdwebClient,
value: toWei("10"),
});
const promise = sendTransaction({
account: faucet,
transaction,
});
toast.promise(promise, {
error: "Failed to get funds from localhost",
success: "Successfully got funds from localhost",
loading: "Requesting funds",
});
};
return (
@@ -235,7 +235,7 @@ export const CmdKSearch: React.FC = () => {
<DialogHeader>
<DialogTitle className="mb-1">Search Contracts</DialogTitle>
<DialogDescription>
Search a contract by name or contract address accross all
Search a contract by name or contract address across all
blockchains
</DialogDescription>
</DialogHeader>
@@ -1,13 +1,11 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Alert,
AlertIcon,
Flex,
FormControl,
Input,
Radio,
RadioGroup,
SimpleGrid,
Stack,
} from "@chakra-ui/react";
import { ChainIcon } from "components/icons/ChainIcon";
import type { StoredChain } from "contexts/configured-chains";
@@ -16,7 +14,7 @@ import { useSupportedChainsNameRecord } from "hooks/chains/configureChains";
import { useRemoveChainModification } from "hooks/chains/useModifyChain";
import { getDashboardChainRpc } from "lib/rpc";
import { useForm } from "react-hook-form";
import { Button, FormErrorMessage, FormLabel, Text } from "tw-components";
import { FormErrorMessage, FormLabel } from "tw-components";
import { ChainIdInput } from "./Form/ChainIdInput";
import { ConfirmationPopover } from "./Form/ConfirmationPopover";
import { IconUpload } from "./Form/IconUpload";
@@ -217,11 +215,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
const submitBtn = (
<Button
background="bgBlack"
color="bgWhite"
_hover={{
background: "bgBlack",
}}
variant="primary"
type={disableSubmit ? "submit" : overwritingChain ? "button" : "submit"}
disabled={disableSubmit}
>
@@ -243,9 +237,6 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
<Input
autoComplete="off"
placeholder="e.g. My Network"
_placeholder={{
fontWeight: 500,
}}
type="text"
onChange={(e) => {
const value = e.target.value;
@@ -272,9 +263,9 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
{/* Slug URL */}
<NetworkIDInput form={form} disabled={!isFullyEditable} />
<Flex direction="column" gap={6} mt={6}>
<div className="flex flex-col gap-6 mt-6">
{/* Chain ID + Currency Symbol */}
<SimpleGrid columns={{ md: 2, base: 1 }} gap={4}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<ChainIdInput form={form} disabled={!isFullyEditable} />
{/* Currency Symbol */}
@@ -284,33 +275,29 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
disabled={!isFullyEditable}
placeholder="e.g. ETH"
autoComplete="off"
_placeholder={{
fontWeight: 500,
}}
type="text"
{...form.register("currencySymbol", { required: true })}
/>
</FormControl>
</SimpleGrid>
</div>
<SimpleGrid columns={{ md: 2, base: 1 }} gap={4}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Testnet / Mainnet */}
<FormControl>
<FormLabel display="flex">
<FormLabel className="!flex gap-1 items-center">
Network type
<TooltipBox
content={
<>
<Text color="heading" display="inline-block" mb={2}>
{" "}
<span className="mb-2 block">
The network type indicates whether it is intended for
production or testing.
</Text>
</span>
<Text color="heading">
<span className="block">
It{`'`}s only used for displaying network type on the
dashboard and does not affect functionality.
</Text>
</span>
</>
}
/>
@@ -324,10 +311,10 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
}}
value={form.watch("type")}
>
<Stack direction="row" gap={4} mt={3}>
<div className="flex gap-4 mt-3">
<Radio value="mainnet">Mainnet</Radio>
<Radio value="testnet">Testnet</Radio>
</Stack>
</div>
</RadioGroup>
</FormControl>
@@ -335,19 +322,19 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
<FormControl isInvalid={!!form.formState.errors.icon}>
<FormLabel>Icon</FormLabel>
<Flex gap={3} alignItems="center">
<div className="flex gap-1 items-center">
<ChainIcon size={24} ipfsSrc={form.watch("icon")} />
<IconUpload
onUpload={(uri) => {
form.setValue("icon", uri, { shouldDirty: true });
}}
/>
</Flex>
</div>
</FormControl>
</SimpleGrid>
</div>
{editingChain?.status === "deprecated" && (
<SimpleGrid columns={{ md: 2, base: 1 }} gap={4}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Active / Deprecated */}
<FormControl>
<FormLabel display="flex">Network status</FormLabel>
@@ -360,13 +347,13 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
}}
value={form.watch("status")}
>
<Stack direction="row" gap={4} mt={3}>
<div className="flex mt-3 gap-4">
<Radio value="active">Live</Radio>
<Radio value="deprecated">Deprecated</Radio>
</Stack>
</div>
</RadioGroup>
</FormControl>
</SimpleGrid>
</div>
)}
{/* RPC URL */}
@@ -382,11 +369,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
)}
{/* Buttons */}
<Flex
gap={4}
direction={{ base: "column", md: "row" }}
justifyContent={{ base: "center", md: "flex-end" }}
>
<div className="flex gap-4 flex-col md:flex-row justify-end">
{/* Add / Update Button */}
{overwritingChain && !disableSubmit ? (
<ConfirmationPopover
@@ -417,8 +400,8 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
Reset
</Button>
)}
</Flex>
</Flex>
</div>
</div>
</form>
);
};
@@ -1,10 +1,4 @@
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalOverlay,
} from "@chakra-ui/react";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import type { StoredChain } from "contexts/configured-chains";
import { useSetEditChain } from "hooks/networkConfigModal";
import { ConfigureNetworks } from "./ConfigureNetworks";
@@ -24,31 +18,26 @@ export const ConfigureNetworkModal: React.FC<AddNetworkModalProps> = (
};
return (
<Modal isOpen={true} onClose={onModalClose} isCentered>
<ModalOverlay />
<ModalContent
borderRadius="xl"
overflow="hidden"
w="500px"
mt={{ base: 8, md: 16 }}
mb={{ base: 0, md: 16 }}
maxW={{ base: "100vw", md: "calc(100vw - 40px)" }}
<Dialog
open={true}
onOpenChange={(v) => {
if (!v) {
onModalClose();
}
}}
>
<DialogContent
className="p-0 z-[10001] max-w-[480px]"
dialogOverlayClassName="z-[10000]"
>
<ModalCloseButton />
<ModalBody
p={0}
_dark={{ background: "backgroundBody" }}
_light={{ background: "white" }}
>
<ConfigureNetworks
onNetworkAdded={(chain) => {
props.onNetworkAdded?.(chain);
onModalClose();
}}
onNetworkConfigured={onModalClose}
/>
</ModalBody>
</ModalContent>
</Modal>
<ConfigureNetworks
onNetworkAdded={(chain) => {
props.onNetworkAdded?.(chain);
onModalClose();
}}
onNetworkConfigured={onModalClose}
/>
</DialogContent>
</Dialog>
);
};
@@ -1,10 +1,9 @@
import { Box, useToast } from "@chakra-ui/react";
import type { StoredChain } from "contexts/configured-chains";
import { useTrack } from "hooks/analytics/useTrack";
import { useAddRecentlyUsedChainId } from "hooks/chains/recentlyUsedChains";
import { useModifyChain } from "hooks/chains/useModifyChain";
import { useEditChain } from "hooks/networkConfigModal";
import { Heading } from "tw-components";
import { toast } from "sonner";
import { ConfigureNetworkForm } from "./ConfigureNetworkForm";
function useChainConfigTrack() {
@@ -31,17 +30,6 @@ export const ConfigureNetworks: React.FC<ConfigureNetworksProps> = (props) => {
const modifyChain = useModifyChain();
const editChain = useEditChain();
const toast = useToast();
const successToast = (message: string) => {
toast({
title: message,
status: "success",
duration: 3000,
isClosable: true,
});
};
const handleSubmit = (chain: StoredChain) => {
modifyChain(chain);
addRecentlyUsedChainId(chain.chainId);
@@ -52,44 +40,29 @@ export const ConfigureNetworks: React.FC<ConfigureNetworksProps> = (props) => {
}
trackChainConfig("add", chain);
successToast("Network Added Successfully");
toast.success("Network Added Successfully");
} else {
if (props.onNetworkConfigured) {
props.onNetworkConfigured(chain);
}
trackChainConfig("update", chain);
successToast("Network Updated Successfully");
toast.success("Network Updated Successfully");
}
};
return (
<Box
p={{ base: 4, md: 8 }}
minH="600px"
borderTop={{ md: "none", base: "1px solid" }}
marginTop={{ base: 6, md: 0 }}
borderColor="inputBg"
>
<Heading
as={"h3"}
size="label.xl"
mb={8}
display="flex"
gap={2}
alignItems="center"
>
<div className="p-6">
<h3 className="text-2xl font-semibold tracking-tight mb-5">
{editChain ? "Edit Network" : "Add Custom Network"}
</Heading>
</h3>
{/* Modify the given chain */}
{editChain && (
<Box mt={9}>
<ConfigureNetworkForm
editingChain={editChain}
onSubmit={handleSubmit}
/>
</Box>
<ConfigureNetworkForm
editingChain={editChain}
onSubmit={handleSubmit}
/>
)}
{/* Custom chain */}
@@ -100,6 +73,6 @@ export const ConfigureNetworks: React.FC<ConfigureNetworksProps> = (props) => {
onSubmit={handleSubmit}
/>
)}
</Box>
</div>
);
};
@@ -1,11 +1,11 @@
import { FormControl, Input } from "@chakra-ui/react";
import { Input } from "@/components/ui/input";
import { FormControl } from "@chakra-ui/react";
import type { UseFormReturn } from "react-hook-form";
import { FormLabel } from "tw-components";
import type { NetworkConfigFormData } from "../ConfigureNetworkForm";
export const ChainIdInput: React.FC<{
// biome-ignore lint/suspicious/noExplicitAny: FIXME
form: UseFormReturn<NetworkConfigFormData, any>;
form: UseFormReturn<NetworkConfigFormData>;
disabled: boolean;
}> = ({ form, disabled }) => {
return (
@@ -18,9 +18,6 @@ export const ChainIdInput: React.FC<{
disabled={disabled}
placeholder="e.g. 152"
autoComplete="off"
_placeholder={{
fontWeight: 500,
}}
onKeyDown={(e) => {
// prevent typing e, +, -
if (e.key === "e" || e.key === "+" || e.key === "-") {
@@ -1,23 +1,22 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { useDashboardStorageUpload } from "@3rdweb-sdk/react/hooks/useDashboardStorageUpload";
import { Icon } from "@chakra-ui/react";
import { useQueryClient } from "@tanstack/react-query";
import { FileInput } from "components/shared/FileInput";
import { PINNED_FILES_QUERY_KEY_ROOT } from "components/storage/your-files";
import { useErrorHandler } from "contexts/error-handler";
import { FiUpload } from "react-icons/fi";
import { Button } from "tw-components";
import { toast } from "sonner";
export const IconUpload: React.FC<{ onUpload: (url: string) => void }> = ({
onUpload,
}) => {
const errorHandler = useErrorHandler();
const storageUpload = useDashboardStorageUpload();
const queryClient = useQueryClient();
const handleIconUpload = (file: File) => {
// if file size is larger than 5000kB, show error
if (file.size > 5000 * 1024) {
errorHandler.onError("Icon image can not be larger than 5MB");
toast.error("Icon image can not be larger than 5MB");
return;
}
@@ -28,21 +27,20 @@ export const IconUpload: React.FC<{ onUpload: (url: string) => void }> = ({
queryClient.invalidateQueries([PINNED_FILES_QUERY_KEY_ROOT]);
},
onError(error) {
errorHandler.onError(error, "Failed to upload file");
console.error(error);
toast.error("Failed to upload file");
},
});
};
return (
<FileInput setValue={handleIconUpload} accept={{ "image/*": [] }}>
<Button
bg="inputBg"
size="sm"
variant="solid"
aria-label="Upload to IPFS"
rightIcon={<Icon as={FiUpload} />}
isLoading={storageUpload.isLoading}
>
<Button variant="ghost" className="gap-2 px-2" size="sm">
{storageUpload.isLoading ? (
<Spinner className="size-4" />
) : (
<FiUpload className="size-4" />
)}
Upload Icon
</Button>
</FileInput>
@@ -1,19 +1,13 @@
import { FormControl, Input } from "@chakra-ui/react";
import { Input } from "@/components/ui/input";
import { FormControl } from "@chakra-ui/react";
import { useAllChainsData } from "hooks/chains/allChains";
import type { UseFormReturn } from "react-hook-form";
import {
CodeBlock,
FormErrorMessage,
FormLabel,
Heading,
Text,
} from "tw-components";
import { FormErrorMessage, FormLabel } from "tw-components";
import type { NetworkConfigFormData } from "../ConfigureNetworkForm";
import { TooltipBox } from "./TooltipBox";
export const NetworkIDInput: React.FC<{
// biome-ignore lint/suspicious/noExplicitAny: FIXME
form: UseFormReturn<NetworkConfigFormData, any>;
form: UseFormReturn<NetworkConfigFormData>;
disabled?: boolean;
}> = ({ form, disabled }) => {
const slug = form.watch("slug");
@@ -25,23 +19,16 @@ export const NetworkIDInput: React.FC<{
mt={6}
isInvalid={form.formState.errors.slug?.type === "taken"}
>
<FormLabel display="flex">
<FormLabel className="!flex items-center gap-1">
Network ID
<TooltipBox
content={
<>
<Text color="heading" mb={4}>
Network ID is used to identify the network in the URL{" "}
</Text>
<Heading fontSize="14px" mb={3}>
Example
</Heading>
<CodeBlock
fontSize="14px"
code="thirdweb.com/<network-id>/<contract-address>"
language="bash"
canCopy={false}
/>
Network ID is used to identify the network in the URL
<p className="font-semibold mt-2 mb-1">Example</p>
<p className="text-link-foreground text-xs">
{"thirdweb.com/<network-id>/<contract-address>"}
</p>
</>
}
/>
@@ -50,9 +37,6 @@ export const NetworkIDInput: React.FC<{
disabled={disabled}
autoComplete="off"
placeholder="e.g. ethereum"
_placeholder={{
fontWeight: 500,
}}
onKeyDown={(e) => {
// only allow alphanumeric characters and dashes
if (!/^[a-z0-9-]*$/i.test(e.key)) {
@@ -1,4 +1,5 @@
import { Alert, AlertIcon, FormControl, Input } from "@chakra-ui/react";
import { Input } from "@/components/ui/input";
import { Alert, AlertIcon, FormControl } from "@chakra-ui/react";
import type { UseFormReturn } from "react-hook-form";
import { IoWarning } from "react-icons/io5";
import { FormErrorMessage, FormLabel } from "tw-components";
@@ -28,15 +29,7 @@ export const RpcInput: React.FC<{
isDisabled={form.watch("status") !== "active"}
>
<FormLabel>RPC URL</FormLabel>
<Input
autoComplete="off"
placeholder="https://"
type="url"
_placeholder={{
fontWeight: 500,
}}
{...reg}
/>
<Input autoComplete="off" placeholder="https://" type="url" {...reg} />
<FormErrorMessage fontSize="12px">Invalid RPC URL</FormErrorMessage>
@@ -1,34 +1,12 @@
import { Flex, Icon, Tooltip } from "@chakra-ui/react";
import { BsQuestionCircle } from "react-icons/bs";
import { Card } from "tw-components";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { CircleHelpIcon } from "lucide-react";
export const TooltipBox: React.FC<{
content: React.ReactNode;
iconColor?: string;
}> = ({ content, iconColor = "accent.600" }) => {
}> = ({ content }) => {
return (
<Tooltip
placement="top-start"
borderRadius="md"
bg="transparent"
boxShadow="none"
p={4}
minW={{ md: "450px" }}
label={
<Card py={2} px={4} bgColor="backgroundCardHighlight">
{content}
</Card>
}
>
<Flex alignItems="center" justifyContent="center">
<Icon
ml={2}
mr={1}
as={BsQuestionCircle}
color={iconColor}
boxSize={4}
/>
</Flex>
</Tooltip>
<ToolTipLabel label={<div>{content}</div>}>
<CircleHelpIcon className="size-4 text-muted-foreground" />
</ToolTipLabel>
);
};
@@ -0,0 +1,16 @@
export function Fieldset(props: {
legend: string;
children: React.ReactNode;
}) {
return (
<fieldset>
<legend className="text-2xl mb-3 font-semibold tracking-tight">
{props.legend}
</legend>
<div className="border rounded-lg border-border p-4 md:p-6">
{props.children}
</div>
</fieldset>
);
}
@@ -4,6 +4,7 @@ import { FileInput } from "components/shared/FileInput";
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
import { FormErrorMessage, FormLabel } from "tw-components";
import type { useContractPublishMetadataFromURI } from "../hooks";
import { Fieldset } from "./common";
import type { CustomContractDeploymentForm } from "./custom-contract";
interface ContractMetadataFieldsetProps {
@@ -15,12 +16,8 @@ export const ContractMetadataFieldset: React.FC<
ContractMetadataFieldsetProps
> = ({ form, metadata }) => {
return (
<fieldset>
<legend className="text-2xl font-semibold tracking-tight mb-3">
Contract Metadata
</legend>
<div className="border border-border rounded-lg p-4 md:p-6 flex flex-col md:grid gap-6 md:grid-cols-[270px_1fr]">
<Fieldset legend="Contract Metadata">
<div className="flex flex-col md:grid gap-6 md:grid-cols-[270px_1fr]">
<div>
<FormControl
isDisabled={!metadata.isSuccess}
@@ -119,6 +116,6 @@ export const ContractMetadataFieldset: React.FC<
</FormControl>
</div>
</div>
</fieldset>
</Fieldset>
);
};
@@ -1,4 +1,5 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
@@ -20,6 +21,8 @@ import { useTrack } from "hooks/analytics/useTrack";
import { useSupportedChain } from "hooks/chains/configureChains";
import { useTxNotifications } from "hooks/useTxNotifications";
import { replaceTemplateValues } from "lib/deployment/template-values";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useMemo } from "react";
import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
@@ -39,13 +42,18 @@ import {
useFunctionParamsFromABI,
useTransactionsForDeploy,
} from "../hooks";
import { Fieldset } from "./common";
import { ContractMetadataFieldset } from "./contract-metadata-fieldset";
import {
ModularContractDefaultExtensionsFieldset,
DeployStatusModal,
useDeployStatusModal,
} from "./deploy-context-modal";
import {
ModularContractDefaultModulesFieldset,
showPrimarySaleFiedset,
showRoyaltyFieldset,
useModularContractsDefaultExtensionsInstallParams,
} from "./modular-contract-default-extensions-fieldset";
useModularContractsDefaultModulesInstallParams,
} from "./modular-contract-default-modules-fieldset";
import { Param } from "./param";
import { PlatformFeeFieldset } from "./platform-fee-fieldset";
import { PrimarySaleFieldset } from "./primary-sale-fieldset";
@@ -69,7 +77,7 @@ export type CustomContractDeploymentFormData = {
saltForCreate2: string;
signerAsSalt: boolean;
deployParams: Record<string, string>;
modularContractDefaultExtensionsInstallParams: Record<string, string>[];
modularContractDefaultModulesInstallParams: Record<string, string>[];
contractMetadata?: {
name: string;
description: string;
@@ -82,6 +90,21 @@ export type CustomContractDeploymentFormData = {
export type CustomContractDeploymentForm =
UseFormReturn<CustomContractDeploymentFormData>;
const rewardParamsSet = new Set([
"_rewardToken",
"_stakingToken",
"_timeUnit",
"_rewardsPerUnitTime",
]);
const voteParamsSet = new Set([
"_token",
"_initialVotingDelay",
"_initialVotingPeriod",
"_initialProposalThreshold",
"_initialVoteQuorumFraction",
]);
const CustomContractForm: React.FC<CustomContractFormProps> = ({
ipfsHash,
version,
@@ -137,11 +160,11 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
fullPublishMetadata.data?.deployType === "customFactory";
const isModular = fullPublishMetadata.data?.routerType === "modular";
const defaultExtensions = fullPublishMetadata.data?.defaultExtensions;
const defaultModules = fullPublishMetadata.data?.defaultModules;
const modularContractDefaultExtensionsInstallParams =
useModularContractsDefaultExtensionsInstallParams({
defaultExtensions,
const modularContractDefaultModulesInstallParams =
useModularContractsDefaultModulesInstallParams({
defaultModules,
isQueryEnabled: isModular,
});
@@ -196,11 +219,11 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
signerAsSalt: true,
deployParams: parsedDeployParams,
recipients: [{ address: connectedWallet || "", sharesBps: 10000 }],
// set default values for modular contract extensions with custom components
modularContractDefaultExtensionsInstallParams:
// set default values for modular contract modules with custom components
modularContractDefaultModulesInstallParams:
(activeAccount &&
isTWPublisher &&
modularContractDefaultExtensionsInstallParams.data?.map((ext) => {
modularContractDefaultModulesInstallParams.data?.map((ext) => {
const paramNames = ext.params.map((param) => param.name);
const returnVal: Record<string, string> = {};
@@ -222,7 +245,7 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
parsedDeployParams,
isAccountFactory,
connectedWallet,
modularContractDefaultExtensionsInstallParams.data,
modularContractDefaultModulesInstallParams.data,
isTWPublisher,
activeAccount,
],
@@ -243,6 +266,7 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
"_royaltyBps" in formDeployParams &&
"_royaltyRecipient" in formDeployParams;
const hasPrimarySale = "_saleRecipient" in formDeployParams;
const hasPrimarySaleRecipient = "_primarySaleRecipient" in formDeployParams;
const hasPlatformFee =
"_platformFeeBps" in formDeployParams &&
"_platformFeeRecipient" in formDeployParams;
@@ -256,6 +280,17 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
"_token" in formDeployParams;
const hasTrustedForwarders = "_trustedForwarders" in formDeployParams;
const rewardDeployParams = Object.keys(formDeployParams).filter((paramKey) =>
rewardParamsSet.has(paramKey),
);
const voteDeployParams = Object.keys(formDeployParams).filter((paramKey) =>
voteParamsSet.has(paramKey),
);
const showRewardsSection = rewardDeployParams.length === rewardParamsSet.size;
const showVoteSection = voteDeployParams.length === voteParamsSet.size;
const shouldHide = (paramKey: string) => {
if (isAccountFactory) {
return false;
@@ -268,6 +303,9 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
(hasRoyalty &&
(paramKey === "_royaltyBps" || paramKey === "_royaltyRecipient")) ||
(hasPrimarySale && paramKey === "_saleRecipient") ||
(hasPrimarySaleRecipient && paramKey === "_primarySaleRecipient") ||
(showRewardsSection && rewardParamsSet.has(paramKey)) ||
(showVoteSection && voteParamsSet.has(paramKey)) ||
(hasPlatformFee &&
(paramKey === "_platformFeeBps" ||
paramKey === "_platformFeeRecipient")) ||
@@ -286,18 +324,18 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
(extension) => extension.name === "ERC721SharedMetadata",
);
const deploy = useCustomContractDeployMutation(
const deployStatusModal = useDeployStatusModal();
const deploy = useCustomContractDeployMutation({
ipfsHash,
version,
isImplementationDeploy,
{
hasContractURI,
hasRoyalty,
isSplit,
isVote,
isErc721SharedMetadadata,
},
);
forceDirectDeploy: isImplementationDeploy,
hasContractURI,
hasRoyalty,
isSplit,
isVote,
isErc721SharedMetadadata,
deployStatusModal,
});
const router = useRouter();
const { onSuccess, onError } = useTxNotifications(
@@ -375,17 +413,17 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
const deployParams = { ...formData.deployParams };
// if Modular contract has extensions
if (isModular && modularContractDefaultExtensionsInstallParams.data) {
const extensionInstallData: string[] =
modularContractDefaultExtensionsInstallParams.data.map(
// if Modular contract has modules
if (isModular && modularContractDefaultModulesInstallParams.data) {
const moduleInstallData: string[] =
modularContractDefaultModulesInstallParams.data.map(
(ext, extIndex) => {
return encodeAbiParameters(
// param name+type []
ext.params.map((p) => ({ name: p.name, type: p.type })),
// value []
Object.values(
formData.modularContractDefaultExtensionsInstallParams[
formData.modularContractDefaultModulesInstallParams[
extIndex
] || {},
),
@@ -393,8 +431,7 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
},
);
deployParams._extensionInstallData =
JSON.stringify(extensionInstallData);
deployParams._moduleInstallData = JSON.stringify(moduleInstallData);
}
deploy.mutate(
@@ -450,7 +487,7 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
networkInfo,
`Network not found for chainId ${selectedChain}`,
);
router.replace(
deployStatusModal.setViewContractLink(
`/${networkInfo.slug}/${deployedContractAddress}`,
);
}
@@ -479,81 +516,140 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
/>
)}
{(hasRoyalty || hasPrimarySale || hasPlatformFee) && (
<fieldset>
<legend className="text-2xl font-semibold tracking-tight mb-3">
Fee Distribution
</legend>
<div className="border rounded-lg border-border p-4 md:p-6 flex flex-col gap-6">
{/* Primary Sale */}
{hasPrimarySale && (
<PrimarySaleFieldset
isInvalid={
!!form.getFieldState(
"deployParams._saleRecipient",
form.formState,
).error
}
register={form.register("deployParams._saleRecipient")}
errorMessage={
form.getFieldState(
"deployParams._saleRecipient",
form.formState,
).error?.message
}
/>
)}
{/* Royalty */}
{hasRoyalty && (
<RoyaltyFieldset
royaltyRecipient={{
isInvalid: !!form.getFieldState(
"deployParams._royaltyRecipient",
form.formState,
).error,
register: form.register(
"deployParams._royaltyRecipient",
),
errorMessage: form.getFieldState(
"deployParams._royaltyRecipient",
form.formState,
).error?.message,
}}
royaltyBps={{
value: form.watch("deployParams._royaltyBps"),
isInvalid: !!form.getFieldState(
"deployParams._royaltyBps",
form.formState,
).error,
setValue: (v) => {
form.setValue("deployParams._royaltyBps", v, {
shouldTouch: true,
});
},
errorMessage: form.getFieldState(
"deployParams._royaltyBps",
form.formState,
).error?.message,
}}
/>
)}
{hasPlatformFee && <PlatformFeeFieldset form={form} />}
</div>
</fieldset>
{/* Primary Sale */}
{hasPrimarySale && (
<PrimarySaleFieldset
isInvalid={
!!form.getFieldState(
"deployParams._saleRecipient",
form.formState,
).error
}
register={form.register("deployParams._saleRecipient")}
errorMessage={
form.getFieldState(
"deployParams._saleRecipient",
form.formState,
).error?.message
}
/>
)}
{hasPrimarySaleRecipient && (
<PrimarySaleFieldset
isInvalid={
!!form.getFieldState(
"deployParams._primarySaleRecipient",
form.formState,
).error
}
register={form.register("deployParams._primarySaleRecipient")}
errorMessage={
form.getFieldState(
"deployParams._primarySaleRecipient",
form.formState,
).error?.message
}
/>
)}
{/* Royalty */}
{hasRoyalty && (
<RoyaltyFieldset
royaltyRecipient={{
isInvalid: !!form.getFieldState(
"deployParams._royaltyRecipient",
form.formState,
).error,
register: form.register("deployParams._royaltyRecipient"),
errorMessage: form.getFieldState(
"deployParams._royaltyRecipient",
form.formState,
).error?.message,
}}
royaltyBps={{
value: form.watch("deployParams._royaltyBps"),
isInvalid: !!form.getFieldState(
"deployParams._royaltyBps",
form.formState,
).error,
setValue: (v) => {
form.setValue("deployParams._royaltyBps", v, {
shouldTouch: true,
});
},
errorMessage: form.getFieldState(
"deployParams._royaltyBps",
form.formState,
).error?.message,
}}
/>
)}
{hasPlatformFee && <PlatformFeeFieldset form={form} />}
{isSplit && <SplitFieldset form={form} />}
{hasTrustedForwarders && <TrustedForwardersFieldset form={form} />}
{/* for StakeERC721 */}
{showRewardsSection && (
<Fieldset legend="Reward Parameters">
<div className="flex flex-col gap-6">
{rewardDeployParams.map((paramKey) => {
const deployParam = deployParams.find(
(p) => p.name === paramKey,
);
const contructorParams =
fullPublishMetadata.data?.constructorParams || {};
const extraMetadataParam = contructorParams[paramKey];
return (
<Param
key={paramKey}
paramKey={paramKey}
deployParam={deployParam}
extraMetadataParam={extraMetadataParam}
/>
);
})}
</div>
</Fieldset>
)}
{/* for Vote */}
{showVoteSection && (
<Fieldset legend="Voting Parameters">
<div className="flex flex-col gap-6">
{voteDeployParams.map((paramKey) => {
const deployParam = deployParams.find(
(p) => p.name === paramKey,
);
const contructorParams =
fullPublishMetadata.data?.constructorParams || {};
const extraMetadataParam = contructorParams[paramKey];
return (
<Param
key={paramKey}
paramKey={paramKey}
deployParam={deployParam}
extraMetadataParam={extraMetadataParam}
/>
);
})}
</div>
</Fieldset>
)}
{Object.keys(formDeployParams)
.filter((paramName) => {
if (
isModular &&
(paramName === "_extensionInstallData" ||
paramName === "_extensions")
(paramName === "_moduleInstallData" ||
paramName === "_modules")
) {
return false;
}
@@ -584,12 +680,10 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
{isModular && (
<>
{modularContractDefaultExtensionsInstallParams.data ? (
<ModularContractDefaultExtensionsFieldset
{modularContractDefaultModulesInstallParams.data ? (
<ModularContractDefaultModulesFieldset
form={form}
extensions={
modularContractDefaultExtensionsInstallParams.data
}
modules={modularContractDefaultModulesInstallParams.data}
isTWPublisher={isTWPublisher}
/>
) : (
@@ -626,144 +720,160 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
</>
)}
{fullPublishMetadata.data?.deployType === "standard" && (
<>
{/* Deterministic deploy */}
<CheckboxWithLabel>
<Checkbox
{...form.register("deployDeterministic")}
checked={form.watch("deployDeterministic")}
onCheckedChange={(c) =>
form.setValue("deployDeterministic", !!c)
}
/>
<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
</span>
<FiHelpCircle className="size-4" />
</div>
</ToolTipLabel>
</CheckboxWithLabel>
<Fieldset legend="Deploy Options">
<div className="flex flex-col gap-6">
{/* Chain */}
<FormControl isRequired>
<FormLabel>Chain</FormLabel>
{/* Optional Salt Input */}
{isCreate2Deployment && (
<FormControl>
<Flex alignItems="center" my={1}>
<FormLabel mb={0} flex="1" display="flex">
<Flex alignItems="baseline" gap={1}>
Optional Salt Input
<Text size="label.sm">(saltForCreate2)</Text>
</Flex>
</FormLabel>
<FormHelperText mt={0}>string</FormHelperText>
</Flex>
<SolidityInput
defaultValue={""}
solidityType={"string"}
{...form.register("saltForCreate2")}
/>
<div className="h-2" />
<p className="text-muted-foreground text-sm mb-3">
Select a network to deploy this contract on. We recommend
starting with a testnet.{" "}
<TrackedLinkTW
href="/chainlist"
category="deploy"
label="chainlist"
target="_blank"
className="text-link-foreground hover:text-foreground"
>
View all chains
</TrackedLinkTW>
</p>
<div className="flex flex-col gap-3">
<div className="flex flex-col md:flex-row gap-4">
<NetworkSelectorButton
isDisabled={
isImplementationDeploy ||
deploy.isLoading ||
!compilerMetadata.isSuccess
}
onSwitchChain={(chain) => {
onChainSelect(chain.chainId);
}}
networksEnabled={
fullPublishMetadata.data?.name === "AccountFactory" ||
fullPublishMetadata.data?.networksForDeployment
?.allNetworks ||
!fullPublishMetadata.data?.networksForDeployment
? undefined
: fullPublishMetadata.data?.networksForDeployment
?.networksEnabled
}
/>
<Button asChild variant="outline">
<Link href="/chainlist" className="gap-2">
View all chains <ExternalLinkIcon className="size-4" />
</Link>
</Button>
</div>
<DeprecatedAlert chain={networkInfo} />
</div>
</FormControl>
{fullPublishMetadata.data?.deployType === "standard" && (
<>
{/* Deterministic deploy */}
<CheckboxWithLabel>
<Checkbox
{...form.register("signerAsSalt")}
checked={form.watch("signerAsSalt")}
onCheckedChange={(c) => form.setValue("signerAsSalt", !!c)}
{...form.register("deployDeterministic")}
checked={form.watch("deployDeterministic")}
onCheckedChange={(c) =>
form.setValue("deployDeterministic", !!c)
}
/>
<span className="text-sm">
Include deployer wallet address in salt (recommended)
</span>
<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
</span>
<FiHelpCircle className="size-4" />
</div>
</ToolTipLabel>
</CheckboxWithLabel>
</FormControl>
{/* Optional Salt Input */}
{isCreate2Deployment && (
<FormControl>
<Flex alignItems="center" my={1}>
<FormLabel mb={0} flex="1" display="flex">
<Flex alignItems="baseline" gap={1}>
Optional Salt Input
<Text size="label.sm">(saltForCreate2)</Text>
</Flex>
</FormLabel>
<FormHelperText mt={0}>string</FormHelperText>
</Flex>
<SolidityInput
defaultValue={""}
solidityType={"string"}
{...form.register("saltForCreate2")}
/>
<div className="h-2" />
<CheckboxWithLabel>
<Checkbox
{...form.register("signerAsSalt")}
checked={form.watch("signerAsSalt")}
onCheckedChange={(c) =>
form.setValue("signerAsSalt", !!c)
}
/>
<span className="text-sm">
Include deployer wallet address in salt (recommended)
</span>
</CheckboxWithLabel>
</FormControl>
)}
</>
)}
</>
)}
{/* Chain */}
<fieldset>
<legend className="text-2xl font-semibold tracking-tight mb-2">
Chain
</legend>
{/* Import Enable/Disable */}
<CheckboxWithLabel>
<Checkbox
{...form.register("addToDashboard")}
checked={form.watch("addToDashboard")}
onCheckedChange={(checked) =>
form.setValue("addToDashboard", !!checked)
}
/>
<span>
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>
</span>
</CheckboxWithLabel>
<p className="text-muted-foreground text-sm mb-3">
Select a network to deploy this contract on. We recommend starting
with a testnet.{" "}
<TrackedLinkTW
href="/chainlist"
category="deploy"
label="chainlist"
target="_blank"
className="text-link-foreground hover:text-foreground"
>
View all chains
</TrackedLinkTW>
</p>
<div className="flex flex-col gap-3">
<NetworkSelectorButton
isDisabled={
isImplementationDeploy ||
deploy.isLoading ||
!compilerMetadata.isSuccess
}
onSwitchChain={(chain) => {
onChainSelect(chain.chainId);
}}
networksEnabled={
fullPublishMetadata.data?.name === "AccountFactory" ||
fullPublishMetadata.data?.networksForDeployment?.allNetworks ||
!fullPublishMetadata.data?.networksForDeployment
? undefined
: fullPublishMetadata.data?.networksForDeployment
?.networksEnabled
}
/>
<DeprecatedAlert chain={networkInfo} />
{/* Depoy */}
<div className="flex md:justify-end">
<TransactionButton
onChainSelect={onChainSelect}
upsellTestnet
flexShrink={0}
type="submit"
form="custom-contract-form"
isLoading={deploy.isLoading}
isDisabled={!compilerMetadata.isSuccess || !selectedChain}
colorScheme="blue"
transactionCount={transactionCount}
className="w-full md:w-auto"
>
Deploy Now
</TransactionButton>
</div>
</div>
</fieldset>
{/* Import Enable/Disable */}
<CheckboxWithLabel>
<Checkbox
{...form.register("addToDashboard")}
checked={form.watch("addToDashboard")}
onCheckedChange={(checked) =>
form.setValue("addToDashboard", !!checked)
}
/>
<span>
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>
</span>
</CheckboxWithLabel>
{/* Depoy */}
<div className="flex justify-end">
<TransactionButton
onChainSelect={onChainSelect}
upsellTestnet
flexShrink={0}
type="submit"
form="custom-contract-form"
isLoading={deploy.isLoading}
isDisabled={!compilerMetadata.isSuccess || !selectedChain}
colorScheme="blue"
transactionCount={transactionCount}
>
Deploy Now
</TransactionButton>
</div>
</Fieldset>
</Flex>
<DeployStatusModal deployStatusModal={deployStatusModal} />
</FormProvider>
);
};
@@ -1,192 +1,204 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Center,
Flex,
Icon,
Modal,
ModalContent,
ModalOverlay,
Spinner,
useDisclosure,
} from "@chakra-ui/react";
import { createContext, useContext, useState } from "react";
import { FiActivity, FiCheck } from "react-icons/fi";
import invariant from "tiny-invariant";
import { Card, Heading, Text } from "tw-components";
import type { ComponentWithChildren } from "types/component-with-children";
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { CircleCheck, CircleIcon } from "lucide-react";
import Link from "next/link";
import { useCallback, useState } from "react";
interface DeployModalStep {
title: string;
description: string | JSX.Element;
action?:
| {
type: "link";
label: string;
href: string;
}
| {
type: "button";
label: string;
onClick: () => void;
};
}
export type DeployModalStep = {
type: "deploy" | "setNFTMetadata" | "import";
signatureCount: number;
};
interface DeployModalContext {
nextStep: () => void;
export type DeployStatusModal = {
open: (steps: DeployModalStep[]) => void;
close: () => void;
_inProvider: true;
}
const DeployModalContext = createContext<DeployModalContext>(
{} as DeployModalContext,
);
export const DeployModalProvider: ComponentWithChildren = ({ children }) => {
const modalState = useDisclosure();
nextStep: () => void;
isModalOpen: boolean;
steps: DeployModalStep[];
activeStep: number;
setIsModalOpen: (isOpen: boolean) => void;
viewContractLink: string | undefined;
setViewContractLink: (link: string) => void;
};
export function useDeployStatusModal(): DeployStatusModal {
const [isModalOpen, setIsModalOpen] = useState(false);
const [steps, setSteps] = useState<DeployModalStep[]>([]);
const [activeStep, setActiveStep] = useState(0);
return (
<DeployModalContext.Provider
value={{
open: (steps_) => {
setSteps(steps_);
modalState.onOpen();
},
close: () => {
modalState.onClose();
setSteps([]);
setActiveStep(0);
},
nextStep: () => {
setActiveStep((currStep) => {
return currStep + 1;
});
},
const [viewContractLink, setViewContractLink] = useState<
string | undefined
>();
_inProvider: true,
}}
>
{children}
<Modal
closeOnEsc
closeOnOverlayClick
// can only be closed by the context
onClose={() => undefined}
isOpen={modalState.isOpen}
isCentered
const open = useCallback((steps_: DeployModalStep[]) => {
setSteps(steps_);
setIsModalOpen(true);
}, []);
const close = useCallback(() => {
setIsModalOpen(false);
setSteps([]);
setActiveStep(0);
}, []);
const nextStep = useCallback(() => {
setActiveStep((currStep) => {
return currStep + 1;
});
}, []);
return {
open,
close,
nextStep,
isModalOpen,
steps,
activeStep,
setIsModalOpen,
viewContractLink,
setViewContractLink,
};
}
export function DeployStatusModal(props: {
deployStatusModal: DeployStatusModal;
}) {
const { steps, activeStep, isModalOpen, viewContractLink, setIsModalOpen } =
props.deployStatusModal;
// DO not close modal on outside click
return (
<Dialog open={isModalOpen}>
<DialogContent
dialogCloseClassName="hidden"
className="max-w-[480px] p-0 gap-0"
>
<ModalOverlay />
<ModalContent as={Card} borderRadius="xl">
<Flex flexDir="column" gap={4}>
<Heading size="subtitle.sm">Deploy Status</Heading>
<div className="p-6 flex flex-col gap-6">
<DialogHeader>
<DialogTitle className="text-2xl font-semibold tracking-tight">
Deploy Status
</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
{steps.map((step, i) => {
const isActive = i === activeStep;
const hasCompleted = i < activeStep;
return (
<DeployModalStep
key={step.title}
{...step}
<RenderDeployModalStep
key={step.type}
step={step}
isActive={isActive}
hasCompleted={hasCompleted}
/>
);
})}
</Flex>
</ModalContent>
</Modal>
</DeployModalContext.Provider>
);
};
</div>
</div>
export function useDeployContextModal() {
const { _inProvider, ...context } = useContext(DeployModalContext);
invariant(
_inProvider,
"useDeployContextModal must be used within DeployModalProvider",
{viewContractLink && (
<div className="flex justify-between mt-2 border-border border-t p-6 gap-4">
<Button variant="outline" onClick={() => setIsModalOpen(false)}>
Close
</Button>
<Button variant="primary" asChild>
<Link href={viewContractLink}>View Contract</Link>
</Button>
</div>
)}
</DialogContent>
</Dialog>
);
return context;
}
interface DeployModalStepProps extends DeployModalStep {
type DeployModalStepProps = {
isActive: boolean;
hasCompleted: boolean;
}
const DeployModalStep: React.FC<DeployModalStepProps> = ({
title,
description,
isActive,
hasCompleted,
}) => {
return (
<Flex
as={Card}
flexDir="row"
gap={4}
opacity={isActive ? 1 : hasCompleted ? 0.8 : 0.3}
align="center"
>
{isActive ? (
<Center boxSize={5} flexShrink={0}>
<Spinner size="sm" />
</Center>
) : hasCompleted ? (
<Icon as={FiCheck} color="green.500" boxSize={5} flexShrink={0} />
) : (
<Icon as={FiActivity} boxSize={5} flexShrink={0} />
)}
<Flex flexDir="column">
<Heading size="label.lg" mb={3}>
{title}
</Heading>
<Text size="body.md">{description}</Text>
</Flex>
</Flex>
);
step: DeployModalStep;
};
export function getStepDeploy(
transactionsNumber: number,
requiresSignature = true,
): DeployModalStep {
return {
title: "Deploying contract",
description: requiresSignature
? `Your wallet will prompt you to sign ${
transactionsNumber === 1 ? "the" : transactionsNumber || 1
} transaction${transactionsNumber > 1 ? "s" : ""}.`
: "This may take a few seconds.",
};
function RenderDeployModalStep(props: DeployModalStepProps) {
const { isActive, hasCompleted } = props;
const { title, description } = getStepInfo(props.step);
return (
<div className="rounded-lg border border-border">
<div
className={cn(
"flex gap-4 p-4 items-center",
!isActive && !hasCompleted && "opacity-30",
)}
>
<div className="flex items-center justify-center shrink-0">
{isActive ? (
<Spinner className="size-6 text-link-foreground" />
) : hasCompleted ? (
<CircleCheck className="size-6 text-green-500" />
) : (
<CircleIcon className="size-6 text-secondary-foreground" />
)}
</div>
<div className="flex flex-col">
<h3 className="text-semibold mb-1 font-semibold">{title}</h3>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
</div>
</div>
);
}
export function getStepSetNFTMetadata(
requiresSignature = true,
): DeployModalStep {
return {
title: "Setting NFT metadata",
description: (
<>
{requiresSignature
? "Your wallet will prompt you to sign the transaction. "
: "This may take a few seconds."}
</>
),
};
}
export function getStepAddToRegistry(
requiresSignature = true,
): DeployModalStep {
return {
title: "Adding to dashboard",
description: (
<>
{requiresSignature
? "Your wallet will prompt you to sign the transaction. "
: ""}
<i>
This step is <b>gasless</b>.
</i>
</>
),
};
type TitleAndDesc = {
title: string;
description: React.ReactNode;
};
function getStepInfo(step: DeployModalStep): TitleAndDesc {
switch (step.type) {
case "deploy": {
return {
title: "Deploying contract",
description:
step.signatureCount > 0
? `Your wallet will prompt you to sign ${
step.signatureCount === 1 ? "the" : step.signatureCount || 1
} transaction${step.signatureCount > 1 ? "s" : ""}.`
: "This may take a few seconds",
};
}
case "setNFTMetadata": {
return {
title: "Setting NFT metadata",
description: (
<>
{step.signatureCount > 0
? "Your wallet will prompt you to sign the transaction. "
: "This may take a few seconds."}
</>
),
};
}
case "import": {
return {
title: "Adding to dashboard",
description: (
<>
{step.signatureCount > 0
? "Your wallet will prompt you to sign the transaction. "
: ""}
<i>
This step is <b>gasless</b>.
</i>
</>
),
};
}
}
}
@@ -1,72 +1,72 @@
import { FormControl } from "@chakra-ui/react";
import { useQuery } from "@tanstack/react-query";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { getExtensionInstalledParams } from "contract-ui/tabs/manage/components/getExtensionInstalledParams";
import { getModuleInstalledParams } from "contract-ui/tabs/manage/components/getModuleInstalledParams";
import invariant from "tiny-invariant";
import { FormErrorMessage, FormLabel } from "tw-components";
import type { CustomContractDeploymentForm } from "./custom-contract";
import { PrimarySaleFieldset } from "./primary-sale-fieldset";
import { RoyaltyFieldset } from "./royalty-fieldset";
type ExtensionMeta = {
extensionName: string;
extensionVersion: string;
type ModuleMeta = {
moduleName: string;
moduleVersion: string;
publisherAddress: string;
};
/**
* Get the install params for all given extensions
* Get the install params for all given modules
*/
export function useModularContractsDefaultExtensionsInstallParams(props: {
defaultExtensions?: ExtensionMeta[];
export function useModularContractsDefaultModulesInstallParams(props: {
defaultModules?: ModuleMeta[];
isQueryEnabled: boolean;
}) {
const { defaultExtensions, isQueryEnabled } = props;
const { defaultModules, isQueryEnabled } = props;
return useQuery({
queryKey: [
"useModularContractsDefaultExtensionsInstallParams",
defaultExtensions,
"useModularContractsDefaultModulesInstallParams",
defaultModules,
],
queryFn: async () => {
invariant(defaultExtensions, "defaultExtensions must be defined");
return Promise.all(defaultExtensions.map(getExtensionInstalledParams));
invariant(defaultModules, "defaultModules must be defined");
return Promise.all(defaultModules.map(getModuleInstalledParams));
},
enabled: !!(isQueryEnabled && defaultExtensions),
enabled: !!(isQueryEnabled && defaultModules),
refetchOnWindowFocus: false,
});
}
export type UseModularContractsDefaultExtensionsInstallParams = ReturnType<
typeof useModularContractsDefaultExtensionsInstallParams
export type UseModularContractsDefaultModulesInstallParams = ReturnType<
typeof useModularContractsDefaultModulesInstallParams
>;
type Extensions = NonNullable<
UseModularContractsDefaultExtensionsInstallParams["data"]
type Modules = NonNullable<
UseModularContractsDefaultModulesInstallParams["data"]
>;
type ExtensionWithIndex = Extensions[number] & { extensionIndex: number };
type ModuleWithIndex = Modules[number] & { moduleIndex: number };
export function ModularContractDefaultExtensionsFieldset(props: {
extensions: Extensions;
export function ModularContractDefaultModulesFieldset(props: {
modules: Modules;
form: CustomContractDeploymentForm;
isTWPublisher: boolean;
}) {
// save the index of the extension before filtering out
const extensionsWithIndex: ExtensionWithIndex[] = props.extensions
// save the index of the module before filtering out
const modulesWithIndex: ModuleWithIndex[] = props.modules
.map((v, i) => ({
...v,
extensionIndex: i,
moduleIndex: i,
}))
.filter((v) => v.params.length > 0);
return (
<div className="py-4">
<div className="flex flex-col gap-4">
{extensionsWithIndex.map((ext) => {
{modulesWithIndex.map((ext) => {
return (
<RenderExtension
key={ext.extensionName}
extension={ext}
<RenderModule
key={ext.moduleName}
module={ext}
isTWPublisher={props.isTWPublisher}
form={props.form}
/>
@@ -77,34 +77,26 @@ export function ModularContractDefaultExtensionsFieldset(props: {
);
}
function RenderExtension(props: {
extension: ExtensionWithIndex;
function RenderModule(props: {
module: ModuleWithIndex;
form: CustomContractDeploymentForm;
isTWPublisher: boolean;
}) {
const { extension, form } = props;
const { module, form } = props;
// only consider mapping if published by thirdweb, else show the generic form
if (props.isTWPublisher) {
const paramNames = extension.params.map((param) => param.name);
const paramNames = module.params.map((param) => param.name);
if (showRoyaltyFieldset(paramNames)) {
return (
<RenderRoyaltyFieldset
extension={extension}
form={form}
isTWPublisher
/>
<RenderRoyaltyFieldset module={module} form={form} isTWPublisher />
);
}
if (showPrimarySaleFiedset(paramNames)) {
return (
<RenderPrimarySaleFieldset
extension={extension}
form={form}
isTWPublisher
/>
<RenderPrimarySaleFieldset module={module} form={form} isTWPublisher />
);
}
}
@@ -112,12 +104,12 @@ function RenderExtension(props: {
return (
<div>
<h3 className="text-lg mb-2 text-secondary-foreground font-medium">
{extension.extensionName}
{module.moduleName}
</h3>
<div className="flex flex-col gap-3">
{extension.params.map((param) => {
{module.params.map((param) => {
const formFieldKey =
`modularContractDefaultExtensionsInstallParams.${extension.extensionIndex}.${param.name}` as const;
`modularContractDefaultModulesInstallParams.${module.moduleIndex}.${param.name}` as const;
return (
<FormControl
@@ -149,14 +141,14 @@ function RenderExtension(props: {
}
function RenderPrimarySaleFieldset(prosp: {
extension: ExtensionWithIndex;
module: ModuleWithIndex;
form: CustomContractDeploymentForm;
isTWPublisher: boolean;
}) {
const { extension, form } = prosp;
const { module, form } = prosp;
const primarySaleRecipientPath =
`modularContractDefaultExtensionsInstallParams.${extension.extensionIndex}.primarySaleRecipient` as const;
`modularContractDefaultModulesInstallParams.${module.moduleIndex}.primarySaleRecipient` as const;
return (
<PrimarySaleFieldset
@@ -173,17 +165,17 @@ function RenderPrimarySaleFieldset(prosp: {
}
function RenderRoyaltyFieldset(props: {
extension: ExtensionWithIndex;
module: ModuleWithIndex;
form: CustomContractDeploymentForm;
isTWPublisher: boolean;
}) {
const { extension: ext, form } = props;
const { module: ext, form } = props;
const royaltyRecipientPath =
`modularContractDefaultExtensionsInstallParams.${ext.extensionIndex}.royaltyRecipient` as const;
`modularContractDefaultModulesInstallParams.${ext.moduleIndex}.royaltyRecipient` as const;
const royaltyBpsPath =
`modularContractDefaultExtensionsInstallParams.${ext.extensionIndex}.royaltyBps` as const;
`modularContractDefaultModulesInstallParams.${ext.moduleIndex}.royaltyBps` as const;
return (
<RoyaltyFieldset
@@ -1,7 +1,8 @@
import { FormControl } from "@chakra-ui/react";
import { BasisPointsInput } from "components/inputs/BasisPointsInput";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { FormErrorMessage, FormLabel } from "tw-components";
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
import { Fieldset } from "./common";
import type { CustomContractDeploymentForm } from "./custom-contract";
interface PlatformFeeFieldsetProps {
@@ -12,17 +13,7 @@ export const PlatformFeeFieldset: React.FC<PlatformFeeFieldsetProps> = ({
form,
}) => {
return (
<div>
<h3 className="text-lg mb-1 font-semibold">Platform fees</h3>
<p className="text-muted-foreground text-sm mb-3">
For contract with primary sales, get additional fees for all primary
sales that happen on this contract. (This is useful if you are deploying
this contract for a 3rd party and want to take fees for your service).
If this contract is a marketplace, get a percentage of all the secondary
sales that happen on your contract.
</p>
<Fieldset legend="Platform fees">
<div className="flex flex-col md:flex-row gap-4">
<FormControl
isRequired
@@ -47,6 +38,13 @@ export const PlatformFeeFieldset: React.FC<PlatformFeeFieldsetProps> = ({
).error?.message
}
</FormErrorMessage>
<FormHelperText className="!text-sm text-muted-foreground">
For contract with primary sales, get additional fees for all primary
sales that happen on this contract. (This is useful if you are
deploying this contract for a 3rd party and want to take fees for
your service). If this contract is a marketplace, get a percentage
of all the secondary sales that happen on your contract.
</FormHelperText>
</FormControl>
<FormControl
isRequired
@@ -74,6 +72,6 @@ export const PlatformFeeFieldset: React.FC<PlatformFeeFieldsetProps> = ({
</FormErrorMessage>
</FormControl>
</div>
</div>
</Fieldset>
);
};
@@ -1,7 +1,8 @@
import { FormControl } from "@chakra-ui/react";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import type { UseFormRegisterReturn } from "react-hook-form";
import { FormErrorMessage, FormLabel } from "tw-components";
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
import { Fieldset } from "./common";
interface PrimarySaleFieldsetProps {
isInvalid: boolean;
@@ -13,14 +14,7 @@ export const PrimarySaleFieldset: React.FC<PrimarySaleFieldsetProps> = (
props,
) => {
return (
<div>
<h3 className="text-lg mb-1 font-semibold">Primary Sales</h3>
<p className="text-muted-foreground text-sm mb-3">
The wallet address that should receive the revenue from initial sales of
the assets.
</p>
<Fieldset legend=" Primary Sales">
<FormControl isRequired isInvalid={props.isInvalid}>
<FormLabel>Recipient Address</FormLabel>
<SolidityInput
@@ -28,8 +22,14 @@ export const PrimarySaleFieldset: React.FC<PrimarySaleFieldsetProps> = (
variant="filled"
{...props.register}
/>
<FormErrorMessage>{props.errorMessage}</FormErrorMessage>
<FormHelperText className="!text-sm text-muted-foreground">
The wallet address that should receive the revenue from initial sales
of the assets.
</FormHelperText>
</FormControl>
</div>
</Fieldset>
);
};
@@ -2,7 +2,8 @@ import { FormControl } from "@chakra-ui/react";
import { BasisPointsInput } from "components/inputs/BasisPointsInput";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import type { UseFormRegisterReturn } from "react-hook-form";
import { FormErrorMessage, FormLabel } from "tw-components";
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
import { Fieldset } from "./common";
export function RoyaltyFieldset(props: {
royaltyRecipient: {
@@ -19,13 +20,8 @@ export function RoyaltyFieldset(props: {
}) {
const bpsNumValue = Number.parseInt(props.royaltyBps.value);
return (
<div>
<h3 className="text-lg mb-1 font-semibold">Royalties</h3>
<p className="text-muted-foreground text-sm mb-3">
The wallet address that should receive the revenue from royalties earned
from secondary sales of the assets.
</p>
<div className="flex flex-col gap-2 md:gap-4 md:flex-row">
<Fieldset legend="Royalties">
<div className="flex flex-col gap-4 md:flex-row">
<FormControl isRequired isInvalid={props.royaltyRecipient.isInvalid}>
<FormLabel>Recipient Address</FormLabel>
<SolidityInput
@@ -36,6 +32,10 @@ export function RoyaltyFieldset(props: {
<FormErrorMessage>
{props.royaltyRecipient.errorMessage}
</FormErrorMessage>
<FormHelperText className="!text-sm text-muted-foreground">
The wallet address that should receive the revenue from royalties
earned from secondary sales of the assets.
</FormHelperText>
</FormControl>
<FormControl
isRequired
@@ -52,6 +52,6 @@ export function RoyaltyFieldset(props: {
<FormErrorMessage>{props.royaltyBps.errorMessage}</FormErrorMessage>
</FormControl>
</div>
</div>
</Fieldset>
);
}
@@ -1,20 +1,13 @@
import {
Alert,
AlertDescription,
AlertIcon,
Box,
Code,
Flex,
FormControl,
IconButton,
} from "@chakra-ui/react";
import { IoMdAdd } from "@react-icons/all-files/io/IoMdAdd";
import { IoMdRemove } from "@react-icons/all-files/io/IoMdRemove";
import { Button } from "@/components/ui/button";
import { FormControl } from "@chakra-ui/react";
import { BasisPointsInput } from "components/inputs/BasisPointsInput";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { InfoIcon, PlusIcon, Trash2Icon } from "lucide-react";
import { useFieldArray } from "react-hook-form";
import { ZERO_ADDRESS } from "thirdweb";
import { Button, FormErrorMessage, Heading, Text } from "tw-components";
import { FormErrorMessage, FormLabel } from "tw-components";
import { Alert, AlertTitle } from "../../../@/components/ui/alert";
import { Fieldset } from "./common";
import type { CustomContractDeploymentForm } from "./custom-contract";
interface SplitFieldsetProps {
@@ -38,23 +31,12 @@ export const SplitFieldset: React.FC<SplitFieldsetProps> = ({ form }) => {
?.reduce((a: number, b: Recipient) => a + b.sharesBps, 0) || 0;
return (
<>
<Flex px={0} as="section" direction="column" gap={4}>
<Flex direction="column" gap={2}>
<Heading size="label.lg">Split Settings</Heading>
<Text size="body.md" fontStyle="italic">
Define the recipients and share percentages for this Split contract.
</Text>
</Flex>
<Flex direction="column" gap={2}>
<Fieldset legend="Split Settings">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-6">
{fields.map((field, index) => {
return (
<Flex
key={field.id}
gap={2}
direction={{ base: "column", md: "row" }}
>
<div key={field.id} className="flex gap-3 items-end">
<FormControl
isInvalid={
!!form.getFieldState(
@@ -63,6 +45,7 @@ export const SplitFieldset: React.FC<SplitFieldsetProps> = ({ form }) => {
).error
}
>
<FormLabel>Recipient Address</FormLabel>
<SolidityInput
solidityType="address"
formContext={form}
@@ -79,8 +62,9 @@ export const SplitFieldset: React.FC<SplitFieldsetProps> = ({ form }) => {
}
</FormErrorMessage>
</FormControl>
<FormControl
w="35%"
className="max-w-[160px]"
isInvalid={
!!form.getFieldState(
`recipients.${index}.sharesBps`,
@@ -88,6 +72,7 @@ export const SplitFieldset: React.FC<SplitFieldsetProps> = ({ form }) => {
).error
}
>
<FormLabel> Percentage </FormLabel>
<BasisPointsInput
variant="filled"
value={form.watch(`recipients.${index}.sharesBps`)}
@@ -108,57 +93,45 @@ export const SplitFieldset: React.FC<SplitFieldsetProps> = ({ form }) => {
}
</FormErrorMessage>
</FormControl>
<IconButton
borderRadius="md"
isDisabled={
fields.length === 1 || form.formState.isSubmitting
}
colorScheme="red"
icon={<IoMdRemove />}
<Button
variant="destructive"
disabled={fields.length === 1 || form.formState.isSubmitting}
aria-label="remove row"
onClick={() => remove(index)}
/>
</Flex>
>
<Trash2Icon className="size-4" />
</Button>
</div>
);
})}
</Flex>
</div>
{/* then render high level controls */}
<Flex align="center" gap={2}>
<Box w="50%">
<Button
size="sm"
leftIcon={<IoMdAdd />}
onClick={() => append({ address: "", sharesBps: 0 })}
>
Add Recipient
</Button>
</Box>
<Flex w="50%" direction="column">
<Heading as="label" textTransform="uppercase" size="label.sm">
Total Shares
</Heading>
<Code p={0} bg="transparent">
{Math.round(totalShares / 100)}%
</Code>
</Flex>
<Box flexShrink={0} boxSize={8} />
</Flex>
</Flex>
{totalShares < 10000 ? (
<Alert status="warning" borderRadius="lg">
<AlertIcon />
<AlertDescription>
Total shares need to add up to 100%. Total shares currently add up
to {totalShares / 100}%.
</AlertDescription>
</Alert>
) : totalShares > 10000 ? (
<Alert status="warning">
<AlertIcon />
<AlertDescription>Total shares cannot go over 100%.</AlertDescription>
</Alert>
) : null}
</>
{/* div added to avoid full-width button */}
<div>
<Button
className="gap-2"
variant="outline"
onClick={() => append({ address: "", sharesBps: 0 })}
>
<PlusIcon className="size-4" />
Add Recipient
</Button>
</div>
{totalShares !== 10000 && (
<Alert
variant="destructive"
className="h-auto flex items-center gap-2"
>
<InfoIcon className="size-5 !text-destructive-text shrink-0 !static" />
<AlertTitle className="!m-0 !p-0 !text-destructive-text">
Total shares need to add up to 100%, Total shares currently add up
to {totalShares / 100}%
</AlertTitle>
</Alert>
)}
</div>
</Fieldset>
);
};
@@ -2,8 +2,12 @@ import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { Flex, FormControl, InputGroup } from "@chakra-ui/react";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { FormErrorMessage } from "tw-components";
import { useState } from "react";
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
import { DynamicHeight } from "../../../@/components/ui/DynamicHeight";
import { Switch } from "../../../@/components/ui/switch";
import { useDefaultForwarders } from "../hooks";
import { Fieldset } from "./common";
import type { CustomContractDeploymentForm } from "./custom-contract";
interface TrustedForwardersFieldsetProps {
@@ -14,68 +18,86 @@ export const TrustedForwardersFieldset: React.FC<
TrustedForwardersFieldsetProps
> = ({ form }) => {
const defaultForwarders = useDefaultForwarders();
const [showFull, setShowFull] = useState(false);
return (
<fieldset>
<legend className="text-2xl font-semibold tracking-tight mb-2">
Trusted Forwarders
</legend>
<p className="text-muted-foreground text-sm mb-1">
Trusted forwarder addresses to enable ERC-2771 transactions (i.e.
gasless).
</p>
<p className="text-muted-foreground text-sm mb-3">
You can provide your own forwarder, or click the button below to use
default forwarders provided by thirdweb. Leave empty if not needed.
</p>
<FormControl
isRequired
isInvalid={
!!form.getFieldState(
"deployParams._trustedForwarders",
form.formState,
).error
}
>
<InputGroup size="md">
<Flex flexDir="column" w="full">
<SolidityInput
value={form.watch("deployParams._trustedForwarders")}
solidityType="address[]"
{...form.register("deployParams._trustedForwarders")}
/>
</Flex>
</InputGroup>
<ToolTipLabel label="Click to apply default trusted forwarders">
<Button
type="button"
variant="outline"
className="mt-2"
// size="sm"
onClick={() =>
form.setValue(
"deployParams._trustedForwarders",
JSON.stringify(defaultForwarders.data),
)
}
>
Get Default
</Button>
</ToolTipLabel>
<FormErrorMessage>
{
form.getFieldState(
<Fieldset legend="Gasless">
<DynamicHeight>
<FormControl
isRequired
isInvalid={
!!form.getFieldState(
"deployParams._trustedForwarders",
form.formState,
).error?.message
).error
}
</FormErrorMessage>
</FormControl>
</fieldset>
>
<div className="flex gap-6 justify-between items-center">
{/* left */}
<div>
<FormLabel>Trusted Forwarders</FormLabel>
<FormHelperText className="!text-sm text-muted-foreground">
<span className="text-muted-foreground text-sm mb-1 block">
Trusted forwarder addresses to enable ERC-2771 transactions
(i.e. gasless).
</span>
<span className="text-muted-foreground text-sm block">
You can provide your own forwarder, or click the button below
to use default forwarders provided by thirdweb. Leave empty if
not needed.
</span>
</FormHelperText>
</div>
{/* Right */}
<Switch checked={showFull} onCheckedChange={setShowFull} />
</div>
<div
className={
!showFull
? "hidden"
: "block pt-3 fade-in-0 animate-in duration-400"
}
>
<InputGroup size="md">
<Flex flexDir="column" w="full">
<SolidityInput
value={form.watch("deployParams._trustedForwarders")}
solidityType="address[]"
{...form.register("deployParams._trustedForwarders")}
/>
</Flex>
</InputGroup>
<ToolTipLabel label="Click to apply default trusted forwarders">
<Button
type="button"
variant="outline"
className="mt-2"
onClick={() =>
form.setValue(
"deployParams._trustedForwarders",
JSON.stringify(defaultForwarders.data),
)
}
>
Get Default
</Button>
</ToolTipLabel>
<FormErrorMessage>
{
form.getFieldState(
"deployParams._trustedForwarders",
form.formState,
).error?.message
}
</FormErrorMessage>
</div>
</FormControl>
</DynamicHeight>
</Fieldset>
);
};

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