Compare commits

..
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
bd42363602 Version Packages (#4175)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-20 11:27:36 -07:00
gregfromstl 42cdc3a2bc UI Cleanup (#4191)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the connect modal styles and adds support for wallet authentication in the authentication flow.

### Detailed summary
- Updated connect modal styles
- Added support for wallet authentication
- Modified styles for various UI components
- Added a new component for copying addresses
- Improved wallet image rendering based on wallet type

> The following files were skipped due to too many changes: `packages/thirdweb/src/wallets/custom/smart/image.ts`, `packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx`

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

<!-- end pr-codex -->
2024-08-20 17:52:34 +00:00
MananTank 4abf7a7f25 Fix Conditionally rendered hook error in Buy with Fiat flow (#4211)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing a conditionally rendered hook error when buying funds using fiat.

### Detailed summary
- Added a new `onPostOnrampSuccess` function to handle successful post onramp swap
- Changed the logic to report fiat status instead of post onramp swap status after successful swap

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

<!-- end pr-codex -->
2024-08-20 17:31:14 +00:00
kien-ngo 8b0fd13adb [Dashboard] Sources page to v5 (#4201)
## 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 contract-related components and hooks, resolve ABI types, and improve UX in the dashboard.

### Detailed summary
- Updated components and hooks related to contracts
- Resolved ABI types with `abitype`
- Improved UX in the dashboard by updating sources display

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

<!-- end pr-codex -->
2024-08-20 17:11:57 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
20bd3aa399 chore(deps): bump tailwind-merge from 2.4.0 to 2.5.2 (#4189)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-20 10:10:48 -07:00
joaquim-verges fcfd22c01e feat(adapters): add documentation for wagmi and privy integration (#4198)
## 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 usage of adapters in the thirdweb SDK for compatibility with different libraries.

### Detailed summary
- Updated adapter names in sidebar links
- Added new adapters in sidebar
- Modified the conversion process for ethers5 and ethers6 adapters
- Added examples for using thirdweb SDK with wagmi and privy

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

<!-- end pr-codex -->
2024-08-20 16:54:22 +00:00
kien-ngo 8dc23e376e [Dashboard] Fix import path (#4200)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the dashboard contract UI tabs page by removing unused imports and refactoring some dependencies.

### Detailed summary
- Removed unused import `thirdwebClient`
- Refactored dependencies related to `3rdweb-sdk/react` and `3rdweb-dev/react`

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

<!-- end pr-codex -->
2024-08-20 10:12:12 +00:00
Kien NgoandGitHub 542f97dc4b [Dashboard] Migrate useContractSources to v5 (#4145) 2024-08-20 21:04:19 +12:00
kien-ngo 46ca21d128 [Dashboard] Explorer page to v5 (#4174)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to enhance contract function interaction by integrating V5 features and updating contract call handling.

### Detailed summary
- Added `useV5DashboardChain` and `getContract` from `lib/v5-adapter` and `thirdweb` respectively
- Updated `ContractFunction` component to use V5 contract and handle contract calls
- Updated `InteractiveAbiFunction` component to use `ThirdwebContract` and improve contract call handling

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

<!-- end pr-codex -->
2024-08-20 08:45:43 +00:00
kien-ngo 551349cbc6 [Dashboard] envs clean-up (#4170)
- [x] Remove a lof of duplicated instances of `ThirdwebClient` & env variables across multiple files
- [x] There's only 1 main ThirdwebClient from now on which can be imported from `lib/thirdweb-client`. It can switch between clientId and secretKey depends on the environment
- [x] All env variables like `DASHBOARD_THIRDWEB_CLIENT_ID`, `DASHBOARD_THIRDWEB_SECRET_KEY`, `isProd` etc. now live inside `@/constants/env`

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates import paths and constants related to the `thirdwebClient` and `isProd` across multiple files.

### Detailed summary
- Updated import paths for `thirdwebClient` and `isProd`
- Replaced `constants/rpc` with `constants/env`
- Removed unused imports and constants

> The following files were skipped due to too many changes: `apps/dashboard/src/contract-ui/tabs/overview/page.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/airdrop-tab.tsx`, `apps/dashboard/src/components/app-layouts/app.tsx`, `apps/dashboard/src/components/app-layouts/provider-setup.tsx`, `apps/dashboard/src/contract-ui/tabs/manage/page.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/opengraph-image.tsx`, `apps/dashboard/src/lib/sdk.ts`, `apps/dashboard/src/@/constants/client.ts`, `apps/dashboard/src/app/(dashboard)/(chain)/components/server/chain-icon.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/tx/[txHash]/page.tsx`, `apps/dashboard/src/pages/api/moralis/balances.ts`

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

<!-- end pr-codex -->
2024-08-20 07:55:31 +00:00
gregfromstl 4bf8296546 Dashboard: Allow more domains (#4192)
Requested for electron app domains

<!-- start pr-codex -->

---

## PR-Codex overview
This PR loosens the domain validation in `validations.ts` to allow cases where strict validation was causing issues.

### Detailed summary
- Loosened domain validation to allow cases where strict validation was too limiting
- Updated the condition to check if the domain includes ":" instead of splitting it

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

<!-- end pr-codex -->
2024-08-20 07:34:44 +00:00
Joaquim VergesandGitHub 7efef7d9e1 feat(pay): show pay modal when insufficient balance for gas cost (#4194) 2024-08-20 19:29:52 +12:00
kien-ngo 4d85aa56c6 [Dashboard] Fix types for contract published version (#4197)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `publish.tsx` and `index.tsx` files to handle cases where `version` is undefined by defaulting to "latest".

### Detailed summary
- In `publish.tsx`, added a default value of "latest" if `v` is undefined in the `SelectItem` component.
- In `index.tsx`, changed the `version` type from `string` to `string | undefined` and added a default value of "latest" if `version` is undefined when constructing the `PublishedContractOG` object.

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

<!-- end pr-codex -->
2024-08-20 07:18:40 +00:00
Jonas DanielsandGitHub decc1a56d2 [Fix] Handle optional versions in contract publishing (#4196) 2024-08-19 21:57:27 -07:00
joaquim-verges a60db3a057 feat(adapters): add documentation and examples for ethers5, ethers6, and viem adapters (#4195)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to introduce adapters for converting between Thirdweb and ethers.js/viem objects.

### Detailed summary
- Added `ethers5Adapter` and `ethers6Adapter` for Thirdweb to ethers.js conversion
- Added `viemAdapter` for Thirdweb to viem conversion
- Convert Thirdweb accounts, contracts, clients to ethers.js/viem and vice versa

> The following files were skipped due to too many changes: `packages/thirdweb/src/adapters/viem.ts`

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

<!-- end pr-codex -->
2024-08-20 04:51:32 +00:00
Jonas DanielsandGitHub 377f176b10 [Refactor] Replace max with min function and update import paths (#4193) 2024-08-19 20:08:13 -07:00
joaquim-verges 5ead75e41a fix(dashboard): update Expo Starter repo URL (#4190)
## 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 repository link in the Expo Starter template from `node-starter` to `expo-starter`.

### Detailed summary
- Updated repository link from "https://github.com/thirdweb-example/node-starter" to "https://github.com/thirdweb-example/expo-starter".

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

<!-- end pr-codex -->
2024-08-20 00:32:31 +00:00
MananTank 8836c27b8d Fix Table loading spinner position (#4187)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a `className="relative"` to various `TableContainer` components in different files for improved styling consistency.

### Detailed summary
- Added `className="relative"` to `TableContainer` in `contract-table/index.tsx`
- Added `className="relative"` to `TableContainer` in `contract-table-v2/index.tsx`
- Added `className="relative"` to `TableContainer` in `deployed-contracts.tsx`
- Added `className="relative"` to `TableContainer` in `table.tsx`
- Added `className="relative"` to `TableContainer` in `marketplace-table.tsx`

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

<!-- end pr-codex -->
2024-08-20 00:04:50 +00:00
kien-ngo cbd6fd87d0 [SDK] Update Lens test (#4188)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating a test in `resolveAddress.test.ts` to run only if a secret key is provided.

### Detailed summary
- Updated test to run only if `process.env.TW_SECRET_KEY` is provided.

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

<!-- end pr-codex -->
2024-08-19 21:51:35 +00:00
kien-ngo ef9cc55b69 [Dashboard] Remove v4 code on some Marketplace components
(#4128)

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR exposes `max()` and `min()` utility methods for bigints and updates marketplace-related components to use these methods for better handling of large numbers.

### Detailed summary
- Exposes `max()` and `min()` utility methods for bigints in `thirdweb/utils`
- Updates components in `dashboard` related to marketplace to use `max()` and `min()` for handling large numbers efficiently

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

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

<!-- end pr-codex -->
2024-08-19 20:06:26 +00:00
kien-ngo 15899ea231 [Dashboard] Fix styling for no-image NFT (#4181)
## 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 styling of the `NFTMedia` component by adding explicit `width` and `height` properties.

### Detailed summary
- Added inline styles for `width` and `height` based on `props`
- Updated `className` to include `border-border` class

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

<!-- end pr-codex -->
2024-08-19 19:47:32 +00:00
kien-ngo bd4669984d [Portal] Update some docs (#4177)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to introduce gasless transaction configurations and transaction annotations.

### Detailed summary
- Added gasless transaction configurations for Engine, OpenZeppelin, and Biconomy providers
- Introduced transaction annotations for Engine, OpenZeppelin, and gasless options
- Updated transaction button components to include transaction annotations
- Exposed GaslessOptions type for gasless transactions configuration

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

<!-- end pr-codex -->
2024-08-19 19:29:55 +00:00
MananTank fb799ccb6d Move publish metadata spinner inside form, show error message (#4180)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds an error message display for failed metadata fetch in the `CustomContractForm` component.

### Detailed summary
- Added error message display for failed metadata fetch
- Imported `InfoIcon` component
- Removed conditional rendering for loading spinner outside the form provider block

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

<!-- end pr-codex -->
2024-08-19 18:24:20 +00:00
kien-ngo c8f7a68225 [Dashboard] (partial) useContract removal (#4111)
Slowly moving everything upward. This PR mainly focuses on removing `useContract` from the page level (`page.tsx` files)

Routes to test:
- [x] direct listings page
- [x] english auction page
- [x] Split contract page
- [x] Contract account page
- [x] contract settings page
- [x] contract token page

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update various components to use `ThirdwebContract` instead of `contractAddress` and `chainId`.

### Detailed summary
- Update components to use `ThirdwebContract` instead of `contractAddress` and `chainId`
- Replace `useContract` calls with `contract.address`
- Import `ThirdwebContract` type for type checking

> The following files were skipped due to too many changes: `apps/dashboard/src/contract-ui/tabs/shared-components/marketplace-table.tsx`, `apps/dashboard/src/contract-ui/tabs/shared-components/cancel-tab.tsx`, `apps/dashboard/src/contract-ui/tabs/shared-components/listing-drawer.tsx`, `apps/dashboard/src/contract-ui/tabs/english-auctions/components/table.tsx`, `apps/dashboard/src/contract-ui/tabs/direct-listings/components/table.tsx`, `apps/dashboard/src/contract-ui/tabs/english-auctions/page.tsx`, `apps/dashboard/src/contract-ui/tabs/direct-listings/page.tsx`, `apps/dashboard/src/contract-ui/tabs/split/page.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/page.tsx`, `apps/dashboard/src/contract-ui/tabs/shared-components/list-button.tsx`, `apps/dashboard/src/contract-ui/tabs/account-permissions/page.tsx`, `apps/dashboard/src/contract-ui/tabs/embed/page.tsx`, `apps/dashboard/src/contract-ui/tabs/settings/page.tsx`, `apps/dashboard/src/contract-ui/tabs/account/page.tsx`, `apps/dashboard/src/contract-ui/tabs/tokens/page.tsx`, `apps/dashboard/src/contract-ui/hooks/useRouteConfig.tsx`

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

<!-- end pr-codex -->
2024-08-19 18:12:53 +00:00
gregfromstl 246701d615 Wallet Linking UI (#4124)
https://github.com/user-attachments/assets/abf70934-8070-4844-8f15-8db6ec40c116

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds the ability to link a wallet in the Connect UI.

### Detailed summary
- Added a new `linkWallet` field in locale types for various languages
- Updated UI components to support linking a wallet
- Introduced `WalletAuth` component for wallet authentication
- Modified wallet selection behavior in `WalletSelector` component

> The following files were skipped due to too many changes: `packages/thirdweb/src/react/web/ui/ConnectWallet/WalletSelector.tsx`, `packages/thirdweb/src/react/web/wallets/shared/PassKeyLogin.tsx`, `packages/thirdweb/src/react/web/wallets/in-app/WalletAuth.tsx`

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

<!-- end pr-codex -->
2024-08-19 17:01:20 +00:00
Joaquim VergesandGitHub cb591aa6af feat(pay): only show pay modal if the active account doesnt have enough funds (#4172) 2024-08-19 21:15:54 +12:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
09927f3ac5 Version Packages (#4162)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-19 20:17:56 +12:00
gregfromstl 7778063458 Improve Blobbie Appearance (#4173)
Blobbies now select from a subset of predefined colors to form a basic radial gradient form the bottom right corner.

![image](https://github.com/user-attachments/assets/5b708533-506d-417d-ac49-2356aa0a2247)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the appearance of the Blobbie avatar in the ConnectWallet feature of `thirdweb`.

### Detailed summary
- Updated Blobbie appearance with unique gradient colors based on address
- Replaced complex color pairing logic with predefined color options
- Simplified Blobbie rendering logic for better performance

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

<!-- end pr-codex -->
2024-08-19 06:17:43 +00:00
Joaquim VergesandGitHub 574ff0295d feat(dashboard): update apple-app-site-association for universal links (#4171) 2024-08-19 11:54:40 +12:00
kien-ngo a43907e2df [SDK] resolveAddress for Lens handles (#4150)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to enhance the `Lens` extension in the `thirdweb` package by adding the ability to resolve addresses and handle crypto transactions efficiently.

### Detailed summary
- Added `resolveAddress` function for handling crypto transactions with lens handles
- Updated contract addresses and overrides for Lens smart contracts
- Created tests for `resolveAddress` function to ensure correct address resolution

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

<!-- end pr-codex -->
2024-08-18 23:10:46 +00:00
kien-ngo f6d6c86ec0 [Dashboard] Fix ENS resolve issue (#4168)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `thirdwebClient` configuration in `thirdweb-client.ts` to include a `secretKey` option if `DASHBOARD_THIRDWEB_SECRET_KEY` is defined.

### Detailed summary
- Updated `thirdwebClient` configuration to include `secretKey` if `DASHBOARD_THIRDWEB_SECRET_KEY` is defined
- Added conditional logic for `secretKey` presence

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

<!-- end pr-codex -->
2024-08-18 22:21:10 +00:00
Joaquim VergesandGitHub 23041c013f feat(dashboard): add apple universal link association (#4169) 2024-08-19 10:04:20 +12:00
kien-ngo 43f9319ac7 [SDK] Add GaslessOptions to useSendAndConfirmTransaction (#4165)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to add Gasless transaction support for the `sendTransaction` and `sendAndConfirmTransaction` functions, as well as for the `useSendTransaction` and `useSendAndConfirmTransaction` hooks.

### Detailed summary
- Added `GaslessOptions` support to `sendTransaction`
- Introduced Gasless usage with `thirdweb Engine` in various transaction-related functions and hooks
- Added `GaslessOptions` configuration to `useSendTransaction` and `useSendAndConfirmTransaction` hooks
- Updated documentation and examples to showcase Gasless transaction usage

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

<!-- end pr-codex -->
2024-08-18 10:05:37 +00:00
kien-ngo fc8daaa2bc [Dashboard] Remove ethers.js (#4163)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on exporting a utility method `isBytes` and updating types to `bigint`.

### Detailed summary
- Exported `isBytes` utility method
- Updated `nextTokenIdToMint` type to `bigint`
- Updated imports for `isAddress`, `isBytes`, and `isHex`
- Refactored display balance calculations to use `toTokens` function

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

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

<!-- end pr-codex -->
2024-08-18 07:04:05 +00:00
kien-ngo 9133b45700 [Dashboard] useVote to v5 (#4154)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates TypeScript documentation and refactors Vote-related components for clarity and consistency.

### Detailed summary
- Updated TypeScript documentation for `ProposalVoteInfo`
- Refactored `VoteButton` and `DelegateButton` components to use `ThirdwebContract`
- Improved display of voting token decimals in `Proposal` component

> The following files were skipped due to too many changes: `apps/dashboard/src/@3rdweb-sdk/react/hooks/useVote.ts`

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

<!-- end pr-codex -->
2024-08-18 05:02:05 +00:00
kien-ngo 44c78f4643 [Dashboard] useRemoveContractMutation to v5 + minor cleanups (#4155)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to refactor gasless transaction handling in the Dashboard by centralizing contract addresses and relayer URLs.

### Detailed summary
- Introduced `MULTICHAIN_REGISTRY_CONTRACT` constant for contract address
- Centralized relayer URL and forwarder address in `DASHBOARD_ENGINE_RELAYER_URL` and `DASHBOARD_FORWARDER_ADDRESS`
- Updated functions to use centralized constants for contract addresses and relayer URLs

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

<!-- end pr-codex -->
2024-08-18 03:55:32 +00:00
kien-ngo 7aee37b10f [Dashboard] Replace useSDKChainId with useDashboardEVMChainId (#4144)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to replace the usage of `useSDKChainId` with `useDashboardEVMChainId` in multiple components for better compatibility and functionality.

### Detailed summary
- Replaced `useSDKChainId` with `useDashboardEVMChainId` in multiple components.
- Updated imports and dependencies accordingly.
- Improved compatibility and functionality for the dashboard.

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

<!-- end pr-codex -->
2024-08-18 03:43:32 +00:00
kien-ngo 23a665bdaa [Dashboard] Remove v4 type: OptionalPropertiesInput (#4158)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to update the type definition for `attributes` in the `IPropertyFieldValues` interface.

### Detailed summary
- Updated `OptionalPropertiesInput` type definition to be a key-value pair object
- Changed `attributes` type in `IPropertyFieldValues` interface to use the updated type definition

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

<!-- end pr-codex -->
2024-08-18 03:32:30 +00:00
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
358 changed files with 9066 additions and 4355 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
@@ -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',
},
],
},
],
+1 -1
View File
@@ -106,7 +106,7 @@
"sonner": "^1.5.0",
"spdx-correct": "^3.2.0",
"swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.4.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
"tiny-invariant": "^1.3.3",
@@ -1,5 +1,28 @@
{
"applinks": {},
"applinks": {
"apps": ["XYHGSFAMHX.com.thirdweb.demo"],
"details": [
{
"appIDs": ["XYHGSFAMHX.com.thirdweb.demo"],
"components": [
{
"#": "no_universal_links",
"exclude": true,
"comment": "Matches any URL with a fragment that equals no_universal_links and instructs the system not to open it as a universal link."
},
{
"/": "/mobile-wallet-protocol",
"comment": "Matches any URL with a path that starts with /mobile-wallet-protocol for coinbase wallet mobile redirects."
},
{
"/": "/dashboard/*",
"exclude": true,
"comment": "no universal link for dashboard"
}
]
}
]
},
"webcredentials": {
"apps": ["XYHGSFAMHX.com.thirdweb.demo"]
},
@@ -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,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 -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}
@@ -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}
+28 -9
View File
@@ -1,11 +1,30 @@
import {
DASHBOARD_THIRDWEB_CLIENT_ID,
DASHBOARD_THIRDWEB_SECRET_KEY,
IPFS_GATEWAY_URL,
} from "@/constants/env";
import { createThirdwebClient } from "thirdweb";
import { DASHBOARD_THIRDWEB_CLIENT_ID, IPFS_GATEWAY_URL } from "./env";
export const thirdwebClient = createThirdwebClient({
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
config: {
storage: {
gatewayUrl: IPFS_GATEWAY_URL,
},
},
});
export const thirdwebClient = createThirdwebClient(
DASHBOARD_THIRDWEB_SECRET_KEY
? {
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
config: {
storage: {
gatewayUrl: IPFS_GATEWAY_URL,
},
},
}
: {
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
config: {
storage: {
gatewayUrl: IPFS_GATEWAY_URL,
},
},
},
);
/**
* DO NOT ADD ANYTHING TO THIS FILE IF YOU ARE NOT ABSOLUTELY SURE IT IS OK
*/
+6
View File
@@ -10,3 +10,9 @@ export const IPFS_GATEWAY_URL =
export const THIRDWEB_ENGINE_FAUCET_WALLET =
process.env.NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET || "";
export const isProd =
(process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV) ===
"production";
export const PROD_OR_DEV_URL = isProd ? "thirdweb.com" : "thirdweb-dev.com";
@@ -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);
};
},
};
}
+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%;
}
}
@@ -333,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;
@@ -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,38 +1,27 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useSigner } from "@thirdweb-dev/react";
import { addContractToMultiChainRegistry } from "components/contract-components/utils";
import { MULTICHAIN_REGISTRY_CONTRACT } from "constants/contracts";
import {
addContractToMultiChainRegistry,
getGaslessPolygonSDK,
} from "components/contract-components/utils";
DASHBOARD_ENGINE_RELAYER_URL,
DASHBOARD_FORWARDER_ADDRESS,
} from "constants/misc";
import type { BasicContract } from "contract-ui/types/types";
import { getAllMultichainRegistry } from "dashboard-extensions/common/read/getAllMultichainRegistry";
import { useAllChainsData } from "hooks/chains/allChains";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { defineDashboardChain } from "lib/defineDashboardChain";
import { thirdwebClient } from "lib/thirdweb-client";
import { useMemo } from "react";
import { getContract } from "thirdweb";
import { polygon } from "thirdweb/chains";
import { sendAndConfirmTransaction } from "thirdweb";
import { remove } from "thirdweb/extensions/thirdweb";
import { useActiveAccount } from "thirdweb/react";
import invariant from "tiny-invariant";
const MULTICHAIN_REGISTRY_ADDRESS =
"0xcdAD8FA86e18538aC207872E8ff3536501431B73";
const registryContract = getContract({
client: thirdwebClient,
address: MULTICHAIN_REGISTRY_ADDRESS,
// eslint-disable-next-line no-restricted-syntax
chain: defineDashboardChain(polygon.id, undefined),
});
function useMultiChainRegContractList(walletAddress?: string) {
return useQuery(
["dashboard-registry", walletAddress, "multichain-contract-list"],
async () => {
invariant(walletAddress, "walletAddress is required");
const contracts = await getAllMultichainRegistry({
contract: registryContract,
contract: MULTICHAIN_REGISTRY_CONTRACT,
address: walletAddress,
});
@@ -97,28 +86,30 @@ type RemoveContractParams = {
};
export function useRemoveContractMutation() {
const walletAddress = useActiveAccount()?.address;
const signer = useSigner();
const { chainIdToChainRecord } = useAllChainsData();
const queryClient = useQueryClient();
const account = useActiveAccount();
return useMutation(
async (data: RemoveContractParams) => {
invariant(
walletAddress,
"cannot add a contract without a wallet address",
);
invariant(chainIdToChainRecord, "chains not initialzed yet");
invariant(signer, "no wallet connected");
invariant(data.chainId, "chainId not provided");
invariant(account, "No wallet connected");
const { contractAddress, chainId } = data;
const gaslessPolygonSDK = getGaslessPolygonSDK(signer);
return await gaslessPolygonSDK.multiChainRegistry?.removeContract({
address: contractAddress,
chainId,
const transaction = remove({
contract: MULTICHAIN_REGISTRY_CONTRACT,
deployer: account.address,
deployment: contractAddress,
chainId: BigInt(chainId),
});
return await sendAndConfirmTransaction({
transaction,
account,
gasless: {
experimentalChainlessSupport: true,
provider: "engine",
relayerUrl: DASHBOARD_ENGINE_RELAYER_URL,
relayerForwarderAddress: DASHBOARD_FORWARDER_ADDRESS,
},
});
},
{
@@ -0,0 +1,21 @@
import { useQuery } from "@tanstack/react-query";
import type { Abi } from "abitype";
import type { ThirdwebContract } from "thirdweb";
import { resolveContractAbi } from "thirdweb/contract";
import invariant from "tiny-invariant";
export function useResolveContractAbi(contract?: ThirdwebContract) {
return useQuery({
queryKey: [
"full-contract-abi",
contract?.chain.id || "",
contract?.address || "",
],
queryFn: async () => {
invariant(contract, "contract is required");
const abi = await resolveContractAbi<Abi>(contract);
return abi;
},
enabled: !!contract,
});
}
@@ -1,18 +1,36 @@
import { useContract, useContractMetadata, useSDK } from "@thirdweb-dev/react";
import type { Vote, VoteType } from "@thirdweb-dev/sdk";
import { useActiveAccount } from "thirdweb/react";
import { thirdwebClient } from "@/constants/client";
import { type ThirdwebContract, getContract, toTokens } from "thirdweb";
import {
balanceOf,
decimals,
delegate,
delegates,
} from "thirdweb/extensions/erc20";
import {
type VoteType,
canExecute,
castVoteWithReason,
executeProposal,
getAll,
hasVoted,
propose,
token,
} from "thirdweb/extensions/vote";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import invariant from "tiny-invariant";
import type { RequiredParam } from "utils/types";
import { voteKeys } from "../cache-keys";
import {
useMutationWithInvalidate,
useQueryWithNetwork,
} from "./query/useQueryWithNetwork";
export function useVoteProposalList(contract?: Vote) {
export function useVoteProposalList(contract?: ThirdwebContract) {
return useQueryWithNetwork(
voteKeys.proposals(contract?.getAddress()),
async () => await contract?.getAll(),
voteKeys.proposals(contract?.address || ""),
async () => {
invariant(contract, "contract is required");
return await getAll({ contract });
},
{
enabled: !!contract,
},
@@ -20,17 +38,20 @@ export function useVoteProposalList(contract?: Vote) {
}
export function useHasVotedOnProposal(
contract: RequiredParam<Vote>,
proposalId: string,
contract: ThirdwebContract,
proposalId: bigint,
) {
const address = useActiveAccount()?.address;
const userAddress = useActiveAccount()?.address;
return useQueryWithNetwork(
voteKeys.userHasVotedOnProposal(
proposalId,
contract?.getAddress(),
address,
proposalId.toString(),
contract.address,
userAddress,
),
async () => await contract?.hasVoted(proposalId, address),
async () => {
invariant(userAddress, "address is required");
return await hasVoted({ contract, proposalId, account: userAddress });
},
{
enabled: !!contract,
},
@@ -38,65 +59,83 @@ export function useHasVotedOnProposal(
}
export function useCanExecuteProposal(
contract: RequiredParam<Vote>,
proposalId: string,
contract: ThirdwebContract,
proposalId: bigint,
) {
return useQueryWithNetwork(
voteKeys.canExecuteProposal(proposalId, contract?.getAddress()),
async () => await contract?.canExecute(proposalId),
voteKeys.canExecuteProposal(proposalId.toString(), contract.address),
async () => await canExecute({ contract, proposalId }),
{
enabled: !!contract,
},
);
}
export function useTokensDelegated(contract?: Vote) {
const sdk = useSDK();
const address = useActiveAccount()?.address;
export function useTokensDelegated(contract: ThirdwebContract) {
const userAddress = useActiveAccount()?.address;
return useQueryWithNetwork(
voteKeys.delegation(contract?.getAddress(), address),
voteKeys.delegation(contract.address, userAddress),
async () => {
invariant(address, "address is required");
invariant(contract, "vote contract is required");
const metadata = await contract?.metadata.get();
const tokenAddress = metadata?.voting_token_address;
const tokenContract = await sdk?.getToken(tokenAddress);
const delegation = await tokenContract?.getDelegationOf(address);
return delegation?.toLowerCase() === address.toLowerCase();
invariant(userAddress, "wallet address is required");
const tokenAddress = await token({ contract });
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
address: tokenAddress,
chain: contract.chain,
client: thirdwebClient,
});
const delegatedAddress = await delegates({
contract: tokenContract,
account: userAddress,
});
return delegatedAddress?.toLowerCase() === userAddress.toLowerCase();
},
{
enabled: !!contract && !!address,
enabled: !!contract && !!userAddress,
},
);
}
export function useVoteTokenBalances(contract?: Vote, addresses?: string[]) {
const { data } = useContractMetadata(contract);
const tokenContract = useContract(
data?.voting_token_address,
"token",
).contract;
export function useVoteTokenBalances(
contract: ThirdwebContract,
addresses: string[],
) {
return useQueryWithNetwork(
voteKeys.balances(contract?.getAddress(), addresses),
async () => {
invariant(data, "contract metadata is required");
invariant(tokenContract, "voting contract is required");
invariant(addresses, "addresses are required");
const balances = addresses.map(async (address) => {
voteKeys.balances(contract.address, addresses),
async (): Promise<
Array<{ address: string; balance: string; decimals: number }>
> => {
invariant(addresses.length, "addresses are required");
const tokenAddress = await token({ contract });
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
address: tokenAddress,
chain: contract.chain,
client: thirdwebClient,
});
const _decimals = await decimals({ contract: tokenContract });
const balanceData = await Promise.all(
addresses.map((address) =>
balanceOf({ contract: tokenContract, address }),
),
);
const balances = addresses.map((address, index) => {
const balance = balanceData[index] || 0n;
return {
address,
balance: (await tokenContract.balanceOf(address)).displayValue,
balance: toTokens(balance, _decimals),
decimals: _decimals,
};
});
return await Promise.all(balances);
},
{
enabled: !!data && !!contract && !!addresses?.length,
enabled: addresses.length > 0,
},
);
}
@@ -105,94 +144,143 @@ export interface IProposalInput {
description: string;
}
export function useProposalCreateMutation(contractAddress?: string) {
const voteContract = useContract(contractAddress, "vote").contract;
/**
* This hook is a simplified version meant to use on the Dashboard
* since _currently_ the dashboard only supports creating proposals with descriptions
*
* todo: make an extension in the SDK that is more robust and user-friendly ?
*/
export function useProposalCreateMutation(contract: ThirdwebContract) {
const { mutateAsync } = useSendAndConfirmTransaction();
return useMutationWithInvalidate(
(proposal: IProposalInput) => {
invariant(voteContract, "contract is required");
async (proposal: IProposalInput) => {
const { description } = proposal;
return voteContract.propose(description);
const transaction = propose({
contract,
description,
targets: [contract.address],
values: [0n],
calldatas: ["0x"],
});
return await mutateAsync(transaction);
},
{
onSuccess: (_data, _options, _variables, invalidate) => {
return invalidate([voteKeys.proposals(contractAddress)]);
return invalidate([voteKeys.proposals(contract.address)]);
},
},
);
}
export function useDelegateMutation(contract?: Vote) {
const sdk = useSDK();
const address = useActiveAccount()?.address;
const contractAddress = contract?.getAddress();
export function useDelegateMutation(contract: ThirdwebContract) {
const userAddress = useActiveAccount()?.address;
const { mutateAsync } = useSendAndConfirmTransaction();
return useMutationWithInvalidate(
async () => {
invariant(address, "address is required");
invariant(contractAddress, "contract address is required");
invariant(contract, "vote contract is required");
const metadata = await contract?.metadata.get();
const tokenAddress = metadata?.voting_token_address;
const tokenContract = await sdk?.getToken(tokenAddress);
return tokenContract?.delegateTo(address);
invariant(userAddress, "address is required");
const tokenAddress = await token({ contract });
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
address: tokenAddress,
chain: contract.chain,
client: thirdwebClient,
});
const transaction = delegate({
contract: tokenContract,
delegatee: userAddress,
});
return await mutateAsync(transaction);
},
{
onSuccess: (_data, _options, _variables, invalidate) => {
return invalidate([voteKeys.delegation(contractAddress, address)]);
return invalidate([voteKeys.delegation(contract.address, userAddress)]);
},
},
);
}
/**
* Get the decimals of the voting erc20 token
*/
export function useVotingTokenDecimals(contract: ThirdwebContract) {
return useMutationWithInvalidate(async () => {
const tokenAddress = await token({ contract });
if (!tokenAddress) {
throw new Error("Expected a delegated token address");
}
const tokenContract = getContract({
address: tokenAddress,
chain: contract.chain,
client: thirdwebClient,
});
const _decimals = await decimals({ contract: tokenContract });
return _decimals;
});
}
interface IVoteCast {
voteType: VoteType;
reason?: string;
}
export function useCastVoteMutation(
contract: RequiredParam<Vote>,
proposalId: string,
contract: ThirdwebContract,
proposalId: bigint,
) {
const address = useActiveAccount()?.address;
const contractAddress = contract?.getAddress();
const contractAddress = contract.address;
const { mutateAsync } = useSendAndConfirmTransaction();
return useMutationWithInvalidate(
async (vote: IVoteCast) => {
async (voteItem: IVoteCast) => {
invariant(contract, "contract is required");
invariant(address, "address is required");
const { voteType, reason } = vote;
return contract.vote(proposalId, voteType, reason);
const { voteType, reason } = voteItem;
const transaction = castVoteWithReason({
contract,
proposalId,
support: voteType,
reason: reason ?? "",
});
return await mutateAsync(transaction);
},
{
onSuccess: (_data, _options, _variables, invalidate) => {
return invalidate([
voteKeys.proposals(contractAddress),
voteKeys.userHasVotedOnProposal(proposalId, contractAddress, address),
voteKeys.canExecuteProposal(proposalId, contractAddress),
voteKeys.userHasVotedOnProposal(
proposalId.toString(),
contractAddress,
address,
),
voteKeys.canExecuteProposal(proposalId.toString(), contractAddress),
]);
},
},
);
}
/**
* This hook is a simplified version for the Dashboard
* It doesn't pass any extra info to the execute function
*/
export function useExecuteProposalMutation(
contract: RequiredParam<Vote>,
proposalId: string,
contract: ThirdwebContract,
proposalId: bigint,
) {
const contractAddress = contract?.getAddress();
const contractAddress = contract.address;
const mutation = useSendAndConfirmTransaction();
return useMutationWithInvalidate(
async () => {
invariant(contract, "contract is required");
return contract.execute(proposalId);
const transaction = executeProposal({ contract, proposalId });
return await mutation.mutateAsync(transaction);
},
{
onSuccess: (_data, _options, _variables, invalidate) => {
return invalidate([
voteKeys.proposals(contractAddress),
voteKeys.canExecuteProposal(proposalId, contractAddress),
voteKeys.canExecuteProposal(proposalId.toString(), contractAddress),
]);
},
},
@@ -2,13 +2,13 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { thirdwebClient } from "@/constants/client";
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
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 { toast } from "sonner";
import { toUnits } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
@@ -3,9 +3,9 @@
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { thirdwebClient } from "@/constants/client";
import { isProd } from "@/constants/env";
import { useQuery } from "@tanstack/react-query";
import { isProd } from "constants/rpc";
import { thirdwebClient } from "lib/thirdweb-client";
import { CircleCheck, XIcon } from "lucide-react";
import { useState } from "react";
import { hostnameEndsWith } from "utils/url";
@@ -1,10 +1,6 @@
/* eslint-disable @next/next/no-img-element */
import {
DASHBOARD_THIRDWEB_SECRET_KEY,
IPFS_GATEWAY_URL,
} from "@/constants/env";
import { thirdwebClient } from "@/constants/client";
import { ImageResponse } from "next/og";
import { createThirdwebClient } from "thirdweb";
import { download } from "thirdweb/storage";
import { fetchChain } from "utils/fetchChain";
@@ -66,15 +62,6 @@ const TWLogo: React.FC = () => (
export default async function Image({
params,
}: { params: { chain_id: string } }) {
const thirdwebClient = createThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
config: {
storage: {
gatewayUrl: IPFS_GATEWAY_URL,
},
},
});
const chain = await fetchChain(params.chain_id);
if (!chain) {
return new Response("Chain not found", { status: 400 });
@@ -1,8 +1,7 @@
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { IPFS_GATEWAY_URL } from "@/constants/env";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "constants/rpc";
import { ZERO_ADDRESS, createThirdwebClient, toTokens } from "thirdweb";
import { thirdwebClient } from "@/constants/client";
import { ZERO_ADDRESS, toTokens } from "thirdweb";
import {
eth_getBlockByHash,
eth_getTransactionByHash,
@@ -16,14 +15,6 @@ import { getChain } from "../../../utils";
export default async function Page(props: {
params: { chain_id: string; txHash: `0x${string}` };
}) {
const thirdwebClient = createThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
config: {
storage: {
gatewayUrl: IPFS_GATEWAY_URL,
},
},
});
const chain = await getChain(props.params.chain_id);
const rpcRequest = getRpcClient({
@@ -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") => {
@@ -1,12 +1,9 @@
/* eslint-disable @next/next/no-img-element */
import "server-only";
import {
DASHBOARD_THIRDWEB_SECRET_KEY,
IPFS_GATEWAY_URL,
} from "@/constants/env";
import { thirdwebClient } from "@/constants/client";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
import { cn } from "@/lib/utils";
import { createThirdwebClient } from "thirdweb";
import { resolveScheme } from "thirdweb/storage";
const fallbackChainIcon =
@@ -17,16 +14,8 @@ export async function ChainIcon(props: {
className?: string;
}) {
if (props.iconUrl) {
const client = createThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
config: {
storage: {
gatewayUrl: IPFS_GATEWAY_URL,
},
},
});
const resolved = resolveScheme({
client,
client: thirdwebClient,
uri: props.iconUrl,
});
const res = await fetch(resolved, {
@@ -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";
@@ -9,8 +9,8 @@ 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 { thirdwebClient } from "@/constants/client";
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";
@@ -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,7 +1,8 @@
"use client";
import { Button } from "@/components/ui/button";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { ChevronDown } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { usePathname } from "next/navigation";
import type { SortBy, TimeRange } from "../../../../lib/search";
export type SortingHeaderProps = {
@@ -13,7 +14,7 @@ export type SortingHeaderProps = {
};
export function SortingHeader(props: SortingHeaderProps) {
const router = useRouter();
const router = useDashboardRouter();
const path = usePathname();
const enableSorting = !!props.searchParams;
const { timeRange, page, sortBy } = props.searchParams || {};
@@ -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";
+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}
@@ -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",
@@ -1,3 +1,4 @@
import { isProd } from "@/constants/env";
import {
type EVMContractInfo,
EVMContractInfoProvider,
@@ -20,7 +21,6 @@ import { AppShell, type AppShellProps } from "components/layout/app-shell";
import { Onboarding as OnboardingModal } from "components/onboarding";
import { OpCreditsGrantedModalWrapper } from "components/onboarding/OpCreditsGrantedModalWrapper";
import { PosthogIdentifier } from "components/wallets/PosthogIdentifier";
import { isProd } from "constants/rpc";
import { AllChainsProvider } from "contexts/all-chains";
import { ChainsProvider } from "contexts/configured-chains";
import { ErrorProvider } from "contexts/error-handler";
@@ -1,16 +1,16 @@
"use client";
import { thirdwebClient } from "@/constants/client";
import {
DASHBOARD_THIRDWEB_CLIENT_ID,
DASHBOARD_THIRDWEB_SECRET_KEY,
} from "@/constants/env";
import {
type EVMContractInfo,
useEVMContractInfo,
} from "@3rdweb-sdk/react/hooks/useActiveChainId";
import { useQueryClient } from "@tanstack/react-query";
import { ThirdwebSDKProvider } from "@thirdweb-dev/react";
import {
DASHBOARD_THIRDWEB_CLIENT_ID,
DASHBOARD_THIRDWEB_SECRET_KEY,
} from "constants/rpc";
import type { Signer } from "ethers";
import { useSupportedChains } from "hooks/chains/configureChains";
import { useNativeColorMode } from "hooks/useNativeColorMode";
@@ -10,6 +10,7 @@ import {
} from "@/components/ui/dialog";
import { thirdwebClient } from "@/constants/client";
import { cn } from "@/lib/utils";
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import {
Box,
@@ -25,7 +26,6 @@ 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 { 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 {
@@ -33,6 +33,7 @@ import type {
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";
@@ -41,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,
@@ -51,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";
@@ -62,7 +65,7 @@ const GAS_FREE_CHAINS = [
function useNetworkMismatchAdapter() {
const walletChainId = useActiveWalletChain()?.id;
const v4SDKChainId = useSDKChainId();
const v4SDKChainId = useDashboardEVMChainId();
if (!walletChainId || !v4SDKChainId) {
// simply not ready yet, assume false
return false;
@@ -397,7 +400,7 @@ const MismatchNotice: React.FC<{
onClose: (hasSwitched: boolean) => void;
}> = ({ initialFocusRef, onClose }) => {
const connectedChainId = useActiveWalletChain()?.id;
const desiredChainId = useSDKChainId();
const desiredChainId = useDashboardEVMChainId();
const switchNetwork = useSwitchActiveWalletChain();
const connectionStatus = useActiveWalletConnectionStatus();
const activeWallet = useActiveWallet();
@@ -485,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 (
@@ -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>
);
};
@@ -21,7 +21,7 @@ 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 { ExternalLinkIcon, InfoIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useMemo } from "react";
@@ -349,15 +349,6 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
(isErc721SharedMetadadata ? 1 : 0);
const isCreate2Deployment = form.watch("deployDeterministic");
if (fullPublishMetadata.isLoading) {
return (
<div className="min-h-[200px] flex items-center justify-center">
<Spinner className="size-10" />
</div>
);
}
const advancedParams = Object.keys(formDeployParams)
.map((paramKey) => {
const deployParam = deployParams.find((p) => p.name === paramKey);
@@ -382,6 +373,19 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
return (
<FormProvider {...form}>
{fullPublishMetadata.isLoading && (
<div className="min-h-[200px] flex items-center justify-center">
<Spinner className="size-10" />
</div>
)}
{fullPublishMetadata.isError && (
<div className="bg-destructive p-4 border border-destructive-text/30 rounded-lg text-destructive-text flex gap-2 items-center">
<InfoIcon className="size-4" />
Failed to fetch Publish metadata
</div>
)}
<Flex
flexGrow={1}
minH="full"
@@ -171,7 +171,7 @@ export const PublishedContractTable: ComponentWithChildren<
data: rows,
});
return (
<TableContainer>
<TableContainer className="relative">
{isFetching && (
<Spinner
color="primary"
@@ -56,7 +56,7 @@ export const DeployableContractTable: ComponentWithChildren<
data: contractIds.map((contractId) => ({ contractId })),
});
return (
<TableContainer>
<TableContainer className="relative">
{isFetching && (
<Spinner
color="primary"
@@ -750,6 +750,20 @@ export function useCustomContractDeployMutation(options: {
message,
);
}
case "eth_sendTransaction": {
const tx = params?.[0];
if (!tx) {
throw new Error("No transaction provided");
}
return signer.sendTransaction(tx);
}
case "personal_sign": {
const data = params?.[0];
if (!data) {
throw new Error("Nothing to sign");
}
return signer.signMessage(data);
}
default: {
return (signer as providers.JsonRpcSigner)?.provider?.send(
method,
@@ -59,7 +59,7 @@ export interface ExtendedPublishedContract extends PublishedContractType {
name: string;
displayName?: string;
description: string;
version: string;
version: string | undefined;
publisher: string;
tags?: string[];
logo?: string;
@@ -146,7 +146,7 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
PublishedContractOG.toUrl({
name: publishedContractName,
description: contract.description,
version: contract.version,
version: contract.version || "latest",
publisher: publisherEnsOrAddress,
extension: extensionNames,
license: licenses,
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
Box,
type BoxProps,
@@ -35,8 +34,7 @@ export const MarkdownRenderer: React.FC<
{...restProps}
remarkPlugins={[remarkGfm]}
components={{
// biome-ignore lint/suspicious/noExplicitAny: FIXME
h1: (props: any) => (
h1: (props) => (
<Heading
as="h2"
size="title.lg"
@@ -44,11 +42,11 @@ export const MarkdownRenderer: React.FC<
borderBottomColor="borderColor"
{...commonHeadingProps}
mb={4}
{...props}
{...cleanedProps(props)}
/>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
h2: (props: any) => (
h2: (props) => (
<Heading
as="h3"
size="title.md"
@@ -57,51 +55,51 @@ export const MarkdownRenderer: React.FC<
{...commonHeadingProps}
mt={8}
mb={4}
{...props}
{...cleanedProps(props)}
/>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
h3: (props: any) => (
h3: (props) => (
<Heading
as="h4"
size="title.sm"
{...commonHeadingProps}
{...props}
{...cleanedProps(props)}
mt={4}
/>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
h4: (props: any) => (
h4: (props) => (
<Heading
as="h5"
size="subtitle.md"
{...commonHeadingProps}
{...props}
{...cleanedProps(props)}
mt={4}
/>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
h5: (props: any) => (
h5: (props) => (
<Heading
as="h6"
size="subtitle.sm"
{...commonHeadingProps}
{...props}
{...cleanedProps(props)}
mt={4}
/>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
h6: (props: any) => (
h6: (props) => (
<Heading
as="p"
size="label.md"
{...commonHeadingProps}
{...props}
{...cleanedProps(props)}
mt={4}
/>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
a: (props: any) => (
a: (props) => (
<Link
_dark={{
color: "blue.400",
@@ -116,13 +114,12 @@ export const MarkdownRenderer: React.FC<
},
}}
isExternal
{...props}
{...cleanedProps(props)}
mt={4}
/>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
code: ({ inline, ...props }: any) => {
code: ({ ...props }) => {
if (props?.className) {
const language = props.className.replace("language-", "");
return (
@@ -130,7 +127,7 @@ export const MarkdownRenderer: React.FC<
code={onlyText(props.children).trim()}
language={language}
mb={4}
{...props}
{...cleanedProps(props)}
/>
);
}
@@ -145,16 +142,21 @@ export const MarkdownRenderer: React.FC<
_dark={{ bg: "whiteAlpha.100" }}
borderRadius="md"
fontFamily="mono"
{...props}
{...cleanedProps(props)}
/>
);
},
// biome-ignore lint/suspicious/noExplicitAny: FIXME
p: (props: any) => (
<Text size="body.md" mb={4} {...props} lineHeight={1.5} />
p: (props) => (
<Text
size="body.md"
mb={4}
{...cleanedProps(props)}
lineHeight={1.5}
/>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
table: (props: any) => (
table: (props) => (
<Box
maxW="100%"
overflowX="auto"
@@ -164,53 +166,44 @@ export const MarkdownRenderer: React.FC<
mb={4}
borderTopRadius="lg"
>
<Table {...props} />
<Table {...cleanedProps(props)} />
</Box>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
th: ({ children: c, ...props }: any) => (
<Th
// biome-ignore lint/suspicious/noExplicitAny: FIXME
{...(props as unknown as any)}
textAlign="left!important"
border="none"
>
<Text as="label" size="label.sm" color="faded">
th: ({ children: c, ...props }) => (
<Th {...cleanedProps(props)} textAlign="left" border="none">
<Text as="span" size="label.sm" color="faded">
{c}
</Text>
</Th>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
td: (props: any) => (
td: (props) => (
<Td
// biome-ignore lint/suspicious/noExplicitAny: FIXME
{...(props as unknown as any)}
{...cleanedProps(props)}
borderColor="borderColor"
textAlign="left!important"
textAlign="left"
borderBottomWidth={"inherit"}
/>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
thead: (props: any) => <Thead {...props} />,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
tbody: (props: any) => <Tbody {...props} />,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
tr: (props: any) => (
thead: (props) => <Thead {...cleanedProps(props)} />,
tbody: (props) => <Tbody {...cleanedProps(props)} />,
tr: (props) => (
<Tr
{...props}
{...cleanedProps(props)}
transition="all 0.1s"
borderBottomWidth={1}
_last={{ borderBottomWidth: 0 }}
/>
),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
ul: ({ ordered, ...props }: any) => <UnorderedList {...props} mb={4} />,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
ol: ({ ordered, ...props }: any) => <OrderedList {...props} mb={4} />,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
li: ({ children: c, ordered, ...props }: any) => (
<ListItem {...props}>
<Text>{c}</Text>
ul: (props) => {
return <UnorderedList {...cleanedProps(props)} mb={4} />;
},
ol: (props) => <OrderedList {...cleanedProps(props)} mb={4} />,
li: ({ children: c, ...props }) => (
<ListItem {...cleanedProps(props)}>
{/* hydration error - can not render p > p, so use span instead - remove extra span when we move to tailwind */}
<Text as="span">{c}</Text>
</ListItem>
),
}}
@@ -219,3 +212,11 @@ export const MarkdownRenderer: React.FC<
</ChakraReactMarkdown>
);
};
function cleanedProps<T extends object & { node?: unknown }>(
props: T,
): Omit<T, "node"> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { node, ...rest } = props;
return rest;
}
@@ -5,7 +5,7 @@ import {
AccordionItem,
AccordionPanel,
} from "@chakra-ui/react";
import type { Abi } from "@thirdweb-dev/sdk";
import type { Abi } from "abitype";
import { CodeBlock, Heading } from "tw-components";
import type { SourceFile } from "../types";
@@ -20,6 +20,34 @@ export const SourcesAccordion: React.FC<SourcesAccordionProps> = ({
}) => {
return (
<Accordion allowMultiple defaultIndex={[]}>
{/* ABI Accordion is put at the top for better UX */}
{abi && (
<AccordionItem
gap={4}
flexDirection="column"
borderColor="borderColor"
_first={{ borderTopWidth: 0 }}
_last={{ borderBottomWidth: 0 }}
>
{({ isExpanded }) => (
<>
<AccordionButton justifyContent="space-between" py={2}>
<Heading size="label.md">ABI</Heading>
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
{isExpanded && (
<CodeBlock
code={JSON.stringify(abi, null, 2)}
language="json"
overflow="auto"
/>
)}
</AccordionPanel>
</>
)}
</AccordionItem>
)}
{sources.map((signature) => (
<AccordionItem
gap={4}
@@ -48,33 +76,6 @@ export const SourcesAccordion: React.FC<SourcesAccordionProps> = ({
)}
</AccordionItem>
))}
{abi && (
<AccordionItem
gap={4}
flexDirection="column"
borderColor="borderColor"
_first={{ borderTopWidth: 0 }}
_last={{ borderBottomWidth: 0 }}
>
{({ isExpanded }) => (
<>
<AccordionButton justifyContent="space-between" py={2}>
<Heading size="label.md">ABI</Heading>
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
{isExpanded && (
<CodeBlock
code={JSON.stringify(abi, null, 2)}
language="json"
overflow="auto"
/>
)}
</AccordionPanel>
</>
)}
</AccordionItem>
)}
</Accordion>
);
};
@@ -1,5 +1,5 @@
import { Flex } from "@chakra-ui/react";
import type { Abi } from "@thirdweb-dev/sdk";
import type { Abi } from "abitype";
import { Link, Text } from "tw-components";
import type { SourceFile } from "../types";
import { SourcesAccordion } from "./sources-accordion";
@@ -1,8 +1,8 @@
import { thirdwebClient } from "@/constants/client";
import { useDashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import { Skeleton } from "@chakra-ui/react";
import type { BasicContract } from "contract-ui/types/types";
import { useChainSlug } from "hooks/chains/chainSlug";
import { thirdwebClient } from "lib/thirdweb-client";
import { useV5DashboardChain } from "lib/v5-adapter";
import { memo } from "react";
import { getContract } from "thirdweb";
@@ -416,6 +416,7 @@ const ContractTable: ComponentWithChildren<ContractTableProps> = ({
overflowX={{ base: "auto", md: "initial" }}
// to avoid clipping the network selector menu on mobile
minH={{ base: "600px", md: "initial" }}
className="relative"
>
{isFetching && (
<Spinner
@@ -6,16 +6,12 @@ import {
detectFeatures as detectFeaturesFromSdk,
isExtensionEnabled,
} from "@thirdweb-dev/sdk";
import type { Signer } from "ethers";
import { getDashboardChainRpc } from "lib/rpc";
import { getThirdwebSDK } from "lib/sdk";
import { thirdwebClient } from "lib/thirdweb-client";
import { MULTICHAIN_REGISTRY_CONTRACT } from "constants/contracts";
import {
getContract,
prepareContractCall,
sendAndConfirmTransaction,
} from "thirdweb";
import { polygon } from "thirdweb/chains";
DASHBOARD_ENGINE_RELAYER_URL,
DASHBOARD_FORWARDER_ADDRESS,
} from "constants/misc";
import { prepareContractCall, sendAndConfirmTransaction } from "thirdweb";
import type { Account } from "thirdweb/wallets";
export function detectFeatures<TContract extends ValidContractInstance | null>(
@@ -43,39 +39,13 @@ export function detectFeatures<TContract extends ValidContractInstance | null>(
);
}
export function getGaslessPolygonSDK(signer?: Signer) {
const polygonSDK = getThirdwebSDK(
polygon.id,
getDashboardChainRpc(polygon.id, undefined),
{
gasless: {
engine: {
relayerUrl:
"https://checkout.engine.thirdweb.com/relayer/0c2bdd3a-307f-4243-b6e5-5ba495222d2b",
relayerForwarderAddress: "0x409d530a6961297ece29121dbee2c917c3398659",
},
experimentalChainlessSupport: true,
},
},
signer,
);
return polygonSDK;
}
const contractAddress = "0xcdAD8FA86e18538aC207872E8ff3536501431B73";
const registry = getContract({
chain: polygon,
client: thirdwebClient,
address: contractAddress,
});
export async function addContractToMultiChainRegistry(
contractData: AddContractInput,
account: Account,
gasOverride?: bigint,
) {
const transaction = prepareContractCall({
contract: registry,
contract: MULTICHAIN_REGISTRY_CONTRACT,
method: "function add(address, address, uint256, string)",
params: [
account.address,
@@ -94,9 +64,8 @@ export async function addContractToMultiChainRegistry(
gasless: {
experimentalChainlessSupport: true,
provider: "engine",
relayerUrl:
"https://checkout.engine.thirdweb.com/relayer/0c2bdd3a-307f-4243-b6e5-5ba495222d2b",
relayerForwarderAddress: "0x409d530a6961297ece29121dbee2c917c3398659",
relayerUrl: DASHBOARD_ENGINE_RELAYER_URL,
relayerForwarderAddress: DASHBOARD_FORWARDER_ADDRESS,
},
});
}
@@ -1,3 +1,4 @@
import { thirdwebClient } from "@/constants/client";
import {
Box,
Divider,
@@ -29,8 +30,10 @@ import {
import { useContractEnabledExtensions } from "components/contract-components/hooks";
import { MarkdownRenderer } from "components/contract-components/published-contract/markdown-renderer";
import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers";
import { useV5DashboardChain } from "lib/v5-adapter";
import { useRouter } from "next/router";
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
import { getContract } from "thirdweb";
import { Badge, Button, Card, Heading, Text } from "tw-components";
import {
COMMANDS,
@@ -66,6 +69,16 @@ const ContractFunction: React.FC<ContractFunctionProps> = ({
return undefined;
}, [enabledExtensions]);
const chain = useV5DashboardChain(contract?.chainId);
const contractV5 =
chain && contract
? getContract({
address: contract.getAddress(),
chain,
client: thirdwebClient,
})
: undefined;
if (!fn) {
return null;
}
@@ -164,10 +177,10 @@ const ContractFunction: React.FC<ContractFunctionProps> = ({
</>
) : null}
{contract && isFunction && (
{contractV5 && isFunction && (
<InteractiveAbiFunction
key={JSON.stringify(fn)}
contract={contract}
contract={contractV5}
abiFunction={fn}
/>
)}
@@ -104,6 +104,7 @@ export const ContractFunctionsOverview: React.FC<ContractFunctionsOverview> = ({
)}
{(sources || abi) && (
<TabPanel>
{/* @ts-expect-error This won't cause any error and will auto-resolve itself once we have fully migrated to v5 */}
<SourcesPanel sources={sources} abi={abi} />
</TabPanel>
)}
@@ -8,17 +8,24 @@ import {
Input,
} from "@chakra-ui/react";
import { useMutation } from "@tanstack/react-query";
import { useContractWrite } from "@thirdweb-dev/react";
import type { AbiFunction, SmartContract } from "@thirdweb-dev/sdk";
import type { AbiFunction } from "@thirdweb-dev/sdk";
import { TransactionButton } from "components/buttons/TransactionButton";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers";
import { BigNumber, type CallOverrides, utils } from "ethers";
import { replaceIpfsUrl } from "lib/sdk";
import { useEffect, useId, useMemo } from "react";
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import { FiPlay } from "react-icons/fi";
import invariant from "tiny-invariant";
import { toast } from "sonner";
import {
type ThirdwebContract,
prepareContractCall,
readContract,
resolveMethod,
toWei,
} from "thirdweb";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import { parseAbiParams, stringify } from "thirdweb/utils";
import {
Button,
Card,
@@ -37,9 +44,8 @@ function formatResponseData(data: unknown): string {
if (typeof data === "string") {
return data;
}
if (BigNumber.isBigNumber(data)) {
// biome-ignore lint/style/noParameterAssign: FIXME
data = data.toString();
if (typeof data === "bigint") {
return data.toString();
}
if (typeof data === "object") {
@@ -56,7 +62,7 @@ function formatResponseData(data: unknown): string {
}
}
return JSON.stringify(data, null, 2);
return stringify(data, null, 2);
}
function formatError(error: Error): string {
@@ -87,7 +93,6 @@ function formatContractCall(
}[]
| undefined;
}[],
value?: BigNumber,
) {
const parsedParams = params
.map((p) => (p.type === "bool" ? p.value !== "false" : p.value))
@@ -104,31 +109,23 @@ function formatContractCall(
return p;
}
});
if (value) {
parsedParams.push({
value,
});
}
return parsedParams;
}
interface InteractiveAbiFunctionProps {
abiFunction?: AbiFunction;
contract: SmartContract;
abiFunction: AbiFunction;
contract: ThirdwebContract;
}
function useDelayedRead(
contract: Required<SmartContract> | undefined,
functionName?: string,
) {
function useAsyncRead(contract: ThirdwebContract, functionName: string) {
return useMutation(
// biome-ignore lint/suspicious/noExplicitAny: FIXME
async ({ args, overrides }: { args: any[]; overrides?: CallOverrides }) => {
invariant(contract, "Contract is required");
invariant(functionName, "functionName is required");
return contract?.call(functionName, args, overrides);
async ({ args, types }: { args: unknown[]; types: string[] }) => {
const params = parseAbiParams(types, args);
return readContract({
contract,
method: resolveMethod(functionName),
params,
});
},
);
}
@@ -141,7 +138,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
const form = useForm({
defaultValues: {
params:
abiFunction?.inputs.map((i) => ({
abiFunction.inputs.map((i) => ({
key: i.name || "key",
value: "",
type: i.type,
@@ -166,15 +163,20 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
mutate,
data,
error: mutationError,
isLoading: mutationLoading,
} = useContractWrite(contract, abiFunction?.name);
isPending: mutationLoading,
} = useSendAndConfirmTransaction();
const {
mutate: readFn,
data: readData,
isLoading: readLoading,
error: readError,
} = useDelayedRead(contract, abiFunction?.name);
} = useAsyncRead(contract, abiFunction.name);
const formattedReadData: string = useMemo(
() => (readData ? formatResponseData(readData) : ""),
[readData],
);
const error = isView ? readError : mutationError;
@@ -183,12 +185,15 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
useEffect(() => {
if (
form.watch("params").length === 0 &&
(abiFunction?.stateMutability === "view" ||
abiFunction?.stateMutability === "pure")
(abiFunction.stateMutability === "view" ||
abiFunction.stateMutability === "pure")
) {
readFn({ args: [] });
readFn({
args: [],
types: [],
});
}
}, [readFn, abiFunction?.stateMutability, form]);
}, [abiFunction, form, readFn]);
return (
<FormProvider {...form}>
@@ -217,19 +222,24 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
const formatted = formatContractCall(d.params);
if (
contract &&
(abiFunction?.stateMutability === "view" ||
abiFunction?.stateMutability === "pure")
(abiFunction.stateMutability === "view" ||
abiFunction.stateMutability === "pure")
) {
readFn({
args: formatted,
});
const types = abiFunction.inputs.map((o) => o.type);
readFn({ args: formatted, types });
} else {
mutate({
args: formatted,
overrides: d.value
? { value: utils.parseEther(d.value) }
: undefined,
if (!abiFunction.name) {
return toast.error("Cannot detect function name");
}
const types = abiFunction.inputs.map((o) => o.type);
const params = parseAbiParams(types, formatted);
const transaction = prepareContractCall({
contract,
method: resolveMethod(abiFunction.name),
params,
value: d.value ? toWei(d.value) : undefined,
});
mutate(transaction);
}
}
})}
@@ -258,7 +268,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
solidityType={item.type}
solidityComponents={item.components}
{...form.register(`params.${index}.value`)}
functionName={abiFunction?.name}
functionName={abiFunction.name}
/>
<FormErrorMessage>
{
@@ -274,7 +284,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
</>
)}
{abiFunction?.stateMutability === "payable" && (
{abiFunction.stateMutability === "payable" && (
<>
<Divider mb="8px" />
<FormControl gap={0.5}>
@@ -318,19 +328,18 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
language="json"
code={formatResponseData(data || readData)}
/>
{typeof readData === "string" &&
readData?.startsWith("ipfs://") && (
<Text size="label.sm">
<TrackedLink
href={replaceIpfsUrl(readData)}
isExternal
category="contract-explorer"
label="open-in-gateway"
>
Open in gateway
</TrackedLink>
</Text>
)}
{formattedReadData.startsWith("ipfs://") && (
<Text size="label.sm">
<TrackedLink
href={replaceIpfsUrl(formattedReadData)}
isExternal
category="contract-explorer"
label="open-in-gateway"
>
Open in gateway
</TrackedLink>
</Text>
)}
</>
) : null}
</Flex>
@@ -9,7 +9,6 @@ import {
Stack,
Tooltip,
} from "@chakra-ui/react";
import type { OptionalPropertiesInput } from "@thirdweb-dev/sdk";
import { FileInput } from "components/shared/FileInput";
import { useEffect } from "react";
import {
@@ -27,10 +26,13 @@ import {
} from "react-hook-form";
import { FiPlus, FiSlash, FiTrash, FiUpload, FiX } from "react-icons/fi";
import { Button, FormErrorMessage, FormLabel } from "tw-components";
import type { z } from "zod";
type OptionalPropertiesInput = {
[key: string]: string | number;
};
interface IPropertyFieldValues extends FieldValues {
attributes?: z.input<typeof OptionalPropertiesInput>;
attributes?: OptionalPropertiesInput;
}
interface IPropertiesFormControlProps<
@@ -0,0 +1,42 @@
import { useRouter } from "next/router";
import type { EngineInstance } from "../../@3rdweb-sdk/react/hooks/useEngine";
import { PageId } from "../../page-id";
import type { ThirdwebNextPage } from "../../utils/types";
import { AppLayout } from "../app-layouts/app";
import {
type ActivePage,
EnginePageLayout,
PageLoading,
} from "./EnginePageLayout";
export function createEnginePage(
activePage: ActivePage,
PageContent: React.FC<{ instance: EngineInstance }>,
): ThirdwebNextPage {
const Page: ThirdwebNextPage = () => {
const router = useRouter();
const engineId = router.query.engineId;
if (typeof engineId !== "string") {
return <PageLoading />;
}
return (
<EnginePageLayout
activePage={activePage}
engineId={engineId}
content={PageContent}
/>
);
};
Page.pageId = PageId.EngineManage;
Page.getLayout = (page, props) => (
<AppLayout {...props} hasSidebar={true}>
{page}
</AppLayout>
);
return Page;
}
@@ -0,0 +1,331 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { ArrowLeftIcon, CircleAlertIcon } from "lucide-react";
import Link from "next/link";
import { useCallback, useMemo } from "react";
import invariant from "tiny-invariant";
import { CopyTextButton } from "../../@/components/ui/CopyTextButton";
import { Button } from "../../@/components/ui/button";
import { Separator } from "../../@/components/ui/separator";
import { useDashboardRouter } from "../../@/lib/DashboardRouter";
import {
type EngineInstance,
useEngineInstances,
} from "../../@3rdweb-sdk/react/hooks/useEngine";
import { SidebarNav } from "../../core-ui/sidebar/nav";
import type { Route } from "../../core-ui/sidebar/types";
import { useTrack } from "../../hooks/analytics/useTrack";
import { EngineVersionBadge } from "./badges/version";
import { useHasEnginePermission } from "./useHasEnginePermission";
export type ActivePage =
| "overview"
| "explorer"
| "relayers"
| "contract-subscriptions"
| "admins"
| "access-tokens"
| "webhooks"
| "configuration"
| "metrics";
const NEXT_PUBLIC_DEMO_ENGINE_URL = process.env.NEXT_PUBLIC_DEMO_ENGINE_URL;
export function EnginePageLayout(props: {
engineId: string;
activePage: ActivePage;
content: React.FC<{ instance: EngineInstance }>;
}) {
const { data } = useAccount();
if (props.engineId === "sandbox" && data) {
invariant(
NEXT_PUBLIC_DEMO_ENGINE_URL,
"missing NEXT_PUBLIC_DEMO_ENGINE_URL",
);
const sandboxEngine: EngineInstance = {
id: "sandbox",
url: NEXT_PUBLIC_DEMO_ENGINE_URL,
name: "Demo Engine",
status: "active",
lastAccessedAt: new Date().toISOString(),
accountId: data?.id,
};
return (
<RenderInstance
instance={sandboxEngine}
activePage={props.activePage}
content={props.content}
/>
);
}
return <QueryAndRenderInstance {...props} />;
}
function QueryAndRenderInstance(props: {
activePage: ActivePage;
engineId: string;
content: React.FC<{ instance: EngineInstance }>;
}) {
const instancesQuery = useEngineInstances();
const instance = instancesQuery.data?.find((x) => x.id === props.engineId);
if (instancesQuery.isLoading) {
return <PageLoading />;
}
if (!instance) {
return <EngineErrorPage>Engine Instance Not Found</EngineErrorPage>;
}
return (
<EnsurePermissionAndRenderInstance
instance={instance}
activePage={props.activePage}
content={props.content}
/>
);
}
function EnsurePermissionAndRenderInstance(props: {
activePage: ActivePage;
content: React.FC<{ instance: EngineInstance }>;
instance: EngineInstance;
}) {
const permissionQuery = useHasEnginePermission({
instanceUrl: props.instance.url,
});
if (permissionQuery.isLoading) {
return <PageLoading />;
}
if (permissionQuery.error instanceof Error) {
if (permissionQuery.error.message.includes("Failed to fetch")) {
return (
<EngineErrorPage>
<p>Unable to connect to Engine</p>
<p>Ensure that your Engine is publicly accessible</p>
</EngineErrorPage>
);
}
return (
<EngineErrorPage>
<p>There was an unexpected error reaching your Engine instance</p>
<p>Try again or contact us if this issue persists.</p>
</EngineErrorPage>
);
}
if (
permissionQuery.data &&
permissionQuery.data.hasPermission === false &&
permissionQuery.data.reason === "Unauthorized"
) {
return (
<div>
You are not an admin for this Engine instance. Contact the owner to add
your wallet as an admin
</div>
);
}
return (
<RenderInstance
instance={props.instance}
activePage={props.activePage}
content={props.content}
/>
);
}
function RenderInstance(props: {
instance: EngineInstance;
activePage: ActivePage;
content: React.FC<{ instance: EngineInstance }>;
}) {
const { instance, activePage } = props;
const router = useDashboardRouter();
const trackEvent = useTrack();
const handleClick = useCallback(
(id: ActivePage) => {
trackEvent({
category: "engine",
action: "navigate-tab",
label: id,
url: instance.url,
});
if (id === "overview") {
router.push(`/dashboard/engine/${instance.id}`);
} else {
router.push(`/dashboard/engine/${instance.id}/${id}`);
}
},
[instance.url, instance.id, trackEvent, router],
);
const links = useMemo(
() =>
[
{
path: `/dashboard/engine/${instance.id}`,
title: "Overview",
name: "overview",
onClick: () => handleClick("overview"),
},
{
path: `/dashboard/engine/${instance.id}/explorer`,
title: "Explorer",
name: "explorer",
onClick: () => handleClick("explorer"),
},
{
path: `/dashboard/engine/${instance.id}/relayers`,
title: "Relayers",
name: "relayers",
onClick: () => handleClick("relayers"),
},
{
path: `/dashboard/engine/${instance.id}/contract-subscriptions`,
title: "Contract Subscriptions",
name: "contract-subscriptions",
onClick: () => handleClick("contract-subscriptions"),
},
{
path: `/dashboard/engine/${instance.id}/admins`,
title: "Admins",
name: "admins",
onClick: () => handleClick("admins"),
},
{
path: `/dashboard/engine/${instance.id}/access-tokens`,
title: "Access Tokens",
name: "access-tokens",
onClick: () => handleClick("access-tokens"),
},
{
path: `/dashboard/engine/${instance.id}/webhooks`,
title: "Webhooks",
name: "webhooks",
onClick: () => handleClick("webhooks"),
},
{
path: `/dashboard/engine/${instance.id}/configuration`,
title: "Configuration",
name: "configuration",
onClick: () => handleClick("configuration"),
},
{
path: `/dashboard/engine/${instance.id}/metrics`,
title: "Metrics",
name: "metrics",
onClick: () => handleClick("metrics"),
},
] satisfies Route[],
[handleClick, instance.id],
);
return (
<>
<SidebarNav links={links} activePage={activePage} title="Engine" />
<div className="flex">
<Button
variant="ghost"
className="px-2 py-1 -translate-x-2 flex items-center gap-2 text-muted-foreground hover:text-foreground h-auto"
>
<Link
href="/dashboard/engine"
aria-label="Go Back"
className="flex items-center gap-2 text-muted-foreground hover:text-foreground"
>
<ArrowLeftIcon className="size-4" /> Back
</Link>
</Button>
</div>
<div className="h-5" />
<div className="flex flex-col lg:flex-row lg:items-center gap-4 lg:justify-between">
<div>
<h1 className="text-3xl md:text-5xl font-semibold tracking-tighter">
{instance.name}
</h1>
<div className="h-1" />
<div className="flex gap-3 items-center">
{!instance.name.startsWith("https://") && (
<CopyTextButton
copyIconPosition="right"
textToShow={instance.url}
textToCopy={instance.url}
tooltip="Copy Engine URL"
variant="ghost"
className="px-2 py-1 -translate-x-2 h-auto text-muted-foreground"
/>
)}
</div>
</div>
<EngineVersionBadge instance={instance} />
</div>
<div className="h-5" />
<Separator />
<div className="h-10 " />
<props.content instance={instance} />
</>
);
}
function EngineErrorPage(props: {
children: React.ReactNode;
}) {
return (
<div>
<Link
href="/dashboard/engine"
className="gap-2 flex items-center text-muted-foreground hover:text-foreground"
>
<ArrowLeftIcon className="size-5" />
Back
</Link>
<div className="border rounded-lg border-border mt-5">
<MessageContainer>
<div className="flex flex-col gap-4 items-center">
<CircleAlertIcon className="size-16 text-destructive-text" />
<p className="text-secondary-foreground text-center">
{props.children}
</p>
</div>
</MessageContainer>
</div>
</div>
);
}
export function MessageContainer(props: {
children: React.ReactNode;
}) {
return (
<div className="h-[300px] lg:h-[400px] flex items-center justify-center">
{props.children}
</div>
);
}
export function PageLoading() {
return (
<MessageContainer>
<Spinner className="size-10" />
</MessageContainer>
);
}
@@ -0,0 +1,26 @@
import { SidebarNav } from "../../core-ui/sidebar/nav";
export function EngineSidebar() {
return (
<SidebarNav
title="Engine"
links={[
{
name: "overview",
path: "/dashboard/engine",
title: "Overview",
},
{
name: "create",
path: "/dashboard/engine/create",
title: "Create",
},
{
name: "import",
path: "/dashboard/engine/import",
title: "Import",
},
]}
/>
);
}
@@ -1,27 +1,25 @@
import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import {
type EngineInstance,
useEngineLatestVersion,
useEngineSystemHealth,
useEngineUpdateVersion,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { CircleArrowDownIcon, CloudDownloadIcon } from "lucide-react";
import { useState } from "react";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import {
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
Tag,
TagLabel,
TagLeftIcon,
Tooltip,
type UseDisclosureReturn,
useDisclosure,
} from "@chakra-ui/react";
import { useTxNotifications } from "hooks/useTxNotifications";
import { FaArrowCircleUp } from "react-icons/fa";
import { Button, Text, TrackedLink } from "tw-components";
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { toast } from "sonner";
export const EngineVersionBadge = ({
instance,
@@ -30,7 +28,7 @@ export const EngineVersionBadge = ({
}) => {
const healthQuery = useEngineSystemHealth(instance.url);
const latestVersionQuery = useEngineLatestVersion();
const disclosure = useDisclosure();
const [isModalOpen, setModalOpen] = useState(false);
const current = healthQuery.data?.engineVersion ?? "...";
const latest = latestVersionQuery.data ?? "...";
@@ -38,112 +36,141 @@ export const EngineVersionBadge = ({
if (!isStale) {
return (
<Tag size="sm">
<TagLabel fontSize="small">{current}</TagLabel>
</Tag>
<ToolTipLabel label="Latest Version">
<Button variant="outline" asChild className="hover:bg-transparent">
<div>{current}</div>
</Button>
</ToolTipLabel>
);
}
return (
<>
<Tooltip label="Update to the latest version">
<Button variant="unstyled">
<Tag
onClick={disclosure.onOpen}
size="sm"
colorScheme="blue"
variant="outline"
>
<TagLeftIcon as={FaArrowCircleUp} boxSize={3} />
<TagLabel fontSize="small">{current}</TagLabel>
</Tag>
</Button>
</Tooltip>
<ToolTipLabel
label="New version is available"
leftIcon={
<CircleArrowDownIcon className="size-4 text-link-foreground" />
}
>
<Button
variant="outline"
className="relative"
onClick={() => setModalOpen(true)}
>
{current}
{disclosure.isOpen && (
<UpdateVersionModal
disclosure={disclosure}
latest={latest ?? ""}
instance={instance}
/>
)}
{/* Notification Dot */}
<span className="absolute -top-1 -right-1">
<PulseDot />
</span>
</Button>
</ToolTipLabel>
<UpdateVersionModal
open={isModalOpen}
onOpenChange={setModalOpen}
latest={latest ?? ""}
instance={instance}
/>
</>
);
};
const UpdateVersionModal = ({
disclosure,
latest,
instance,
}: {
disclosure: UseDisclosureReturn;
const UpdateVersionModal = (props: {
open: boolean;
onOpenChange: (open: boolean) => void;
latest: string;
instance: EngineInstance;
}) => {
const { mutate } = useEngineUpdateVersion();
const { onSuccess, onError } = useTxNotifications(
"Submitted a request to update your Engine instance. Please allow 1-2 business days for this process.",
"Unexpected error updating your Engine instance.",
);
const { open, onOpenChange, latest, instance } = props;
const updateEngine = useEngineUpdateVersion();
if (!instance.cloudDeployedAt) {
// For self-hosted, show a prompt to the Github release page.
return (
<Modal isOpen={disclosure.isOpen} onClose={disclosure.onClose}>
<ModalContent>
<ModalCloseButton />
<ModalHeader>Update your self-hosted Engine to {latest}</ModalHeader>
<ModalBody>
<Text>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="z-[10001] max-w-[400px]"
dialogOverlayClassName="z-[10000]"
>
<DialogHeader>
<DialogTitle className="text-2xl font-semibold tracking-tight pr-4 mb-6">
Update your self-hosted Engine to {latest}
</DialogTitle>
<DialogDescription>
View the changelog in the{" "}
<TrackedLink
<TrackedLinkTW
href="https://github.com/thirdweb-dev/engine/releases"
category="engine"
label="clicked-engine-releases"
isExternal
color="blue.500"
target="_blank"
className="text-link-foreground hover:text-foreground"
>
Engine Github repository
</TrackedLink>
.
</Text>
</ModalBody>
<ModalFooter as={Flex} gap={3}>
<Button type="button" onClick={disclosure.onClose} variant="ghost">
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</TrackedLinkTW>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
);
}
const onClick = () => {
const onClick = async () => {
try {
mutate({ engineId: instance.id });
onSuccess();
} catch (e) {
onError(e);
const promise = updateEngine.mutateAsync({ engineId: instance.id });
toast.promise(promise, {
success:
"Submitted a request to update your Engine instance. Please allow 1-2 business days for this process.",
error: "Unexpected error updating your Engine instance.",
});
await promise;
} finally {
onOpenChange(false);
}
disclosure.onClose();
};
return (
<Modal isOpen={disclosure.isOpen} onClose={disclosure.onClose}>
<ModalContent>
<ModalCloseButton />
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="z-[10001] max-w-[400px]"
dialogOverlayClassName="z-[10000]"
>
<DialogHeader>
<DialogTitle>Update Engine to {latest}?</DialogTitle>
</DialogHeader>
<ModalHeader>Update Engine to {latest}?</ModalHeader>
<ModalFooter as={Flex} gap={3}>
<Button type="button" onClick={disclosure.onClose} variant="ghost">
<DialogFooter className="mt-5">
<Button
type="button"
onClick={() => onOpenChange(false)}
variant="outline"
>
Close
</Button>
<Button type="submit" onClick={onClick} colorScheme="blue">
<Button
type="submit"
onClick={onClick}
variant="primary"
className="gap-2"
>
{updateEngine.isLoading ? (
<Spinner className="size-4" />
) : (
<CloudDownloadIcon className="size-4" />
)}
Update
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
function PulseDot() {
return (
<span className="relative flex size-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75" />
<span className="relative inline-flex rounded-full size-2 bg-primary" />
</span>
);
}
@@ -1,7 +1,9 @@
import { thirdwebClient } from "@/constants/client";
import {
type AddContractSubscriptionInput,
useEngineAddContractSubscription,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { useResolveContractAbi } from "@3rdweb-sdk/react/hooks/useResolveContractAbi";
import {
CheckboxGroup,
Collapse,
@@ -25,11 +27,12 @@ import {
} from "@chakra-ui/react";
import { NetworkDropdown } from "components/contract-components/contract-publish-form/NetworkDropdown";
import { useTrack } from "hooks/analytics/useTrack";
import { useContractAbiItems } from "hooks/useContractAbiItems";
import { useTxNotifications } from "hooks/useTxNotifications";
import { useV5DashboardChain } from "lib/v5-adapter";
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
import { type UseFormReturn, useForm } from "react-hook-form";
import { AiOutlinePlusCircle } from "react-icons/ai";
import { getContract } from "thirdweb";
import {
Button,
Card,
@@ -421,32 +424,66 @@ const FilterSelector = ({
filter: string[];
setFilter: (value: string[]) => void;
}) => {
const abiItemsQuery = useContractAbiItems(
form.getValues("chainId"),
form.getValues("contractAddress") as `0x${string}`,
);
const chain = useV5DashboardChain(form.getValues("chainId"));
const contract = chain
? getContract({
address: form.getValues("contractAddress"),
chain,
client: thirdwebClient,
})
: undefined;
const abiQuery = useResolveContractAbi(contract);
const abiItems = useMemo(() => {
if (!abiQuery.data) {
return {
readFunctions: [],
writeFunctions: [],
events: [],
};
}
const readFunctions: string[] = [];
const writeFunctions: string[] = [];
const events: string[] = [];
for (const abiItem of abiQuery.data) {
if (abiItem.type === "function") {
if (
abiItem.stateMutability === "pure" ||
abiItem.stateMutability === "view"
) {
readFunctions.push(abiItem.name);
} else {
writeFunctions.push(abiItem.name);
}
} else if (abiItem.type === "event") {
events.push(abiItem.name);
}
}
return {
readFunctions: [...new Set(readFunctions)].sort(),
writeFunctions: [...new Set(writeFunctions)].sort(),
events: [...new Set(events)].sort(),
};
}, [abiQuery.data]);
const filterNames = useMemo(() => {
switch (abiItemType) {
case "function": {
return abiItemsQuery.data.writeFunctions;
return abiItems.writeFunctions;
}
case "event": {
return abiItemsQuery.data.events;
return abiItems.events;
}
default: {
return [];
}
}
}, [
abiItemType,
abiItemsQuery.data.events,
abiItemsQuery.data.writeFunctions,
]);
}, [abiItemType, abiItems.events, abiItems.writeFunctions]);
return (
<Card>
{abiItemsQuery.isLoading ? (
{abiQuery.isLoading ? (
<Spinner size="sm" />
) : filterNames.length === 0 ? (
<Text>
@@ -1,3 +1,4 @@
import { thirdwebClient } from "@/constants/client";
import {
type EngineContractSubscription,
useEngineRemoveContractSubscription,
@@ -26,7 +27,6 @@ import { format } from "date-fns";
import { useTrack } from "hooks/analytics/useTrack";
import { useAllChainsData } from "hooks/chains/allChains";
import { useTxNotifications } from "hooks/useTxNotifications";
import { thirdwebClient } from "lib/thirdweb-client";
import { useV5DashboardChain } from "lib/v5-adapter";
import { useState } from "react";
import { FiInfo, FiTrash } from "react-icons/fi";
@@ -1,257 +0,0 @@
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import type { EngineTier } from "@3rdweb-sdk/react/hooks/useEngine";
import {
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
SimpleGrid,
type UseDisclosureReturn,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { ChakraNextImage } from "components/Image";
import { OnboardingModal } from "components/onboarding/Modal";
import { THIRDWEB_API_HOST } from "constants/urls";
import { useTrack } from "hooks/analytics/useTrack";
import { useSingleQueryParam } from "hooks/useQueryParam";
import { useRouter } from "next/router";
import { useState } from "react";
import { Button, Heading, Text } from "tw-components";
import { LazyOnboardingBilling } from "../onboarding/LazyOnboardingBilling";
import { EngineTierCard, MONTHLY_PRICE_USD } from "./tier-card";
interface CreateEngineInstanceButtonProps {
ctaText: string;
refetch: () => void;
}
export const CreateEngineInstanceButton = ({
ctaText,
refetch,
}: CreateEngineInstanceButtonProps) => {
const showModalOnLoad = useSingleQueryParam("requestCloudHosted");
const disclosure = useDisclosure({
defaultIsOpen: showModalOnLoad !== undefined,
});
const paymentDisclosure = useDisclosure();
const trackEvent = useTrack();
const toast = useToast();
const accountQuery = useAccount();
const [engineTier, setEngineTier] = useState<EngineTier>("STARTER");
const addCloudHostedEngine = async (tier?: EngineTier) => {
trackEvent({
category: "engine",
action: "click",
label: "clicked-cloud-hosted",
tier: tier ?? engineTier,
});
try {
const res = await fetch(
`${THIRDWEB_API_HOST}/v1/engine/add-cloud-hosted`,
{
method: "POST",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify({
tier: tier ?? engineTier,
}),
},
);
if (!res.ok) {
if (res.status === 409) {
toast({
status: "warning",
description:
"There is a pending Engine deployment. Please contact [email protected] if this takes longer than 2 hours.",
});
return;
}
const json = await res.json();
const error =
json.error?.message ??
"Unexpected error. Please try again or visit https://thirdweb.com/support.";
throw error;
}
toast({
status: "success",
description: "Thank you! Your Engine deployment will begin shortly.",
});
// Refresh the engine list to show a pending cloud-hosted Engine instance.
refetch();
} catch (e) {
toast({
status: "error",
description: `${e}`,
});
}
};
return (
<>
<Button
onClick={() => {
trackEvent({
category: "engine",
action: "click",
label: "add-engine-instance",
});
disclosure.onOpen();
}}
colorScheme="blue"
px={6}
>
{ctaText}
</Button>
{disclosure.isOpen && (
<CreateCloudHostedEngineModal
disclosure={disclosure}
onClickAddToPlan={async (tier: EngineTier) => {
setEngineTier(tier);
disclosure.onClose();
if (accountQuery.data?.status === AccountStatus.ValidPayment) {
await addCloudHostedEngine(tier);
} else {
trackEvent({
category: "engine",
action: "click",
label: "clicked-add-payment-method",
});
// Switch the Cloud-hosted Engine modal with the Add Payment modal.
paymentDisclosure.onOpen();
}
}}
/>
)}
<OnboardingModal
isOpen={paymentDisclosure.isOpen}
onClose={paymentDisclosure.onClose}
>
<LazyOnboardingBilling
onSave={async () => {
await addCloudHostedEngine();
paymentDisclosure.onClose();
}}
onCancel={paymentDisclosure.onClose}
/>
</OnboardingModal>
</>
);
};
const CreateCloudHostedEngineModal = ({
disclosure,
onClickAddToPlan,
}: {
disclosure: UseDisclosureReturn;
onClickAddToPlan: (tier: EngineTier) => void;
}) => {
const trackEvent = useTrack();
const router = useRouter();
const [modalState, setModalState] = useState<
"selectTier" | "confirmStandard" | "confirmPremium"
>("selectTier");
if (modalState === "confirmStandard" || modalState === "confirmPremium") {
const tier: EngineTier =
modalState === "confirmStandard" ? "STARTER" : "PREMIUM";
return (
<Modal
isOpen={disclosure.isOpen}
onClose={disclosure.onClose}
isCentered
size="lg"
>
<ModalOverlay />
<ModalContent p={4} pt={8}>
<ModalCloseButton />
<ModalBody as={Flex} flexDir="column" gap={6}>
<ChakraNextImage
alt="Engine hero image"
src={require("../../../public/assets/engine/empty-state-header.png")}
w="fit-content"
/>
<Heading size="title.sm">
Are you sure you want to deploy a{" "}
{tier === "STARTER" ? "Standard" : "Premium"} Engine?
</Heading>
<Text>
You will be charged ${MONTHLY_PRICE_USD[tier]} per month for the
subscription.
</Text>
</ModalBody>
<ModalFooter as={Flex} alignItems="flex-end" gap={3}>
<Button onClick={() => setModalState("selectTier")} variant="ghost">
Back
</Button>
<Button onClick={() => onClickAddToPlan(tier)} colorScheme="blue">
Deploy now
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
return (
<Modal
isOpen={disclosure.isOpen}
onClose={disclosure.onClose}
isCentered
size="6xl"
>
<ModalOverlay />
<ModalContent p={4} pb={8}>
<ModalCloseButton />
<ModalHeader>
<Heading size="title.sm">Choose an Engine deployment</Heading>
</ModalHeader>
<ModalBody as={Flex} flexDir="column" gap={4}>
<Text>
Host Engine on thirdweb with no setup or maintenance required.
</Text>
<SimpleGrid columns={{ base: 1, lg: 3 }} gap={6}>
<EngineTierCard
tier="STARTER"
onClick={() => setModalState("confirmStandard")}
/>
<EngineTierCard
tier="PREMIUM"
isPrimaryCta
onClick={() => setModalState("confirmPremium")}
/>
<EngineTierCard
tier="ENTERPRISE"
previousTier="Premium Engine"
onClick={() => {
trackEvent({
category: "engine",
action: "click",
label: "clicked-cloud-hosted",
tier: "ENTERPRISE",
});
router.push("/contact-us");
}}
/>
</SimpleGrid>
</ModalBody>
</ModalContent>
</Modal>
);
};
@@ -1,3 +1,13 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ToolTipLabel } from "@/components/ui/tooltip";
import {
type EditEngineInstanceInput,
type EngineInstance,
@@ -7,52 +17,38 @@ import {
useEngineRemoveCloudHosted,
useEngineRemoveFromDashboard,
} from "@3rdweb-sdk/react/hooks/useEngine";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import {
Alert,
AlertDescription,
AlertTitle,
Flex,
FormControl,
HStack,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Radio,
RadioGroup,
Stack,
Textarea,
Tooltip,
type UseDisclosureReturn,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { useMutation } from "@tanstack/react-query";
import { FormControl, Radio, RadioGroup, Stack } from "@chakra-ui/react";
import { DialogDescription } from "@radix-ui/react-dialog";
import { createColumnHelper } from "@tanstack/react-table";
import { TWTable } from "components/shared/TWTable";
import { useTrack } from "hooks/analytics/useTrack";
import {
type Dispatch,
type ReactNode,
type SetStateAction,
useState,
} from "react";
CircleAlertIcon,
SendIcon,
Trash2Icon,
TriangleAlertIcon,
} from "lucide-react";
import Link from "next/link";
import { type ReactNode, useState } from "react";
import { useForm } from "react-hook-form";
import { BiPencil } from "react-icons/bi";
import { FiArrowRight, FiTrash } from "react-icons/fi";
import { useActiveAccount } from "thirdweb/react";
import { Badge, Button, FormLabel, Heading, Text } from "tw-components";
import { FiTrash } from "react-icons/fi";
import { toast } from "sonner";
import { FormLabel } from "tw-components";
import {
Alert,
AlertDescription,
AlertTitle,
} from "../../@/components/ui/alert";
import { Button } from "../../@/components/ui/button";
import { Input } from "../../@/components/ui/input";
import { Textarea } from "../../@/components/ui/textarea";
interface EngineInstancesTableProps {
instances: EngineInstance[];
isLoading: boolean;
isFetched: boolean;
refetch: ReturnType<typeof useEngineInstances>["refetch"];
setConnectedInstance: Dispatch<SetStateAction<EngineInstance | undefined>>;
}
export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
@@ -60,86 +56,15 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
isLoading,
isFetched,
refetch,
setConnectedInstance,
}) => {
const editDisclosure = useDisclosure();
const removeDisclosure = useDisclosure();
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false);
const trackEvent = useTrack();
const token = useLoggedInUser().user?.jwt ?? null;
const address = useActiveAccount()?.address;
const toast = useToast();
const [instanceToUpdate, setInstanceToUpdate] = useState<
EngineInstance | undefined
>();
const { mutate: connectToInstance } = useMutation({
mutationFn: async (instance: EngineInstance) => {
// Make an authed request.
// If it fails to fetch, the server is unreachable.
// If it returns a 401, the user is not a valid admin.
const res = await fetch(`${instance.url}auth/permissions/get-all`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (res.ok) {
return instance;
}
if (res.status === 401) {
throw new Error("Unauthorized");
}
throw new Error(`Unexpected status code ${res.status}`);
},
onSuccess: (instance) => {
trackEvent({
category: "engine",
action: "connect",
label: "success",
});
setConnectedInstance(instance);
},
onError: (error, instance) => {
const e = error as Error;
if (e?.message === "Failed to fetch") {
trackEvent({
category: "engine",
action: "connect",
label: "unreachable",
});
toast({
status: "error",
description: `Unable to connect to ${instance.url}. Ensure that your Engine is publicly accessible.`,
duration: null,
isClosable: true,
});
} else if (e?.message.indexOf("Unauthorized") > -1) {
trackEvent({
category: "engine",
action: "connect",
label: "unauthorized",
});
toast({
status: "error",
title: "Unauthorized",
description: `You are not an admin for this Engine instance. Contact the owner to add your wallet as an admin: ${address}`,
duration: null,
isClosable: true,
});
} else {
toast({
status: "error",
description:
"There was an unexpected error reaching your Engine instance. Try again or contact us if this issue persists.",
duration: null,
isClosable: true,
});
}
},
});
const columnHelper = createColumnHelper<EngineInstance>();
const columns = [
columnHelper.accessor("id", {
@@ -150,83 +75,62 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
let badge: ReactNode | undefined;
if (status === "requested") {
badge = (
<Tooltip label="Deployment will begin shortly.">
<Badge
borderRadius="full"
size="label.sm"
variant="subtle"
px={3}
py={1.5}
colorScheme="yellow"
>
Pending
</Badge>
</Tooltip>
<ToolTipLabel label="Deployment will begin shortly.">
<div>
<Badge variant="outline" className="gap-1.5">
<Spinner className="size-3" />
Pending
</Badge>
</div>
</ToolTipLabel>
);
} else if (status === "deploying") {
badge = (
<Tooltip label="This step may take up to 30 minutes.">
<Badge
borderRadius="full"
size="label.sm"
variant="subtle"
px={3}
py={1.5}
colorScheme="green"
>
Deploying
</Badge>
</Tooltip>
<ToolTipLabel label="This step may take up to 30 minutes.">
<div>
<Badge variant="default" className="gap-1.5">
<Spinner className="size-3" />
Deploying
</Badge>
</div>
</ToolTipLabel>
);
} else if (status === "paymentFailed") {
badge = (
<Tooltip label="There was an error charging your payment method. Please contact [email protected].">
<Badge
borderRadius="full"
size="label.sm"
variant="subtle"
px={3}
py={1.5}
colorScheme="red"
>
Payment Failed
</Badge>
</Tooltip>
<ToolTipLabel
label={
"There was an error charging your payment method. Please contact [email protected]."
}
>
<div>
<Badge variant="destructive" className="gap-1.5">
<CircleAlertIcon className="size-3" />
Payment Failed
</Badge>
</div>
</ToolTipLabel>
);
}
return (
<Stack py={2}>
<div className="py-3">
{badge ? (
<HStack spacing={4}>
<Text fontWeight="600" size="body.lg">
{name}
</Text>
<div className="flex items-center gap-4 py-2">
<p className="font-semibold text-lg"> {name} </p>
{badge}
</HStack>
</div>
) : (
<>
<Button
onClick={() => {
const instance = instances.find((i) => i.id === id);
if (instance) {
connectToInstance(instance);
}
}}
variant="link"
colorScheme="blue"
w="fit-content"
rightIcon={<FiArrowRight />}
justifyContent="flex-start"
<div className="flex flex-col gap-0.5">
<Link
href={`/dashboard/engine/${id}`}
className="text-foreground flex text-lg font-semibold items-center before:absolute before:inset-0 before:bg-transparent"
>
{name}
</Button>
<Text size="body.sm" color="gray.700">
{url}
</Text>
</>
</Link>
<p className="text-muted-foreground text-sm">{url}</p>
</div>
)}
</Stack>
</div>
);
},
}),
@@ -251,7 +155,7 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
label: "open-modal",
});
setInstanceToUpdate(instance);
editDisclosure.onOpen();
setIsEditModalOpen(true);
},
},
{
@@ -264,24 +168,28 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
label: "open-modal",
});
setInstanceToUpdate(instance);
removeDisclosure.onOpen();
setIsRemoveModalOpen(true);
},
isDestructive: true,
},
]}
bodyRowClassName="hover:bg-secondary relative"
/>
{editDisclosure.isOpen && instanceToUpdate && (
{instanceToUpdate && (
<EditModal
instance={instanceToUpdate}
disclosure={editDisclosure}
open={isEditModalOpen}
onOpenChange={setIsEditModalOpen}
refetch={refetch}
/>
)}
{removeDisclosure.isOpen && instanceToUpdate && (
{instanceToUpdate && (
<RemoveModal
instance={instanceToUpdate}
disclosure={removeDisclosure}
onOpenChange={setIsRemoveModalOpen}
open={isRemoveModalOpen}
refetch={refetch}
/>
)}
@@ -289,18 +197,14 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
);
};
const EditModal = ({
instance,
disclosure,
refetch,
}: {
disclosure: UseDisclosureReturn;
const EditModal = (props: {
open: boolean;
onOpenChange: (open: boolean) => void;
instance: EngineInstance;
refetch: () => void;
}) => {
const toast = useToast();
const { mutate: editInstance } = useEngineEditInstance();
const { onClose } = disclosure;
const editInstance = useEngineEditInstance();
const { onOpenChange, instance, open, refetch } = props;
const form = useForm<EditEngineInstanceInput>({
defaultValues: {
@@ -308,242 +212,298 @@ const EditModal = ({
name: instance.name,
url: instance.url,
},
reValidateMode: "onChange",
});
return (
<Modal isOpen onClose={onClose} isCentered size="lg">
<ModalOverlay />
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
<DialogHeader className="mb-3">
<DialogTitle className="text-2xl font-semibold tracking-tight">
Edit Engine Instance
</DialogTitle>
</DialogHeader>
<ModalContent>
<form
onSubmit={form.handleSubmit((data) =>
editInstance(data, {
editInstance.mutate(data, {
onSuccess: () => {
toast({
status: "success",
description: "Successfully updated this Engine.",
});
toast.success("Engine updated successfully");
refetch();
onClose();
onOpenChange(false);
},
onError: () => {
toast({
status: "error",
description: "Error updating this Engine.",
});
toast.error("Failed to update Engine");
},
}),
)}
>
<ModalHeader>Edit Engine Instance</ModalHeader>
<div className="flex flex-col gap-6">
<FormControl isRequired>
<FormLabel>Name</FormLabel>
<Input
type="text"
placeholder="Enter a descriptive label"
autoFocus
{...form.register("name", {
required: true,
})}
/>
</FormControl>
<FormControl isRequired>
<FormLabel>URL</FormLabel>
<Input
type="url"
placeholder="Enter your Engine URL"
{...form.register("url", {
required: true,
})}
/>
</FormControl>
</div>
<ModalBody>
<Stack spacing={4}>
<FormControl>
<FormLabel>Name</FormLabel>
<Input
type="text"
placeholder="Enter a descriptive label"
autoFocus
{...form.register("name")}
/>
</FormControl>
<FormControl isRequired>
<FormLabel>URL</FormLabel>
<Input
type="url"
placeholder="Enter your Engine URL"
{...form.register("url")}
/>
</FormControl>
</Stack>
</ModalBody>
<ModalFooter as={Flex} gap={3}>
<Button onClick={onClose} variant="ghost">
<DialogFooter className="mt-10 gap-2">
<Button onClick={() => onOpenChange(false)} variant="outline">
Cancel
</Button>
<Button type="submit" colorScheme="blue">
<Button
type="submit"
variant="primary"
className="gap-2"
disabled={!form.formState.isDirty}
>
{editInstance.isLoading ? (
<Spinner className="size-4" />
) : (
<SendIcon className="size-4" />
)}
Update
</Button>
</ModalFooter>
</DialogFooter>
</form>
</ModalContent>
</Modal>
</DialogContent>
</Dialog>
);
};
const RemoveModal = ({
instance,
disclosure,
refetch,
}: {
const RemoveModal = (props: {
instance: EngineInstance;
disclosure: UseDisclosureReturn;
open: boolean;
onOpenChange: (open: boolean) => void;
refetch: () => void;
}) => {
const toast = useToast();
const { mutate: removeFromDashboard } = useEngineRemoveFromDashboard();
const { mutate: removeCloudHosted } = useEngineRemoveCloudHosted();
const { onClose } = disclosure;
const { instance, open, onOpenChange, refetch } = props;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
{instance.status === "paymentFailed" ||
(instance.status === "active" && !instance.cloudDeployedAt) ? (
<RemoveFromDashboardModalContent
refetch={refetch}
instance={instance}
close={() => onOpenChange(false)}
/>
) : (
<CancelSubscriptionModalContent
refetch={refetch}
instance={instance}
close={() => onOpenChange(false)}
/>
)}
</DialogContent>
</Dialog>
);
};
function RemoveFromDashboardModalContent(props: {
refetch: () => void;
instance: EngineInstance;
close: () => void;
}) {
const { refetch, instance, close } = props;
const removeFromDashboard = useEngineRemoveFromDashboard();
return (
<>
<DialogHeader>
<DialogTitle className="text-2xl font-semibold tracking-tight mb-3">
Remove Engine Instance
</DialogTitle>
<DialogDescription className="text-muted-foreground">
<span className="block mb-2">
Are you sure you want to remove{" "}
<em className="not-italic font-semibold">"{instance.name}"</em> from
your dashboard?
</span>
<span className="block">
This action does not modify your Engine infrastructure. You can
re-add it at any time.
</span>
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-8 gap-1">
<Button onClick={close} variant="outline">
Close
</Button>
<Button
onClick={() => {
removeFromDashboard.mutate(instance.id, {
onSuccess: () => {
toast.success("Removed an Engine instance from your dashboard");
refetch();
close();
},
onError: () => {
toast.error(
"Error removing an Engine instance from your dashboard",
);
},
});
}}
variant="destructive"
className="gap-2"
>
{removeFromDashboard.isLoading ? (
<Spinner className="size-4" />
) : (
<Trash2Icon className="size-4" />
)}
Remove
</Button>
</DialogFooter>
</>
);
}
function CancelSubscriptionModalContent(props: {
refetch: () => void;
instance: EngineInstance;
close: () => void;
}) {
const { refetch, instance, close } = props;
const removeCloudHosted = useEngineRemoveCloudHosted();
const form = useForm<RemoveCloudHostedInput>({
defaultValues: {
instanceId: instance.id,
},
reValidateMode: "onChange",
});
return (
<Modal isOpen onClose={onClose} isCentered size="lg">
<ModalOverlay />
<ModalContent>
{instance.status === "paymentFailed" ||
(instance.status === "active" && !instance.cloudDeployedAt) ? (
<>
<ModalHeader>Remove Engine Instance</ModalHeader>
<div>
<DialogHeader>
<DialogTitle className="text-2xl font-semibold tracking-tight mb-1">
Cancel Engine Subscription
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Complete this form to request to cancel your Engine subscription. This
may take up to 2 business days.
</DialogDescription>
</DialogHeader>
<ModalBody as={Flex} flexDir="column" gap={2}>
<Text>
Are you sure you want to remove{" "}
<strong>{instance?.name}</strong> from your dashboard?
</Text>
<Text>
This action does not modify your Engine infrastructure. You can
re-add it at any time.
</Text>
</ModalBody>
<div className="h-3" />
<ModalFooter as={Flex} gap={3}>
<Button onClick={onClose} variant="ghost">
Close
</Button>
<Button
onClick={() => {
removeFromDashboard(instance.id, {
onSuccess: () => {
toast({
status: "success",
description:
"Removed an Engine instance from your dashboard.",
});
refetch();
onClose();
},
onError: () => {
toast({
status: "error",
description:
"Error removing an Engine instance from your dashboard.",
});
},
});
}}
colorScheme="red"
>
Remove
</Button>
</ModalFooter>
</>
) : (
<form
onSubmit={form.handleSubmit((data) =>
removeCloudHosted(data, {
onSuccess: () => {
toast({
status: "success",
description:
"Submitted a request to cancel your Engine subscription. This may take up to 2 business days.",
});
refetch();
onClose();
<Alert variant="destructive">
<TriangleAlertIcon className="size-5 !text-destructive-text" />
<AlertTitle>This action is irreversible!</AlertTitle>
<AlertDescription>
You will no longer be able to access this Engine's local backend
wallets. <strong>Any remaining mainnet funds will be lost.</strong>
</AlertDescription>
</Alert>
<div className="h-5" />
<form
onSubmit={form.handleSubmit((data) =>
removeCloudHosted.mutate(data, {
onSuccess: () => {
toast.success(
"Submitted a request to cancel your Engine subscription. This may take up to 2 business days.",
{
dismissible: true,
duration: 10000,
},
onError: () => {
toast({
status: "error",
description:
"Error requesting to cancel your Engine subscription.",
});
},
}),
)}
>
<ModalHeader>Cancel Engine Subscription</ModalHeader>
);
<ModalBody as={Stack} gap={4}>
<Text>
Complete this form to request to cancel your Engine
subscription. This may take up to 2 business days.
</Text>
{/* Form */}
<FormControl>
<FormLabel>
Please share any feedback to help us improve
</FormLabel>
<RadioGroup>
<Stack>
<Radio
value="USING_SELF_HOSTED"
{...form.register("reason", { required: true })}
>
<Text>Migrating to self-hosted</Text>
</Radio>
<Radio
value="TOO_EXPENSIVE"
{...form.register("reason", { required: true })}
>
<Text>Too expensive</Text>
</Radio>
<Radio
value="MISSING_FEATURES"
{...form.register("reason", { required: true })}
>
<Text>Missing features</Text>
</Radio>
<Radio
value="OTHER"
{...form.register("reason", { required: true })}
>
<Text>Other</Text>
</Radio>
</Stack>
</RadioGroup>
<Textarea
placeholder="Provide additional feedback"
mt={2}
{...form.register("feedback")}
/>
</FormControl>
<Alert status="warning" variant="left-accent">
<Flex direction="column" gap={2}>
<Heading as={AlertTitle} size="label.md">
This action is irreversible!
</Heading>
<Text as={AlertDescription} size="body.md">
You will no longer be able to access this Engine&apos;s
local backend wallets.{" "}
<strong>Any remaining mainnet funds will be lost.</strong>
</Text>
</Flex>
</Alert>
</ModalBody>
<ModalFooter as={Flex} gap={3}>
<Button onClick={onClose} variant="ghost">
Close
</Button>
<Button
type="submit"
colorScheme="primary"
isDisabled={!form.formState.isValid}
>
Request to cancel
</Button>
</ModalFooter>
</form>
refetch();
close();
},
onError: () => {
toast.error(
"Error requesting to cancel your Engine subscription",
);
},
}),
)}
</ModalContent>
</Modal>
>
{/* Form */}
<FormControl isRequired>
<FormLabel className="!text-base">
Please share any feedback to help us improve
</FormLabel>
<RadioGroup>
<Stack>
<Radio
value="USING_SELF_HOSTED"
{...form.register("reason", { required: true })}
>
<span className="text-sm"> Migrating to self-hosted </span>
</Radio>
<Radio
value="TOO_EXPENSIVE"
{...form.register("reason", { required: true })}
>
<span className="text-sm"> Too expensive </span>
</Radio>
<Radio
value="MISSING_FEATURES"
{...form.register("reason", { required: true })}
>
<span className="text-sm"> Missing features </span>
</Radio>
<Radio
value="OTHER"
{...form.register("reason", { required: true })}
>
<span className="text-sm"> Other </span>
</Radio>
</Stack>
</RadioGroup>
</FormControl>
<div className="h-2" />
<Textarea
className="mt-3"
placeholder="Provide additional feedback"
{...form.register("feedback")}
/>
<div className="h-8" />
<DialogFooter className="gap-2">
<Button onClick={close} variant="outline">
Close
</Button>
<Button
type="submit"
variant="destructive"
disabled={!form.formState.isValid}
className="gap-2"
>
{removeCloudHosted.isLoading ? (
<Spinner className="size-4" />
) : (
<SendIcon className="size-4" />
)}
Request to cancel
</Button>
</DialogFooter>
</form>
</div>
);
};
}
@@ -1,164 +1,188 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import {
type EngineInstance,
useEngineInstances,
} from "@3rdweb-sdk/react/hooks/useEngine";
import {
Center,
Divider,
Flex,
Icon,
ListItem,
Stack,
UnorderedList,
} from "@chakra-ui/react";
import { ChakraNextImage } from "components/Image";
import { useEngineInstances } from "@3rdweb-sdk/react/hooks/useEngine";
import { useTrack } from "hooks/analytics/useTrack";
import type { Dispatch, SetStateAction } from "react";
import { VscBook } from "react-icons/vsc";
import invariant from "tiny-invariant";
import { Button, Card, Heading, Link, Text } from "tw-components";
import { CreateEngineInstanceButton } from "./create-engine-instance";
import {
CloudDownloadIcon,
ExternalLinkIcon,
PlusIcon,
RocketIcon,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { UnorderedList } from "../../@/components/ui/List/List";
import { Button } from "../../@/components/ui/button";
import { EngineInstancesTable } from "./engine-instances-table";
import { ImportEngineInstanceButton } from "./import-engine-instance";
interface EngineInstancesListProps {
setConnectedInstance: Dispatch<SetStateAction<EngineInstance | undefined>>;
}
export const EngineInstancesList = ({
setConnectedInstance,
}: EngineInstancesListProps) => {
invariant(
process.env.NEXT_PUBLIC_DEMO_ENGINE_URL,
"missing NEXT_PUBLIC_DEMO_ENGINE_URL",
);
const NEXT_PUBLIC_DEMO_ENGINE_URL = process.env.NEXT_PUBLIC_DEMO_ENGINE_URL;
const { data } = useAccount();
export const EngineInstancesList = () => {
const instancesQuery = useEngineInstances();
const instances = instancesQuery.data ?? [];
const trackEvent = useTrack();
if (instancesQuery.isLoading) {
return (
<div className="grid place-items-center">
<div className="flex justify-center items-center h-[300px]">
<Spinner className="size-14" />
</div>
);
}
return (
<Stack spacing={8}>
{instances.length === 0 ? (
<Center>
<Stack spacing={8}>
<ChakraNextImage
alt="Engine hero image"
src={require("../../../public/assets/engine/empty-state-header.png")}
maxW={650}
/>
<Heading size="title.lg" as="h1">
Your scalable web3 backend server
</Heading>
<UnorderedList color="gray.600">
<ListItem>
Read, write, and deploy contracts at production scale
</ListItem>
<ListItem>
Reliably parallelize and retry transactions with gas &amp; nonce
management
</ListItem>
<ListItem>Securely manage backend wallets</ListItem>
<ListItem>
Built-in support for account abstraction, relayers, and more
</ListItem>
</UnorderedList>
if (instances.length === 0) {
return (
<div className="max-w-[600px] mx-auto">
<Image
alt="Engine hero image"
src={require("../../../public/assets/engine/empty-state-header.png")}
/>
<Flex gap={4}>
<CreateEngineInstanceButton
ctaText="Get Started"
refetch={instancesQuery.refetch}
/>
<Button
onClick={() => {
if (!data) return;
<div className="h-6" />
trackEvent({
category: "engine",
action: "try-demo",
label: "clicked-try-demo",
});
<h1 className="text-2xl md:text-3xl font-bold tracking-tighter">
Your scalable web3 backend server
</h1>
// Need to hardcode the demo engine instance details
// but using the signed-in user account id
setConnectedInstance({
id: "sandbox",
url: NEXT_PUBLIC_DEMO_ENGINE_URL,
name: "Demo Engine",
status: "active",
lastAccessedAt: new Date().toISOString(),
accountId: data.id,
});
}}
variant="outline"
px={6}
>
🚀 Try Demo
</Button>
</Flex>
<Flex direction="column" gap={2}>
<Divider my="2" />
<Text>Already have an Engine Instance?</Text>
<ImportEngineInstanceButton refetch={instancesQuery.refetch} />
</Flex>
</Stack>
</Center>
) : (
<>
<div className="flex flex-col md:flex-row gap-4 justify-between md:items-center">
<h1 className="text-4xl font-bold">Engine</h1>
<div className="flex flex-row gap-2">
<ImportEngineInstanceButton refetch={instancesQuery.refetch} />
<CreateEngineInstanceButton
ctaText="Create Engine Instance"
refetch={instancesQuery.refetch}
/>
</div>
</div>
<div className="h-3" />
<Stack spacing={4}>
<EngineInstancesTable
instances={instancesQuery.data ?? ([] as EngineInstance[])}
isFetched={instancesQuery.isFetched}
isLoading={instancesQuery.isLoading}
refetch={instancesQuery.refetch}
setConnectedInstance={setConnectedInstance}
/>
</Stack>
</>
)}
<UnorderedList>
<li>Read, write, and deploy contracts at production scale</li>
<li>
Reliably parallelize and retry transactions with gas & nonce
management
</li>
<li>Securely manage backend wallets</li>
<li>Built-in support for account abstraction, relayers, and more</li>
</UnorderedList>
<Divider />
<div className="h-6" />
<Card p={8}>
<Stack>
<Flex gap={2} align="center">
<Icon as={VscBook} />
<Heading size="title.xs">Learn more about Engine</Heading>
</Flex>
<Text>Dive into features and integration guides.</Text>
<Link
href="https://portal.thirdweb.com/engine"
isExternal
color="blue.500"
fontSize="small"
<div className="flex gap-3">
<CreateEngineLink label="Get Started" />
<Button
asChild
onClick={() => {
trackEvent({
category: "engine",
action: "try-demo",
label: "clicked-try-demo",
});
}}
variant="outline"
>
View Docs &rarr;
</Link>
</Stack>
</Card>
</Stack>
<Link href={"/dashboard/engine/sandbox"} className="gap-2">
Try Demo
<RocketIcon className="size-4 text-muted-foreground" />
</Link>
</Button>
</div>
<div className="h-10" />
<div className="flex items-center gap-4 justify-between rounded-lg border border-border p-6 bg-secondary">
<p className="text-lg font-semibold tracking-tight">
Already have an Engine Instance?
</p>
<ImportEngineLink label="Import" />
</div>
<div className="h-20" />
<LearnMoreCard />
</div>
);
}
return (
<div>
<div className="flex flex-col md:flex-row gap-4 justify-between md:items-center">
<h1 className="text-4xl font-bold">Engine</h1>
<div className="flex flex-row gap-2">
<ImportEngineLink label="Import" />
<CreateEngineLink label="Create Engine Instance" />
</div>
</div>
<div className="h-6" />
<EngineInstancesTable
instances={instances}
isFetched={instancesQuery.isFetched}
isLoading={instancesQuery.isLoading}
refetch={instancesQuery.refetch}
/>
<div className="h-10" />
<LearnMoreCard />
</div>
);
};
function LearnMoreCard() {
return (
<div className="p-4 border border-border rounded-lg relative hover:bg-secondary">
<h3 className="font-semibold tracking-tight mb-1">
<Link
href="https://portal.thirdweb.com/engine"
target="_blank"
className="before:content before:absolute before:inset-0"
>
Learn more about Engine
</Link>
</h3>
<p className="text-muted-foreground text-sm">
Dive into features and integration guides.
</p>
<ExternalLinkIcon className="size-4 text-muted-foreground absolute right-4 top-4" />
</div>
);
}
function CreateEngineLink(props: {
label: string;
}) {
const trackEvent = useTrack();
return (
<Button
asChild
variant="primary"
onClick={() => {
trackEvent({
category: "engine",
action: "click",
label: "add-engine-instance",
});
}}
>
<Link href={"/dashboard/engine/create"} className="gap-2">
<PlusIcon className="size-4" />
{props.label}
</Link>
</Button>
);
}
function ImportEngineLink(props: {
label: string;
}) {
const trackEvent = useTrack();
return (
<Button
asChild
variant="outline"
onClick={() => {
trackEvent({
category: "engine",
action: "import",
});
}}
>
<Link href={"/dashboard/engine/import"} className="gap-2">
<CloudDownloadIcon className="size-4" />
{props.label}
</Link>
</Button>
);
}
@@ -1,164 +0,0 @@
import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
import { Flex, Stack } from "@chakra-ui/react";
import { SidebarNav } from "core-ui/sidebar/nav";
import type { Route } from "core-ui/sidebar/types";
import { useTrack } from "hooks/analytics/useTrack";
import {
type Dispatch,
type SetStateAction,
useCallback,
useMemo,
useState,
} from "react";
import { FiArrowLeft } from "react-icons/fi";
import { Button, Heading, Text } from "tw-components";
import { EngineVersionBadge } from "./badges/version";
import { EngineConfiguration } from "./configuration/engine-configuration";
import { EngineWebhooks } from "./configuration/engine-webhooks";
import { EngineContractSubscriptions } from "./contract-subscription/engine-contract-subscription";
import { EngineExplorer } from "./explorer/engine-explorer";
import { EngineOverview } from "./overview/engine-overview";
import { EngineAccessTokens } from "./permissions/engine-access-tokens";
import { EngineAdmins } from "./permissions/engine-admins";
import { EngineRelayer } from "./relayer/engine-relayer";
import { EngineSystemMetrics } from "./system-metrics";
interface EngineNavigationProps {
instance: EngineInstance;
setConnectedInstance: Dispatch<SetStateAction<EngineInstance | undefined>>;
}
export const EngineNavigation: React.FC<EngineNavigationProps> = ({
instance,
setConnectedInstance,
}) => {
const trackEvent = useTrack();
const onClickBack = () => {
setConnectedInstance(undefined);
};
const [activePage, setActivePage] = useState<string>("overview");
const handleClick = useCallback(
(id: string) => {
trackEvent({
category: "engine",
action: "navigate-tab",
label: id,
url: instance.url,
});
setActivePage(id);
},
[instance.url, trackEvent],
);
const links = useMemo(
() =>
[
{
path: "/dashboard/engine/overview",
title: "Overview",
name: "overview",
onClick: () => handleClick("overview"),
},
{
path: "/dashboard/engine/explorer",
title: "Explorer",
name: "explorer",
onClick: () => handleClick("explorer"),
},
{
path: "/dashboard/engine/relayers",
title: "Relayers",
name: "relayers",
onClick: () => handleClick("relayers"),
},
{
path: "/dashboard/engine/contract-subscriptions",
title: "Contract Subscriptions",
name: "contract-subscriptions",
onClick: () => handleClick("contract-subscriptions"),
},
{
path: "/dashboard/engine/admins",
title: "Admins",
name: "admins",
onClick: () => handleClick("admins"),
},
{
path: "/dashboard/engine/access-tokens",
title: "Access Tokens",
name: "access-tokens",
onClick: () => handleClick("access-tokens"),
},
{
path: "/dashboard/engine/webhooks",
title: "Webhooks",
name: "webhooks",
onClick: () => handleClick("webhooks"),
},
{
path: "/dashboard/engine/configuration",
title: "Configuration",
name: "configuration",
onClick: () => handleClick("configuration"),
},
{
path: "/dashboard/engine/metrics",
title: "Metrics",
name: "metrics",
onClick: () => handleClick("metrics"),
},
] satisfies Route[],
[handleClick],
);
const activeComponent = useMemo(() => {
const tabs = {
overview: <EngineOverview instanceUrl={instance.url} />,
explorer: <EngineExplorer instanceUrl={instance.url} />,
relayers: <EngineRelayer instanceUrl={instance.url} />,
"contract-subscriptions": (
<EngineContractSubscriptions instanceUrl={instance.url} />
),
admins: <EngineAdmins instanceUrl={instance.url} />,
"access-tokens": <EngineAccessTokens instanceUrl={instance.url} />,
webhooks: <EngineWebhooks instanceUrl={instance.url} />,
configuration: <EngineConfiguration instance={instance} />,
metrics: <EngineSystemMetrics instance={instance} />,
};
return tabs[activePage as keyof typeof tabs];
}, [instance, activePage]);
return (
<>
<SidebarNav links={links} activePage={activePage} title="Engine" />
<Stack spacing={4}>
<Button
onClick={onClickBack}
variant="link"
leftIcon={<FiArrowLeft />}
w="fit-content"
>
Back
</Button>
<Stack>
<Heading size="title.lg" as="h1" isTruncated>
{instance.name}
</Heading>
<Flex gap={3} alignItems="center">
{!instance.name.startsWith("https://") && (
<Text color="gray.600">{instance.url}</Text>
)}
<EngineVersionBadge instance={instance} />
</Flex>
</Stack>
{activeComponent}
</Stack>
</>
);
};
@@ -1,186 +0,0 @@
import {
Alert,
AlertDescription,
AlertIcon,
Flex,
FormControl,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
type UseDisclosureReturn,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { THIRDWEB_API_HOST } from "constants/urls";
import { useTrack } from "hooks/analytics/useTrack";
import { useSingleQueryParam } from "hooks/useQueryParam";
import { useForm } from "react-hook-form";
import { Button, FormLabel, Text, TrackedLink } from "tw-components";
interface ImportEngineInstanceButtonProps {
refetch: () => void;
}
export const ImportEngineInstanceButton = ({
refetch,
}: ImportEngineInstanceButtonProps) => {
const hasImportUrl = !!useSingleQueryParam("importUrl");
const disclosure = useDisclosure({
defaultIsOpen: !!hasImportUrl,
});
const trackEvent = useTrack();
return (
<>
<Button
onClick={() => {
trackEvent({
category: "engine",
action: "import",
label: "open-modal",
});
disclosure.onOpen();
}}
variant="outline"
px={6}
width="fit-content"
>
Import
</Button>
{disclosure.isOpen && (
<ModalImportEngine
disclosure={disclosure}
onSuccess={() => refetch()}
/>
)}
</>
);
};
interface ImportEngineInput {
name: string;
url: string;
}
const ModalImportEngine = ({
disclosure,
onSuccess,
}: {
disclosure: UseDisclosureReturn;
onSuccess: () => void;
}) => {
const toast = useToast();
const defaultUrl = useSingleQueryParam("importUrl");
const form = useForm<ImportEngineInput>({
defaultValues: {
name: "My Engine Instance",
url: defaultUrl ? decodeURIComponent(defaultUrl) : undefined,
},
});
const onSubmit = async (data: ImportEngineInput) => {
try {
// Instance URLs should end with a /.
const url = data.url.endsWith("/") ? data.url : `${data.url}/`;
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: data.name,
url,
}),
});
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}`);
}
disclosure.onClose();
onSuccess();
} catch (e) {
toast({
status: "error",
description:
"Error importing Engine. Please check if the details are correct.",
});
}
};
return (
<Modal
isOpen={disclosure.isOpen}
onClose={disclosure.onClose}
isCentered
size="lg"
>
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<form onSubmit={form.handleSubmit(onSubmit)}>
<ModalHeader>Import Engine Instance</ModalHeader>
<ModalBody>
<Stack spacing={4}>
<Text>
Import an Engine instance hosted on your infrastructure.
<br />
<TrackedLink
href="https://portal.thirdweb.com/infrastructure/engine/get-started"
isExternal
category="engine"
label="clicked-self-host-instructions"
color="blue.500"
>
Get help setting up Engine for free.
</TrackedLink>
</Text>
<FormControl>
<FormLabel>Name</FormLabel>
<Input
type="text"
placeholder="Enter a descriptive label"
autoFocus
{...form.register("name")}
/>
</FormControl>
<FormControl isRequired>
<FormLabel>URL</FormLabel>
<Input
type="url"
placeholder="Enter your Engine URL"
{...form.register("url")}
/>
</FormControl>
<Alert status="warning" borderRadius="m">
<AlertIcon />
<AlertDescription>
Do not import a URL you do not recognize.
</AlertDescription>
</Alert>
</Stack>
</ModalBody>
<ModalFooter as={Flex} gap={3}>
<Button onClick={disclosure.onClose} variant="ghost">
Cancel
</Button>
<Button type="submit" colorScheme="primary">
Import
</Button>
</ModalFooter>
</form>
</ModalContent>
</Modal>
);
};
@@ -18,6 +18,7 @@ import { formatDistanceToNowStrict } from "date-fns/formatDistanceToNowStrict";
import { useAllChainsData } from "hooks/chains/allChains";
import { useState } from "react";
import { FiArrowLeft, FiArrowRight, FiInfo } from "react-icons/fi";
import { toTokens } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import {
Badge,
@@ -311,6 +312,8 @@ const TransactionDetailsDrawer = ({
const chain: ChainMetadata | undefined =
chainIdToChainRecord[Number.parseInt(transaction.chainId)];
const decimals = chain.nativeCurrency.decimals || 18;
const symbol = chain.nativeCurrency.symbol || "ETH";
const explorer = chain?.explorers?.[0];
const status = statusDetails[transaction.status as EngineStatus];
@@ -320,12 +323,15 @@ const TransactionDetailsDrawer = ({
: transaction.functionName ?? null;
let txFeeDisplay = "N/A";
if (transaction.gasLimit && transaction.gasPrice) {
const txFee =
(Number.parseFloat(transaction.gasLimit) *
Number.parseFloat(transaction.gasPrice)) /
10 ** (chain?.nativeCurrency.decimals || 18);
txFeeDisplay = `${txFee} ${chain?.nativeCurrency.symbol || "ETH"}`;
if (transaction.gasLimit) {
const gasPrice =
transaction.effectiveGasPrice ||
transaction.gasPrice ||
transaction.maxFeePerGas;
if (gasPrice) {
const txFeeWei = BigInt(transaction.gasLimit) * BigInt(gasPrice);
txFeeDisplay = `${toTokens(txFeeWei, decimals)} ${symbol}`;
}
}
return (
@@ -397,7 +403,9 @@ const TransactionDetailsDrawer = ({
)}
<FormControl>
<FormLabel>From Address</FormLabel>
<FormLabel>
{transaction.accountAddress ? "Signer Address" : "From Address"}
</FormLabel>
<LinkButton
variant="ghost"
isExternal
@@ -412,8 +420,31 @@ const TransactionDetailsDrawer = ({
</LinkButton>
</FormControl>
{transaction.accountAddress && (
<FormControl>
<FormLabel>Account Address</FormLabel>
<LinkButton
variant="ghost"
isExternal
size="xs"
href={
explorer
? `${explorer.url}/address/${transaction.accountAddress}`
: "#"
}
>
<Text fontFamily="mono">{transaction.accountAddress}</Text>
</LinkButton>
</FormControl>
)}
<FormControl>
<FormLabel>To Address</FormLabel>
{/* The "to" address is usually a contract except for native token transfers. */}
<FormLabel>
{functionCalled === "transfer"
? "Recipient Address"
: "Contract Address"}
</FormLabel>
<LinkButton
variant="ghost"
isExternal
@@ -456,6 +487,24 @@ const TransactionDetailsDrawer = ({
{/* On-chain details */}
<FormControl>
<Flex>
<FormLabel>Value</FormLabel>
<Tooltip
label={`The amount of ${symbol} sent to the "To" .`}
shouldWrapChildren
>
<FiInfo />
</Tooltip>
</Flex>
<Text>
{transaction.value
? toTokens(BigInt(transaction.value), decimals)
: 0}{" "}
{symbol}
</Text>
</FormControl>
{transaction.transactionHash && (
<>
<FormControl>
@@ -528,6 +577,25 @@ const TransactionDetailsDrawer = ({
<Text>{Number(transaction.gasPrice).toLocaleString()}</Text>
</FormControl>
)}
{transaction.blockNumber && (
<FormControl>
<FormLabel>Block</FormLabel>
<LinkButton
variant="ghost"
isExternal
size="xs"
href={
explorer
? `${explorer.url}/block/${transaction.blockNumber}`
: "#"
}
maxW="100%"
>
<Text>{transaction.blockNumber}</Text>
</LinkButton>
</FormControl>
)}
</Stack>
</Collapse>
@@ -21,13 +21,12 @@ import {
useDisclosure,
} from "@chakra-ui/react";
import { NetworkDropdown } from "components/contract-components/contract-publish-form/NetworkDropdown";
import { isAddress } from "ethers/lib/utils";
import { useTrack } from "hooks/analytics/useTrack";
import { useAllChainsData } from "hooks/chains/allChains";
import { useTxNotifications } from "hooks/useTxNotifications";
import { useForm } from "react-hook-form";
import { AiOutlinePlusCircle } from "react-icons/ai";
import { shortenAddress } from "thirdweb/utils";
import { isAddress, shortenAddress } from "thirdweb/utils";
import { Button, FormHelperText, FormLabel } from "tw-components";
interface AddRelayerButtonProps {
@@ -36,7 +36,7 @@ export const EngineSystemMetrics: React.FC<EngineStatusProps> = ({
<Text>
Upgrade to a{" "}
<TrackedLink
href="/dashboard/engine?requestCloudHosted"
href="/dashboard/engine/create"
isExternal
color="blue.500"
category="engine"
@@ -0,0 +1,41 @@
import { useQuery } from "@tanstack/react-query";
import { useLoggedInUser } from "../../@3rdweb-sdk/react/hooks/useLoggedInUser";
// If it fails to fetch, the server is unreachable.
// If it returns a 401, the user is not a valid admin.
export function useHasEnginePermission(props: {
instanceUrl: string;
}) {
const instanceUrl = props.instanceUrl;
const token = useLoggedInUser().user?.jwt ?? null;
return useQuery({
queryKey: ["auth/permissions/get-all", instanceUrl, token],
queryFn: async () => {
const res = await fetch(`${instanceUrl}auth/permissions/get-all`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (res.ok) {
return {
hasPermission: true,
};
}
if (res.status === 401) {
return {
hasPermission: false,
reason: "Unauthorized",
};
}
throw new Error("Unexpected status code");
},
retry: false,
});
}
@@ -1,9 +1,9 @@
import type { GasEstimate } from "@3rdweb-sdk/react/hooks/useGas";
import { Box, type BoxProps, Flex, Icon, Tooltip } from "@chakra-ui/react";
import { AiOutlineInfoCircle } from "@react-icons/all-files/ai/AiOutlineInfoCircle";
import { utils } from "ethers";
import { useTrack } from "hooks/analytics/useTrack";
import { FiExternalLink } from "react-icons/fi";
import { toTokens } from "thirdweb/utils";
import { Heading, Link, Text } from "tw-components";
import { contractSlugMapping } from "./contractSlugMapping";
import { functionNameMapping } from "./functionNameMapping";
@@ -31,18 +31,15 @@ export const GasEstimatorBox: React.FC<GasEstimatorBoxProps> = ({
const formatPrice = (price: number | undefined) => {
if (price && ethOrUsd === "eth") {
return `~${Number(
utils.formatUnits(
`${((gasEstimate?.gasPrice as number) || 30) * price}`,
"gwei",
),
toTokens(BigInt(((gasEstimate?.gasPrice as number) || 30) * price), 9),
).toFixed(4)} ETH`;
}
if (price && ethOrUsd === "usd") {
return `~$${(
Number(
utils.formatUnits(
`${((gasEstimate?.gasPrice as number) || 30) * price}`,
"gwei",
toTokens(
BigInt(((gasEstimate?.gasPrice as number) || 30) * price),
9,
),
) * (gasEstimate?.ethPrice || 3400)
).toFixed(2)}`;
@@ -91,7 +91,7 @@ export const PublishWithVersionPage: React.FC<PublishWithVersionPageProps> = ({
<SelectContent>
{availableVersions.map((v, idx) => {
return (
<SelectItem value={v} key={v}>
<SelectItem value={v || "latest"} key={v}>
{v}
{idx === 0 && (
<span className="text-muted-foreground"> (latest) </span>
@@ -103,7 +103,7 @@ export const CustomChainRenderer = ({
{!disableChainConfig && storedChain && (
<Button
variant="ghost"
className="ml-auto p-2 leading-4 opacity-0 group-hover:opacity-100 hover:bg-transparent transition-opacity"
className="ml-auto p-2 leading-4 md:opacity-0 group-hover:opacity-100 hover:bg-transparent transition-opacity"
aria-label="Configure Network"
onClick={() => {
setEditChain(storedChain);
@@ -1,4 +1,4 @@
import { isAddress } from "ethers/lib/utils";
import { isAddress } from "thirdweb/utils";
import { RE_BUNDLE_ID, RE_DOMAIN } from "utils/regex";
import { validStrList } from "utils/validations";
import { z } from "zod";
@@ -11,7 +11,8 @@ const nameValidation = z
const domainsValidation = z.string().refine(
(str) =>
validStrList(str, (domain) => {
return domain.split(":")[0] === "localhost" || RE_DOMAIN.test(domain);
// This isn't the strictest validation, but users keep running into cases where the validation is TOO strict
return domain.includes(":") || RE_DOMAIN.test(domain);
}),
{
message: "Some of the domains are invalid",
@@ -6,9 +6,9 @@ import { useDisclosure, useToast } from "@chakra-ui/react";
import { createColumnHelper } from "@tanstack/react-table";
import { TWTable } from "components/shared/TWTable";
import { format } from "date-fns/format";
import { isAddress } from "ethers/lib/utils";
import { useTrack } from "hooks/analytics/useTrack";
import { useState } from "react";
import { isAddress } from "thirdweb/utils";
import { Button, Text } from "tw-components";
import type { ComponentWithChildren } from "types/component-with-children";
import { shortenString } from "utils/usedapp-external";
@@ -1,5 +1,5 @@
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
import { Flex, Input, Select, type SelectProps } from "@chakra-ui/react";
import { useSDKChainId } from "@thirdweb-dev/react";
import { CURRENCIES, type CurrencyMetadata } from "constants/currencies";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { useMemo, useState } from "react";
@@ -25,7 +25,7 @@ export const CurrencySelector: React.FC<CurrencySelectorProps> = ({
defaultCurrencies = [],
...props
}) => {
const chainId = useSDKChainId();
const chainId = useDashboardEVMChainId();
const configuredChainsRecord = useSupportedChainsRecord();
const chain = chainId ? configuredChainsRecord[chainId] : undefined;
@@ -1,13 +1,17 @@
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import {
ButtonGroup,
Center,
Divider,
Flex,
Icon,
IconButton,
Menu,
MenuButton,
MenuList,
Spinner,
Table,
Tbody,
@@ -29,7 +33,7 @@ import { type SetStateAction, useMemo, useState } from "react";
import { FaEllipsisVertical } from "react-icons/fa6";
import { FiArrowRight } from "react-icons/fi";
import type { IconType } from "react-icons/lib";
import { Button, MenuItem, TableContainer, Text } from "tw-components";
import { TableContainer, Text } from "tw-components";
type CtaMenuItem<TRowData> = {
icon?: IconType;
@@ -54,6 +58,7 @@ type TWTableProps<TRowData> = {
showLess?: boolean;
};
title: string;
bodyRowClassName?: string;
};
export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
@@ -148,13 +153,13 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
</Th>
))}
{(tableProps.onRowClick || tableProps.onMenuClick) && (
<Th border="none" />
<Th border="none" className="w-0" />
)}
</Tr>
))}
</Thead>
<Tbody>
<Tbody className="!bg-background">
{table.getRowModel().rows.map((row) => {
return (
<Tr
@@ -176,6 +181,7 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
},
}
: {})}
className={tableProps.bodyRowClassName}
>
{row.getVisibleCells().map((cell) => {
return (
@@ -207,30 +213,37 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
borderBottomWidth="inherit"
borderBottomColor="accent.100"
>
<Menu>
<MenuButton
as={IconButton}
variant="outline"
icon={<Icon as={FaEllipsisVertical} boxSize={4} />}
aria-label="Actions"
/>
<MenuList>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
aria-label="Actions"
className="relative z-10 p-2.5 !h-auto"
>
<FaEllipsisVertical className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{tableProps.onMenuClick.map(
({ icon, text, onClick, isDestructive }) => {
return (
<MenuItem
<DropdownMenuItem
key={text}
onClick={() => onClick(row.original)}
icon={icon && <Icon as={icon} boxSize={4} />}
color={isDestructive ? "red.500" : undefined}
className={cn(
"gap-3 px-3 py-3 min-w-[170px] cursor-pointer",
isDestructive &&
"!text-destructive-text hover:!bg-destructive",
)}
>
{icon && <Icon as={icon} boxSize={4} />}
{text}
</MenuItem>
</DropdownMenuItem>
);
},
)}
</MenuList>
</Menu>
</DropdownMenuContent>
</DropdownMenu>
</Td>
) : null}
</Tr>
@@ -22,12 +22,12 @@ import {
import { zodResolver } from "@hookform/resolvers/zod";
import { NetworkDropdown } from "components/contract-components/contract-publish-form/NetworkDropdown";
import { GatedSwitch } from "components/settings/Account/Billing/GatedSwitch";
import { isAddress } from "ethers/lib/utils";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { useMemo } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { LuTrash2 } from "react-icons/lu";
import { isAddress } from "thirdweb/utils";
import {
Button,
FormErrorMessage,
@@ -1,3 +1,4 @@
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "@/constants/env";
import {
ThirdwebProvider,
type WalletConfig,
@@ -5,7 +6,6 @@ import {
es,
ja,
} from "@thirdweb-dev/react";
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "constants/rpc";
import { THIRDWEB_API_HOST, THIRDWEB_DOMAIN } from "constants/urls";
import { StorageSingleton } from "lib/sdk";
@@ -1,9 +1,9 @@
import { isProd } from "@/constants/env";
import {
type WalletConfig,
embeddedWallet,
smartWallet,
} from "@thirdweb-dev/react";
import { isProd } from "constants/rpc";
import { useState } from "react";
import { type WalletId, walletInfoRecord } from "./walletInfoRecord";
+10
View File
@@ -1,3 +1,7 @@
import { thirdwebClient } from "@/constants/client";
import { getContract } from "thirdweb";
import { polygon } from "thirdweb/chains";
/**
* Putting this here solely for the purpose of getting rid of sdk v4
*/
@@ -16,3 +20,9 @@ export type ContractType =
| "token-drop"
| "token"
| "vote";
export const MULTICHAIN_REGISTRY_CONTRACT = getContract({
chain: polygon,
client: thirdwebClient,
address: "0xcdAD8FA86e18538aC207872E8ff3536501431B73",
});
+17
View File
@@ -0,0 +1,17 @@
/**
* publicly available wallet for local nodes
* extracted from SDK v4, used to request localhost testnet funds
*/
export const LOCAL_NODE_PKEY =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
/**
* Used for gasless transactions on Dashboard:
* 1. Add contracts to Multichain Registry
* 2. Remove contracts from Multichain Registry
*/
export const DASHBOARD_ENGINE_RELAYER_URL =
"https://checkout.engine.thirdweb.com/relayer/0c2bdd3a-307f-4243-b6e5-5ba495222d2b";
export const DASHBOARD_FORWARDER_ADDRESS =
"0x409d530a6961297ece29121dbee2c917c3398659";
-11
View File
@@ -1,11 +0,0 @@
export const DASHBOARD_THIRDWEB_CLIENT_ID =
process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "";
export const DASHBOARD_THIRDWEB_SECRET_KEY =
process.env.DASHBOARD_SECRET_KEY || "";
export const isProd =
(process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV) ===
"production";
export const PROD_OR_DEV_URL = isProd ? "thirdweb.com" : "thirdweb-dev.com";
+16
View File
@@ -49,3 +49,19 @@ const AddressSchema = z.custom<string>(
export const AddressOrEnsSchema = z.union([AddressSchema, EnsSchema], {
invalid_type_error: "Provided value was not a valid address or ENS name",
});
const FileSchema = z.instanceof(File) as z.ZodType<InstanceType<typeof File>>;
const FileOrStringSchema = z.union([FileSchema, z.string()]);
export const CommonContractSchema = z
.object({
name: z.string(),
description: z.string().optional(),
image: FileOrStringSchema.optional(),
external_link: z.string().optional(),
app_uri: z.string().optional(),
social_urls: z.record(z.string()).optional(),
defaultAdmin: AddressOrEnsSchema.optional(),
})
.catchall(z.unknown());
@@ -1,4 +1,4 @@
import { isProd } from "constants/rpc";
import { isProd } from "@/constants/env";
import { useAllChainsData } from "hooks/chains/allChains";
import { createContext, useCallback, useEffect, useState } from "react";
import type { ChainMetadata } from "thirdweb/chains";
@@ -1,10 +1,10 @@
import { useQueryClient } from "@tanstack/react-query";
import { ThirdwebSDKProvider, useSigner } from "@thirdweb-dev/react";
import type { SDKOptions } from "@thirdweb-dev/sdk";
import {
DASHBOARD_THIRDWEB_CLIENT_ID,
DASHBOARD_THIRDWEB_SECRET_KEY,
} from "constants/rpc";
} from "@/constants/env";
import { useQueryClient } from "@tanstack/react-query";
import { ThirdwebSDKProvider, useSigner } from "@thirdweb-dev/react";
import type { SDKOptions } from "@thirdweb-dev/sdk";
import {
useSupportedChain,
useSupportedChains,

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