Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
4284b9d77b Version Packages (#8008)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-09 00:39:51 -07:00
b149019318 [SDK] Feature: added glyph wallet in extra wallets (#7973)
Co-authored-by: Joaquim Verges <[email protected]>
2025-09-08 23:46:49 -07:00
Joaquim VergesandGitHub e2ebb602dc thirdweb AI inside dashboard (#8003) 2025-09-08 17:10:07 -07:00
MananTank 2d76cf9040 [BLD-259] Playground, Portal: Add revalidation for API Reference page (#8022)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the metadata and content for the API reference pages in the `portal` and `playground-web` applications, enhancing the descriptions and titles to better reflect the purpose of the APIs. It also introduces a revalidation period for both pages.

### Detailed summary
- Updated `metadata` in `apps/portal/src/app/reference/page.tsx`:
  - Changed `title` from "API Reference" to "thirdweb API Reference".
  - Changed `description` from "Complete REST API reference for all thirdweb services" to "A Unified interface for Web3 development".
- Added `revalidate` property with value `3600` in `apps/portal/src/app/reference/page.tsx`.
- In `apps/playground-web/src/app/reference/page.tsx`:
  - Imported `ScalarApiReference` and `createMetadata`.
  - Added `metadata` with `title` "thirdwebAPI Reference" and `description` "A Unified interface for Web3 development".
  - Added `revalidate` property with value `3600`.

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

<!-- end pr-codex -->
2025-09-08 23:03:32 +00:00
MananTank e7d0899e76 [MNY-163] Portal: Fix broken links in payments sidebar (#8021)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `sidebar` configuration in two files: `sidebar.tsx` for the bridge and payments sections. It removes an entry and modifies the `href` properties for navigation links to ensure correct routing.

### Detailed summary
- Removed the `Playground` entry from the `sidebar` in `apps/portal/src/app/bridge/sidebar.tsx`.
- Updated the `href` for the `Playground` entry in `apps/portal/src/app/payments/sidebar.tsx` to point to `/payments`.
- Changed `href` values for `Sell Tokens`, `Swap Tokens`, `Get Token Prices`, and `Get Routes` to use absolute paths.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Corrected Payments sidebar destinations: Playground now points to the dedicated payments playground, and Bridge-related items use absolute paths for reliable, consistent navigation across the app.

* **Chores**
  * Removed the obsolete Playground link from the Bridge sidebar to streamline navigation.
  * Cleaned up an unused icon import.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-08 23:03:32 +00:00
MananTank 88001cfe49 [BLD-271] Portal: Change Header nav link order (#8013)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on modifying the `links` array in the `Header.tsx` file by removing and then re-adding the `Payments` and `Bridge` items, thereby changing their order within the array.

### Detailed summary
- Removed `Payments` link from the `links` array.
- Removed `Bridge` link from the `links` array.
- Re-added `Payments` link to the `links` array.
- Re-added `Bridge` link to the `links` array.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Style**
  * Updated top navigation order: Payments and Bridge now appear after Contracts instead of immediately after Wallets.
  * Applies to both desktop navigation and the mobile menu, improving consistency across views without changing the available links or destinations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-08 22:28:57 +00:00
MananTank a9c388a613 Dashboard: Add Bridge in framer paths (#8006)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `apps/dashboard/framer-rewrites.js` file by adding a new route for monetization.

### Detailed summary
- Added the route `"/monetize/bridge"` to the existing array of routes in `apps/dashboard/framer-rewrites.js`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* New Features
  * Introduced a new public route: /monetize/bridge. Users can now directly access the Monetize Bridge experience via this URL, enabling easier navigation and deep-linking from emails, bookmarks, or external references. Existing routes remain unchanged, so current links and workflows continue to work as before. This addition improves discoverability and provides a clearer, direct path to the relevant monetization flow.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-08 19:59:33 +00:00
nischitpra 4ff1cf7979 fix polygon amoy gasstation url (#8007)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on a patch for the `thirdweb` library that addresses a fix for the Polygon amoy gas station URL.

### Detailed summary
- Introduced a patch for the `thirdweb` library.
- Fixed the URL for the Polygon amoy gas station.

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

<!-- end pr-codex -->

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

* **Bug Fixes**
  * Corrected the Polygon Amoy gas station endpoint, improving fee estimation and transaction reliability on that network.

* **Chores**
  * Prepared a patch release entry reflecting this fix.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-08 18:41:17 +00:00
nischitpra e78a984b4e polygon amoy gasstation update (#8004)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the gas fee data source for the Polygon network in the `fee-data.ts` file, specifically changing the URL for the testnet case.

### Detailed summary
- In the `fee-data.ts` file, the return URL for the case `80002` has been changed from `"https://gasstation-testnet.polygon.technology/v2"` to `"https://gasstation.polygon.technology/amoy"`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Updated the gas fee data source for Polygon Amoy (chain 80002) to the current gas station endpoint, improving accuracy and reliability of priority fee estimates.
  * Enhances transaction success rates and fee predictions for users interacting with the Amoy network.
  * No changes for Polygon mainnet (137).

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-08 12:45:14 +00:00
YashandGitHub e37bd8e994 feat: move insight page to APIs dropdown (#7991) 2025-09-07 16:12:39 -07:00
Joaquim VergesandGitHub a94cf80dbd [Dashboard] Update analytics API integration and add error logging (#8001) 2025-09-07 04:36:12 -07:00
gregandGitHub f789e7663a [Portal] Feature: Bridge page (#7998) 2025-09-06 12:54:42 -07:00
a51f4a0031 Add chat context controls to playground (#7999)
Co-authored-by: Cursor Agent <[email protected]>
2025-09-05 18:15:57 -07:00
Joaquim VergesandGitHub d06a15ecbb [Docs] Add OpenAI SDK documentation page (#7997) 2025-09-05 17:12:33 -07:00
Joaquim VergesandGitHub 61fddefcb1 [SDK] Add documentation for create7702MinimalAccount function (#7996) 2025-09-05 17:11:55 -07:00
FirekeeperandGitHub 14fe9ee2b9 Remove ability to disable Transactions (#7995) 2025-09-06 04:17:40 +07:00
MananTank 4ca407dbf0 [MNY-139] Dashboard, Portal, Playground: Add Bridge Product (#7984)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the payment and bridge functionalities in the dashboard and playground applications. It includes UI improvements, new components, and refactoring for better organization and usability.

### Detailed summary
- Deleted `RouteDiscoveryCard.tsx`.
- Replaced `BridgeIcon` export.
- Added route rewrites in `next.config.mjs`.
- Updated navigation links in `navLinks.ts`.
- Modified `useTokensData.ts` to include price parameters.
- Refactored `CreatePaymentWebhookButton` to a function.
- Enhanced layout and styling in various components, including `ProjectSidebarLayout`, `EmptyState`, and `QuickstartSection`.
- Introduced `ProjectSettingsBreadcrumb` for better navigation.
- Added support for locked widgets in payment components.
- Improved `PaymentLinksTable` and `PaymentHistory` components with new table structures.
- Enhanced `FeatureCard` and `PayAnalytics` with updated styling and structure.
- Created `CheckoutPlayground` and `TransactionPlayground` components for better payment handling.
- Reorganized the bridge section with improved UI and functionality.

> The following files were skipped due to too many changes: `apps/playground-web/src/app/payments/transactions/page.tsx`

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

<!-- end pr-codex -->

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

* **New Features**
  * Bridge page with analytics, route discovery, and Quickstart; CSV export for payment history; new Buy, Checkout, and Transaction playgrounds.

* **Navigation**
  * Added "Bridge" to project sidebar; Webhooks consolidated with tabs; Payments page streamlined; redirect from /payments/ui-components → /payments and removed UI Components page.

* **UI/UX Improvements**
  * Redesigned analytics and payment history table; updated FeatureCard and EmptyState visuals; removed "Swap Tokens" card; settings pages use breadcrumbs; assorted copy and spacing tweaks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-05 20:17:41 +00:00
0xFirekeeper f714e5d810 Update API reference links to use local /reference paths (#7993)
Replaced external 'https://api.thirdweb.com/reference' links with local '/reference' paths across documentation, sidebar, and metadata files for consistency and improved navigation within the portal.

Closes BLD-221

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the reference URLs from absolute paths to relative paths across various components in the application. This change aims to streamline navigation within the app and improve consistency in referencing API documentation.

### Detailed summary
- Changed `href` values from absolute URLs to relative paths in multiple files:
  - `Header.tsx`
  - `ai/sidebar.tsx`
  - `wallets/sidebar.tsx`
  - `contracts/sidebar.tsx`
  - `ai/llm-txt/page.mdx`
  - `payments/page.mdx`
  - `nebula/api-reference` files
  - `transactions/sponsor/page.mdx`
  - `page.tsx`
  - `Document/APIEndpointMeta/OpenApiEndpoint.tsx`
  - `wallets/server/page.mdx`
  - `transactions/page.mdx`

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

<!-- end pr-codex -->

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

- Documentation
  - Updated API Reference links across the site (AI, Contracts, Transactions, Payments, Wallets, Nebula endpoints, Home, and Header) to use internal paths (/reference and anchored routes).
- Bug Fixes
  - API Reference links now navigate within the docs site instead of opening an external domain, improving navigation continuity.
- Refactor
  - Standardized reference URL handling to consistently use relative routes for a unified in-app experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-05 19:49:28 +00:00
Yash094 a328250be1 feat: Add api to project view section (#7992)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `IntegrateAPIKeyCodeTabs` component by adding new tab keys for `api` and `curl`, along with their respective code examples for API integration. It also updates the default selected tab to `api`.

### Detailed summary
- Added new `TabKey` values: `api` and `curl`.
- Updated `tabNames` to include labels for `api` and `curl`.
- Changed default tab state from `ts` to `api`.
- Introduced `apiCodeExample` function for API integration code.
- Introduced `curlCodeExample` function for cURL integration code.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Added API and cURL tabs to the API integration UI with ready-to-copy code examples (JavaScript fetch and Bash curl) for server-side wallet API calls.
  * Set the API tab as the default selection and updated tab order to include API and cURL.
  * Examples auto-fill the masked secret key when available and include a sample identifier, improving first-time setup guidance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-05 15:49:11 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
928704ad5c Version Packages (#7990)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-05 00:43:09 -07:00
Joaquim VergesandGitHub acbbf2039f [Playground] Add Engine user transactions example to playground (#7470) 2025-09-05 00:37:54 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
4375e7331f Version Packages (#7989)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-05 13:35:47 +12:00
FirekeeperandGitHub 22d736cb24 Refactor wallet user API integration to use tw api (#7987) 2025-09-05 08:09:24 +07:00
0xFirekeeper bb89561a1b Fetch minimal account address from bundler (#7919)
```
<!--

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

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

## Notes for the reviewer

Replaces the hardcoded `MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS` with a dynamic fetch from the bundler using the `tw_getDelegationContract` RPC method.

This change:
- Introduces an async function `getDelegationContractAddress` to fetch the address.
- Implements caching for the fetched address to prevent redundant RPC calls.
- Updates `_sendTxWithAuthorization` and `is7702MinimalAccount` to handle the async nature and use the dynamically fetched address.
- Ensures type safety for address fields.

## How to test

Unit tests cover the functionality. A temporary test was created and verified during development.
```
pnpm test packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts
```

-->
```

---
[Slack Thread](https://thirdwebdev.slack.com/archives/C085FEPFLN9/p1756220164604699?thread_ts=1756220164.604699&cid=C085FEPFLN9)

<a href="https://cursor.com/background-agent?bcId=bc-874d9753-17ac-4429-8dfb-c3d6776f6711">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-cursor-dark.svg">
    <source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-cursor-light.svg">
    <img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg">
  </picture>
</a>
<a href="https://cursor.com/agents?id=bc-874d9753-17ac-4429-8dfb-c3d6776f6711">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-web-dark.svg">
    <source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-web-light.svg">
    <img alt="Open in Web" src="https://cursor.com/open-in-web.svg">
  </picture>
</a>

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `erc7702` extension, refining event filters, and modifying function signatures to enhance clarity and correctness. It also introduces delegation contract handling in the `minimal-account` implementation.

### Detailed summary
- Updated function signatures and event filters in multiple files to replace `signer` with `user` and `newSigner`.
- Improved data structures in the generated files for `MinimalAccount`.
- Added delegation contract address fetching in `minimal-account.ts`.
- Adjusted transaction handling logic to use the delegation contract address.
- Enhanced error handling for RPC responses.

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

<!-- end pr-codex -->

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

- New Features
  - Minimal account updated: token-receiver callbacks, interface support check, payable receive, new executeWithSig entrypoint, and a ValueReceived event; SessionCreated/Executed events now include user/newSigner.

- Bug Fixes
  - Delegation contract resolved dynamically via bundler with caching and improved error handling.
  - Address normalization for transactions and nonce lookups to reduce mismatch errors.
  - Removed a redundant error from the ABI.

- Tests
  - Tests updated to assert renamed event fields (user/newSigner).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 22:22:30 +00:00
Joaquim VergesandGitHub c92081b056 [AI SDK Provider] Update sign swap action type with token fields (#7988) 2025-09-04 15:22:00 -07:00
Olexandr88andGitHub dc57b850d3 chore: add links to Twitter and Telegram (#7983)
Signed-off-by: Olexandr88 <[email protected]>
2025-09-05 04:35:54 +07:00
Manan TankandGitHub 6429a238c1 [BLD-231] Playground: Add API reference, Remove Insight and Payments API playgrounds (#7986) 2025-09-05 04:35:43 +07:00
MananTank 769019dd0b [BLD-242] Dashboard: Fix Notification overflow, UI Improvements (#7985)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the UI of the notification components in the dashboard. It includes updates to layout, spacing, and styles, improving the overall user experience and visual consistency.

### Detailed summary
- Updated `PopoverContent` with new dimensions and layout.
- Enhanced `NotificationList` with improved tab spacing and structure.
- Adjusted styles for empty state messages in `InboxTab` and `ArchiveTab`.
- Modified `NotificationEntry` layout and added icons for better aesthetics.
- Changed button styles and interactions for improved usability.

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

<!-- end pr-codex -->

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

* **Style**
  * Refreshed Notifications panel: larger, rounded popover with improved spacing, overflow handling, and updated alignment.
  * Redesigned notification items: bell capsule indicator, clearer card-like layout, hover-revealed “Mark as read,” and an outline CTA with arrow icon.
  * Improved list layout: flexible, smoother scrolling, simplified tab styling, and refined spacing.
  * Enhanced empty states for Inbox and Archive with centered, card-style icon visuals.

* **Refactor**
  * Reorganized component layouts for consistency and responsiveness without changing behavior or public APIs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 18:09:34 +00:00
MananTank 488a3af154 [MNY-106] Playground: Add overview pages for all sections in sidebar (#7977)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the UI components for various sections in a web application, particularly by introducing `OverviewPage` components for different features and updating the layout for better organization and presentation.

### Detailed summary
- Updated `className` for `PromptInput` in `chat-container.tsx`.
- Added `OverviewPage` components for `Tokens`, `AI`, `Contracts`, `Payments`, `Wallets`, `Transactions`, and `Account Abstraction`.
- Introduced `FeatureCard` metadata for various features in `pages-metadata.ts`.
- Refactored the main `Page` component to use `FeatureSection` for organized display.
- Removed hardcoded feature cards from the main `Page` component.

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

<!-- end pr-codex -->

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

- New Features
  - Added overview pages for AI, Wallets, Headless Components, Transactions, Contracts, Payments, Tokens, and Account Abstraction.
  - Introduced reusable OverviewPage and FeatureCard components.
  - Centralized feature-card metadata for all categories.
  - Updated navigation to add “Overview” entries and renamed “AI SDK” to “AI SDK Integration”.
- Refactor
  - Home page rewritten to a data-driven layout using shared components and metadata with consistent grid rendering.
- Style
  - Tweaked AI chat input borders and top-edge rounding.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 17:50:50 +00:00
MananTank 8a29e36934 Portal: Remove unused pages, empty pages, Fix missing H1s (#7979)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on the deletion of several `.mdx` files related to the `pay` and `account` sections, while adding new content to the `nebula` API reference and making updates to the `engine` wallet configurations.

### Detailed summary
- Deleted multiple `.mdx` files in the `pay` and `account` sections.
- Added new API endpoints in `nebula`:
  - `Execute Action`
  - `Get Session`
  - `List Sessions`
  - `Clear Session`
  - `Create Session`
  - `Delete Session`
  - `Update Session`
- Updated headings for clarity in existing `.mdx` files related to wallet configurations.

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

<!-- end pr-codex -->
2025-09-04 17:44:16 +00:00
MananTank ea98087d41 Portal: UI tweaks/fixes (#7978)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refining the UI components and code structure in the `apps/portal` directory, improving styles, and enhancing the functionality of various elements such as tabs, lists, and layouts.

### Detailed summary
- Adjusted class names for styling consistency.
- Reduced icon sizes in several components.
- Removed unnecessary background styles from `InlineParameterItem`.
- Simplified `UnorderedList` and `OrderedList` components.
- Enhanced `FeatureCard` layout and styles.
- Updated tab triggers by removing redundant margin classes.
- Streamlined payment-related documentation and examples.
- Improved overall readability and structure in multiple markdown files.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- Style
  - Simplified tab headers by removing platform icons across multiple pages.
  - Updated layout and spacing: Insight grid tweaks, refined DocLayout text color, toned-down lists and parameter blocks, redesigned FeatureCard, and streamlined Table of Contents styling.

- Documentation
  - Replaced several tabbed sections with single-flow content; some pages now link directly to live demos.
  - Updated examples: Send now illustrates a multi-step execution flow; corrected/cleaned samples for Sell and other payments pages.
  - Transactions and Contracts pages use direct endpoint rendering in places for clarity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 17:44:16 +00:00
Yash094 030349bb92 remove looking for engine on project view page (#7981)
<!--

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

https://linear.app/thirdweb/issue/BLD-238/remove-looking-for-engine-hint-from-project-view

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refining the rendering logic in the `page.tsx` file for a specific team page, particularly around the display of a billing plan alert and the conditions under which it is shown.

### Detailed summary
- Replaced a conditional rendering of the free billing plan alert from a ternary operator to a logical AND (`&&`) operator.
- Removed the `DismissibleAlert` component that provided information about engines and projects.
- Kept the `Changelog` component intact.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Resolved an issue where teams on paid plans saw an unnecessary alert on the team page. Paid teams will no longer see this message.
  * Free plan teams continue to see the upgrade banner as before.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 15:29:59 +00:00
MananTank 17d40afda3 Dashboard: fix payments tx history table crashing on onramp type purchase (#7982)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the payment-related types and logic in the application to improve type safety and clarity. It introduces a new `BridgePayment` type, updates the `TableRow` component, and modifies how payments are filtered and processed.

### Detailed summary
- Changed `Payment` type to `BridgePayment` in `TableRow` component.
- Updated `originAmount` and `destinationAmount` to use `BigInt`.
- Introduced `BridgePaymentType` and `OnrampPaymentType`.
- Modified `getCSVData` to accept only `BridgePayment` types.
- Added `isBridgePayment` function to filter payments.

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

<!-- end pr-codex -->

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

- New Features
  - Payments views and CSV export now explicitly show only bridge payments; onramp entries are excluded.

- Bug Fixes
  - Improved accuracy for large origin/destination amounts by converting amounts to integer form before token conversion.
  - Consistent token conversions across table, links, and CSV exports.

- Refactor
  - Payment data reorganized into distinct bridge vs onramp variants to improve filtering and export consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 14:45:15 +00:00
YashandGitHub bc6309ddc7 small fix to set API section as default for explorer code snippets (#7980) 2025-09-04 16:45:08 +07:00
f47eda6842 add api snippets to explorer section (#7971)
Co-authored-by: 0xFirekeeper <[email protected]>
2025-09-04 05:47:50 +07:00
2328985699 Add API Reference section to portal app (#7968)
Co-authored-by: Manan Tank <[email protected]>
2025-09-04 04:39:35 +07:00
MananTank 18b7f9104f Dashboard: Fix Import Contract Modal submit button loading state (#7970)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the conditional rendering logic in the `modal.tsx` file to use the `form.formState.isSubmitting` state instead of `addContractToProject.isPending`. This change ensures that the button reflects the correct submission state during the import process.

### Detailed summary
- Changed the conditional check for displaying the loading spinner from `addContractToProject.isPending` to `form.formState.isSubmitting`.
- Updated the button text from "Importing" to reflect the new state condition.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Import Contract modal: The submit button now accurately reflects the submission state. It shows a spinner and the label changes to “Importing” while the form is submitting, and reverts to the standard icon with “Import” when idle. This improves user feedback, reduces confusion, and helps prevent duplicate submissions by clearly indicating when an import is in progress.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-03 20:15:25 +00:00
MananTank 82be4fa53a [MNY-142] Dashboard: Do not render BuyWidget if token is not supported by Bridge (#7975)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `ERC20PublicPage` component to utilize `tokenInfoFromUB` instead of `tokenInfo` for setting the contract image. It also modifies the logic to conditionally render the `BuyEmbed` component based on whether the token is supported.

### Detailed summary
- Replaced `tokenInfo` with `tokenInfoFromUB` for contract image assignment.
- Removed the `buyEmbed` variable; integrated its logic directly into the render method.
- Added a new `showBuyEmbed` variable to determine if `BuyEmbed` should be displayed.
- Updated the rendering of `BuyEmbed` to use `claimConditionMeta`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- New Features
  - Buy section now appears only when supported by the token or when a claim condition exists.
  - Unified buy experience: automatically shows either a direct buy flow or a claim flow based on available conditions.
- Bug Fixes
  - More reliable token icon fallback to reduce missing/blank images.
- Style
  - Preserved decorative layout while conditionally rendering the buy UI for a cleaner, more consistent appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-03 20:04:08 +00:00
MananTank dbb2614306 [BLD-228] SDK/React: Do not exclude specified wallets from all wallets list in Connect UI (#7974)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on removing the `specifiedWallets` prop from various components related to wallet selection in the `thirdweb` package, streamlining the wallet filtering logic.

### Detailed summary
- Removed `specifiedWallets` prop from the `WalletAuth` component.
- Updated `ConnectModalContent` to remove `specifiedWallets` and utilize `recommendedWallets`.
- Modified `AllWalletsUI` to eliminate `specifiedWallets` and simplify wallet filtering logic.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- Refactor
  - Simplified the wallet selection list to a standardized set across the Connect modal and in-app auth.
  - Removed custom list filtering, ensuring a consistent experience regardless of entry point.
  - The list now excludes in-app, embedded, and smart wallets from the “All Wallets” view.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-03 17:43:59 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
b8f496603c Version Packages (#7967)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-02 16:46:46 -07:00
Joey BrightandGitHub 4c782ad64d docs: Fix Typos in docs (#7963) 2025-09-02 16:46:17 -07:00
gregandGitHub af6a306955 [SDK] Feature: country Code on Payment Widgets (#7966) 2025-09-02 16:44:56 -07:00
MananTank 4b319c353d [BLD-227] Dashboard: Add Button to download snapshot as CSV file in Claim Phase UI (#7965)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refining the `ClaimConditionsForm` and enhancing the `ClaimerSelection` component by adjusting imports, improving layout, and adding functionality to download snapshot data as a CSV file.

### Detailed summary
- Removed `ArrowDownToLineIcon` from `ClaimConditionsForm`.
- Updated layout in `ClaimerSelection` to use a flexible column layout.
- Added a button to download snapshots as CSV in `ClaimerSelection`.
- Introduced `downloadSnapshotAsCSV` function to handle CSV generation and downloading.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- New Features
  - Added CSV export for Claimer snapshots with a Download button visible when a snapshot exists.

- Style
  - Updated snapshot actions to a responsive layout (stack on small screens, inline on larger screens).
  - Added a distinct Download button with an icon and adjusted Edit button styling.
  - Removed the icon from the Save Phases button, showing text only.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-02 20:57:23 +00:00
saminacodes aadaf209b5 deprecated and redirected docs (#7960)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on removing several files related to the `infrastructure` section of the application, including markdown files, images, and TypeScript components, while also updating the `redirects.mjs` file to include new external portal redirects.

### Detailed summary
- Deleted the following files:
  - `apps/portal/src/app/infrastructure/page.mdx`
  - `apps/portal/src/app/infrastructure/layout.tsx`
  - `apps/portal/src/app/infrastructure/sidebar.tsx`
  - `apps/portal/src/app/infrastructure/assets/test.jpg`
  - `apps/portal/src/app/infrastructure/storage/overview/page.mdx`
  - `apps/portal/src/app/infrastructure/rpc-edge/overview/page.mdx`
  - `apps/portal/src/app/infrastructure/rpc-edge/get-started/page.mdx`
  - `apps/portal/src/app/infrastructure/storage/how-storage-works/page.mdx`
  - `apps/portal/src/app/infrastructure/rpc-edge/get-started/assets/api-key.png`
  - `apps/portal/src/app/infrastructure/rpc-edge/overview/assets/rpc-diagram.svg`
  - `apps/portal/src/app/infrastructure/rpc-edge/get-started/assets/public-rpcs.png`
  - `apps/portal/src/app/infrastructure/rpc-edge/get-started/assets/usage-dashboard.png`
  - `apps/portal/src/app/infrastructure/storage/how-to-use-storage/host-web-app/page.mdx`
  - `apps/portal/src/app/infrastructure/storage/how-storage-works/assets/storage-diagram.svg`
  - `apps/portal/src/app/infrastructure/storage/how-to-use-storage/upload-files-to-ipfs/page.mdx`
  - `apps/portal/src/app/infrastructure/storage/how-to-use-storage/upload-files-to-ipfs/assets/storage-urls.png`
  - `apps/portal/src/app/infrastructure/storage/how-to-use-storage/upload-files-to-ipfs/assets/storage-upload.png`
  - `apps/portal/src/app/infrastructure/storage/how-to-use-storage/upload-files-to-ipfs/assets/storage-dashboard.png`
- Updated `apps/portal/redirects.mjs` to:
  - Redirect `"/storage/upload-to-ipfs"` to `"/infrastructure/storage/how-to-use-storage/upload-files-to-ipfs"`
  - Add infrastructure redirects to external portal links for `"/infrastructure/storage/:path*"` and `"/infrastructure/rpc-edge/:path*"`

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- Documentation
  - Removed the Infrastructure section from the portal, including Overview, Storage (Overview, How Storage Works, Upload Files to IPFS, Host Web App), and RPC Edge (Overview, Get Started) pages.
  - Updated navigation accordingly to reflect the removal of these pages.

- New Features
  - Added automatic redirects for Infrastructure content:
    - /infrastructure/storage/:path* now points to the external Storage documentation.
    - /infrastructure/rpc-edge/:path* now points to the external RPC documentation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-02 16:56:21 +00:00
Joaquim VergesandGitHub df6f8e2646 [Playground] Refactor AI types and move definitions to types.ts (#7964) 2025-09-02 02:05:40 -07:00
5b871ac281 [Portal] docs: fix broken link (#7962)
Signed-off-by: VolodymyrBg <[email protected]>
Signed-off-by: Joaquim Verges <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
2025-09-02 20:54:54 +12:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
7eed8e61db Version Packages (#7957)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-01 00:06:04 -07:00
Joaquim VergesandGitHub 6ef2f3b37a [AI SDK Provider] Auto handle session ids (#7956) 2025-08-31 22:10:49 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
596aaa5495 Version Packages (#7949)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-31 16:18:40 -07:00
MananTank 89eeac1f8a Dashboard: Fix footer not sticked to bottom on short contracts page (#7951)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the layout of the `contract-page-layout.tsx` component by adding a CSS class to the main `div` element for improved styling and structure.

### Detailed summary
- Changed the main `div` element from `<div>` to `<div className="flex flex-col grow">` to apply flexbox layout and allow the element to grow.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Style**
  * Updated contract page layout to use a flexible, column-based structure for more consistent spacing.
  * Improves use of available screen space so content expands naturally within the dashboard.
  * Enhances responsiveness across viewports, reducing awkward white space and unnecessary scrolling.
  * Provides more stable alignment between sidebar and main content on contract detail pages.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-30 04:03:07 +00:00
MananTank c331d189bb Dashboard: Remove OP Sponsored badge in Chain selector (#7948)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR removes the handling of sponsored chains from the `CustomChainRenderer` component, including the related constant and UI elements that indicate whether a chain is sponsored.

### Detailed summary
- Deleted the import of `OPSponsoredChains` from `@/constants/chains`.
- Removed the `isSponsored` variable that checks if a chain is sponsored.
- Eliminated the UI block that displayed "Sponsored" for sponsored chains.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- Refactor
  - Removed the Sponsored badge from the chain list, simplifying the chain renderer.
  - Deprecated chains still display a Deprecated badge and remain non-interactive.
  - Clicking non-deprecated chains continues to switch networks; the Configure Network button is unchanged.

- Chores
  - Removed obsolete sponsor-related configuration and references.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-29 18:13:06 +00:00
MananTank f867799301 [MNY-129] Dashboard: Use useSendAndConfirmTx everywhere, Replace createToken with prepareCreateToken (#7947)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on replacing the usage of `useSendAndConfirmTransaction` with `useSendAndConfirmTx` across various components in the dashboard application, streamlining transaction handling.

### Detailed summary
- Replaced `useSendAndConfirmTransaction` with `useSendAndConfirmTx` in multiple components.
- Updated imports to reflect the new hook in relevant files.
- Modified transaction handling logic to utilize the new hook consistently across the codebase.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Mintable.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/claim-tokens/claim-tokens-ui.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Royalty.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/nft/create-nft-page.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx`

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

<!-- end pr-codex -->
2025-08-29 16:25:21 +00:00
MananTank 597431ec1d [MNY-131] Add Pay Modal integration in useSendAndConfirmTransaction hook (#7946)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a `Pay Modal` integration in the `useSendAndConfirmTransaction` hook, allowing users to handle insufficient funds by prompting them to purchase more. It also renames the hook and updates related components to utilize the new configuration.

### Detailed summary
- Renamed `useSendAndConfirmTransaction` to `useSendAndConfirmTransactionCore`.
- Updated exports to reflect the renaming.
- Added `payModal` configuration to `useSendAndConfirmTransaction`.
- Updated `CreateDirectListingButton`, `ClaimButton`, and `BuyDirectListingButton` to utilize the new `payModal` configuration.
- Enhanced documentation for `useSendAndConfirmTransaction` with usage examples for the `Pay Modal`.

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

<!-- end pr-codex -->

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

* **New Features**
  * Pay Modal integrated into send-and-confirm transaction flows, matching send-only behavior.
  * Prebuilt buttons (Buy Direct Listing, Claim, Create Direct Listing) pass Pay Modal config and auto-fill metadata with sensible fallbacks for smoother top-ups.
  * Pay Modal options are configurable and compatible with gasless workflows.

* **Documentation**
  * Added examples and guidance for configuring the Pay Modal (theme, disabling, and usage).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-29 16:25:20 +00:00
GiselleNessi d5ff9f2f5d feat: add support siwa feedback system for closed support tickets (#7916)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a feedback system for support tickets, allowing users to submit ratings and comments. It includes API calls for submitting feedback and checking its status, along with UI updates to handle user interactions and display feedback status.

### Detailed summary
- Added `submitSupportFeedback` function to submit feedback.
- Added `checkFeedbackStatus` function to verify if feedback has been submitted.
- Integrated feedback submission UI in `SupportCaseDetails` component.
- Implemented rating selection using stars.
- Added input field for optional feedback comments.
- Displayed loading and error states for feedback status checks.

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

<!-- end pr-codex -->

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

* **New Features**
  * 5‑star rating UI and optional comment for closed support cases, with Send Feedback button, “Checking feedback status…” and sending spinners, disabled send until a rating is chosen, and a thank‑you message after submission.

* **Improvements**
  * Server-backed feedback status check and submission with input validation, 1–5 rating, 1000‑char comment limit, 10s request timeout, and contextual success/error toasts.
  * Accessible star controls with ARIA labels and filled‑star visuals.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-29 15:27:15 +00:00
saminacodes 76694f0cec Update Contracts FAQ (#7930)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds new information to the `page.mdx` file regarding deployment costs and auditing of thirdweb contracts.

### Detailed summary
- Added a new `Details` section on deployment costs for thirdweb contracts, outlining:
  - Mint fees and primary sales fees for specific contract types.
  - A 2% convenience fee and a 0.5% protocol fee on primary sales.
  - Clarified that fees apply only to prebuilt contracts via the dashboard.
- Included information about thirdweb contracts being audited by third-party firms.

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

<!-- end pr-codex -->

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

* **Documentation**
  * Added an FAQ explaining costs for deploying prebuilt contracts via the dashboard.
  * Clarifies a 2% dashboard deployment fee plus a 0.5% protocol fee (total 2.5%) taken from primary sales.
  * Includes an example payout and lists affected contract categories.
  * Notes that custom or externally deployed contracts are exempt.
  * Specifies fees are deducted in the sale’s currency and placed after the existing FAQ on using thirdweb with existing contracts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-29 01:16:31 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
65a8c39ce0 Version Packages (#7945)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-28 18:15:35 -07:00
Joaquim VergesandGitHub ac3a95ebbb [AI SDK Provider] Fix type inference for tools (#7944) 2025-08-28 18:10:21 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
00ffe3e46e Version Packages (#7943)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-28 15:46:28 -07:00
gregandGitHub 50c6371e1e [SDK] Feature: add ability to exclude token prices from Bridge.tokens (#7942) 2025-08-28 15:22:12 -07:00
450 changed files with 21417 additions and 13380 deletions
+2
View File
@@ -172,6 +172,8 @@ See our [open source page](https://thirdweb.com/open-source) for more informatio
- [Documentation](https://portal.thirdweb.com/)
- [Templates](https://thirdweb.com/templates)
- [YouTube](https://www.youtube.com/c/thirdweb)
- [X/Twitter](https://x.com/thirdweb)
- [Telegram](https://t.me/officialthirdweb)
## Support
+24
View File
@@ -95,6 +95,30 @@ module.exports = {
'Import "posthog-js" directly only within the analytics helpers ("src/@/analytics/*"). Use the exported helpers from "@/analytics/track" elsewhere.',
name: "posthog-js",
},
{
importNames: ["useSendTransaction"],
message:
'Use `import { useSendAndConfirmTx } from "@/hooks/useSendTx";` instead',
name: "thirdweb/react",
},
{
importNames: ["useSendAndConfirmTransaction"],
message:
'Use `import { useSendAndConfirmTx } from "@/hooks/useSendTx";` instead',
name: "thirdweb/react",
},
{
importNames: ["sendTransaction"],
message:
'Use `import { useSendAndConfirmTx } from "@/hooks/useSendTx";` instead if used in react component',
name: "thirdweb",
},
{
importNames: ["sendAndConfirmTransaction"],
message:
'Use `import { useSendAndConfirmTx } from "@/hooks/useSendTx";` instead if used in react component',
name: "thirdweb",
},
],
patterns: [
{
+1
View File
@@ -64,4 +64,5 @@ module.exports = [
"/enterprise",
"/token",
"/vault",
"/monetize/bridge",
];
+1
View File
@@ -8,6 +8,7 @@
],
"ignoreBinaries": ["only-allow"],
"ignoreDependencies": [
"@thirdweb-dev/api",
"@thirdweb-dev/service-utils",
"@thirdweb-dev/vault-sdk",
"thirdweb",
+2
View File
@@ -23,6 +23,7 @@
"@shazow/whatsabi": "0.22.2",
"@tanstack/react-query": "5.81.5",
"@tanstack/react-table": "^8.21.3",
"@thirdweb-dev/api": "workspace:*",
"@thirdweb-dev/service-utils": "workspace:*",
"@thirdweb-dev/vault-sdk": "workspace:*",
"@vercel/functions": "2.2.2",
@@ -34,6 +35,7 @@
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"fast-xml-parser": "^5.2.5",
"fetch-event-stream": "0.1.5",
"fuse.js": "7.1.0",
"input-otp": "^1.4.1",
"ioredis": "^5.6.1",
+1
View File
@@ -14,6 +14,7 @@ type ProxyActionParams = {
body?: string;
headers?: Record<string, string>;
parseAsText?: boolean;
signal?: AbortSignal;
};
type ProxyActionResult<T> =
@@ -297,38 +297,53 @@ export type PaymentsResponse = {
totalCount: number;
};
};
type BridgePaymentType = "buy" | "sell" | "transfer";
type OnrampPaymentType = "onramp";
export type Payment = {
// common
id: string;
blockNumber?: bigint;
transactionId: string;
createdAt: string;
clientId: string;
sender: string;
receiver: string;
developerFeeRecipient: string;
developerFeeBps: number;
transactions: Array<{
chainId: number;
transactionHash: string;
}>;
status: "PENDING" | "COMPLETED" | "FAILED" | "NOT_FOUND";
type: "buy" | "sell" | "transfer";
originAmount: bigint;
destinationAmount: bigint;
purchaseData: unknown;
originToken: {
address: string;
symbol: string;
decimals: number;
chainId: number;
};
destinationAmount: string;
destinationToken: {
address: string;
symbol: string;
decimals: number;
chainId: number;
};
createdAt: string;
};
purchaseData: unknown;
} & (
| {
type: BridgePaymentType;
transactionId: string;
blockNumber?: string;
sender: string;
developerFeeRecipient: string;
developerFeeBps: number;
originAmount: string;
originToken: {
address: string;
symbol: string;
decimals: number;
chainId: number;
};
}
| {
onrampId: string;
type: OnrampPaymentType;
}
);
export type BridgePayment = Extract<Payment, { type: BridgePaymentType }>;
export async function getPayments(props: {
clientId: string;
@@ -1,4 +1,5 @@
"use client";
import type React from "react";
import { type Dispatch, type SetStateAction, useMemo } from "react";
import { CodeClient } from "@/components/ui/code/code.client";
@@ -6,11 +7,14 @@ import { TabButtons } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
export type CodeEnvironment =
| "api"
| "javascript"
| "typescript"
| "react"
| "react-native"
| "unity";
| "dotnet"
| "unity"
| "curl";
type SupportedEnvironment = {
environment: CodeEnvironment;
@@ -20,6 +24,10 @@ type SupportedEnvironment = {
type CodeSnippet = Partial<Record<CodeEnvironment, string>>;
const Environments: SupportedEnvironment[] = [
{
environment: "api",
title: "API",
},
{
environment: "javascript",
title: "JavaScript",
@@ -36,10 +44,18 @@ const Environments: SupportedEnvironment[] = [
environment: "react-native",
title: "React Native",
},
{
environment: "dotnet",
title: ".NET",
},
{
environment: "unity",
title: "Unity",
},
{
environment: "curl",
title: "cURL",
},
];
interface CodeSegmentProps {
@@ -122,9 +138,14 @@ export const CodeSegment: React.FC<CodeSegmentProps> = ({
: activeEnvironment === "react" ||
activeEnvironment === "react-native"
? "tsx"
: activeEnvironment === "unity"
: activeEnvironment === "unity" ||
activeEnvironment === "dotnet"
? "cpp"
: activeEnvironment
: activeEnvironment === "api"
? "javascript"
: activeEnvironment === "curl"
? "bash"
: activeEnvironment
}
/>
)}
@@ -3,7 +3,6 @@
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { useMemo } from "react";
import { AppFooter } from "@/components/footers/app-footer";
import {
Collapsible,
CollapsibleContent,
@@ -91,7 +90,6 @@ export function FullWidthSidebarLayout(props: {
<main className="flex min-w-0 grow flex-col max-sm:w-full">
{children}
</main>
<AppFooter />
</div>
</div>
);
@@ -23,7 +23,7 @@ type Action =
component: React.ReactNode;
};
function Action(props: { action: Action; variant?: "default" | "secondary" }) {
function Action(props: { action: Action; variant?: "default" | "outline" }) {
const action = props.action;
return "component" in action ? (
@@ -31,10 +31,7 @@ function Action(props: { action: Action; variant?: "default" | "secondary" }) {
) : (
<Button
asChild
className={cn(
"rounded-full",
props.variant === "secondary" && "border border-border",
)}
className={cn("rounded-full")}
size="sm"
variant={props.variant}
>
@@ -60,7 +57,7 @@ export type ProjectPageHeaderProps = {
icon: React.FC<{ className?: string }>;
isProjectIcon?: boolean;
actions: {
primary: Action;
primary?: Action;
secondary?: Action;
} | null;
@@ -114,7 +111,7 @@ export function ProjectPageHeader(props: ProjectPageHeaderProps) {
{props.actions && (
<div className="hidden lg:flex items-center gap-3">
{props.actions.secondary && (
<Action action={props.actions.secondary} variant="secondary" />
<Action action={props.actions.secondary} variant="outline" />
)}
{props.actions.primary && (
@@ -132,7 +129,7 @@ export function ProjectPageHeader(props: ProjectPageHeaderProps) {
{props.title}
</h2>
{/* description */}
<p className="text-sm text-muted-foreground line-clamp-3 md:line-clamp-2">
<p className="text-sm text-muted-foreground text-pretty">
{props.description}
</p>
</div>
@@ -145,7 +142,7 @@ export function ProjectPageHeader(props: ProjectPageHeaderProps) {
)}
{props.actions?.secondary && (
<Action action={props.actions.secondary} variant="secondary" />
<Action action={props.actions.secondary} variant="outline" />
)}
</div>
)}
@@ -87,7 +87,7 @@ export function ChatBar(props: {
<PopoverContent className="w-72">
<div>
<p className="mb-3 text-muted-foreground text-sm">
Get access to image uploads by signing in to Nebula
Get access to image uploads by signing in to thirdweb
</p>
<Button
className="w-full"
@@ -28,7 +28,10 @@ export function CustomChatButton(props: {
const closeModal = useCallback(() => setIsOpen(false), []);
const ref = useRef<HTMLDivElement>(null);
if (layoutSegments[0] === "~" && layoutSegments[1] === "support") {
if (
(layoutSegments[0] === "~" && layoutSegments[1] === "support") ||
layoutSegments.includes("ai")
) {
return null;
}
@@ -65,6 +65,23 @@ export default function Component() {
events: [preparedEvent]
});
}`,
unity: `using Thirdweb;
// Get your contract
var contract = await ThirdwebManager.Instance.GetContract(
address: "{{contract_address}}",
chainId: {{chainId}},
);
// Listen to contract events
var events = await contract.Events("{{function}}");`,
dotnet: `using Thirdweb;
// Get your contract
var contract = await ThirdwebContract.Create(client, "{{contract_address}}", {{chainId}});
// Listen to contract events
var events = await contract.Events("{{function}}");`,
},
install: {
javascript: "npm i thirdweb",
@@ -73,6 +90,9 @@ export default function Component() {
unity: `// Download the .unitypackage from the latest release:
// https://github.com/thirdweb-dev/unity-sdk/releases
// and drag it into your project`,
dotnet: `// Install the thirdweb SDK via NuGet:
// dotnet add package Thirdweb
// Or search for "Thirdweb" in your NuGet package manager`,
},
read: {
javascript: `import { readContract } from "thirdweb";
@@ -100,6 +120,33 @@ export default function Component() {
params: [{{args}}]
});
}`,
unity: `using Thirdweb;
// Get your contract
var contract = await ThirdwebManager.Instance.GetContract(
address: "{{contract_address}}",
chainId: {{chainId}}
);
// Read from the contract
var result = await contract.Read<T>(
"{{function}}",
{{args}}
);`,
dotnet: `using Thirdweb;
// Get your contract
var contract = await ThirdwebContract.Create(
client,
"{{contract_address}}",
{{chainId}}
);
// Read from the contract
var result = await contract.Read<T>(
"{{function}}",
{{args}}
);`,
},
setup: {
javascript: `import { createThirdwebClient, getContract } from "thirdweb";
@@ -165,11 +212,19 @@ function App() {
`,
unity: `using Thirdweb;
// Reference the SDK
var sdk = ThirdwebManager.Instance.SDK;
// Get your contract
var contract = await ThirdwebManager.Instance.GetContract(
address: "{{contract_address}}",
chainId: {{chainId}}
);`,
dotnet: `using Thirdweb;
// Get your contract
var contract = sdk.GetContract("{{contract_address}}");`,
var contract = await ThirdwebContract.Create(
client,
"{{contract_address}}",
{{chainId}}
);`,
},
write: {
javascript: `import { prepareContractCall, sendTransaction } from "thirdweb";
@@ -213,6 +268,123 @@ export default function Component() {
sendTransaction(transaction);
}
}`,
unity: `using Thirdweb;
// Get your contract
var contract = await ThirdwebManager.Instance.GetContract(
address: "{{contract_address}}",
chainId: {{chainId}}
);
// Write to the contract
var transactionReceipt = await contract.Write(
wallet,
contract,
"{{function}}",
weiValue,
{{args}}
);`,
dotnet: `using Thirdweb;
// Get your contract
var contract = await ThirdwebContract.Create(
client,
"{{contract_address}}",
{{chainId}}
);
// Write to the contract
var transactionReceipt = await contract.Write(
wallet,
contract,
"{{function}}",
weiValue,
{{args}}
);`,
},
api: {
read: `const response = await fetch('https://api.thirdweb.com/v1/contracts/read', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-secret-key': '<YOUR_SECRET_KEY>'
},
body: JSON.stringify({
calls: [
{
contractAddress: "{{contract_address}}",
method: "{{function}}",
params: [{{args}}]
}
],
chainId: {{chainId}}
})
});
const data = await response.json();`,
write: `const response = await fetch('https://api.thirdweb.com/v1/contracts/write', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-secret-key': '<YOUR_SECRET_KEY>'
},
body: JSON.stringify({
calls: [
{
contractAddress: "{{contract_address}}",
method: "{{function}}",
params: [{{args}}]
}
],
chainId: {{chainId}},
from: "<YOUR_WALLET_ADDRESS>"
})
});
const data = await response.json();`,
events: `const response = await fetch('https://api.thirdweb.com/v1/contracts/{{chainId}}/{{contract_address}}/events?eventSignature={{function}}', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-secret-key': '<YOUR_SECRET_KEY>'
}
});
const data = await response.json();`,
},
curl: {
read: `curl https://api.thirdweb.com/v1/contracts/read \\
--request POST \\
--header 'Content-Type: application/json' \\
--header 'x-secret-key: <YOUR_SECRET_KEY>' \\
--data '{
"calls": [
{
"contractAddress": "{{contract_address}}",
"method": "{{function}}",
"params": [{{args}}]
}
],
"chainId": {{chainId}}
}'`,
write: `curl -X POST https://api.thirdweb.com/v1/contracts/write \\
-H "Content-Type: application/json" \\
-H "x-secret-key: <YOUR_SECRET_KEY>" \\
-d '{
"calls": [
{
"contractAddress": "{{contract_address}}",
"method": "{{function}}",
"params": [{{args}}]
}
],
"chainId": {{chainId}},
"from": "<YOUR_WALLET_ADDRESS>"
}'`,
events: `curl https://api.thirdweb.com/v1/contracts/{{chainId}}/{{contract_address}}/events?eventSignature={{function}} \\
--request GET \\
--header 'Content-Type: application/json' \\
--header 'x-secret-key: <YOUR_SECRET_KEY>'`,
},
};
@@ -269,20 +441,17 @@ return (
public async void ConnectWallet()
{
// Reference to your Thirdweb SDK
var sdk = ThirdwebManager.Instance.SDK;
// Configure the connection
var connection = new WalletConnection(
provider: WalletProvider.SmartWallet, // The wallet provider you want to connect to (Required)
chainId: 1, // The chain you want to connect to (Required)
password: "myEpicPassword", // If using a local wallet as personal wallet (Optional)
email: "[email protected]", // If using an email wallet as personal wallet (Optional)
personalWallet: WalletProvider.LocalWallet // The personal wallet you want to use with your Account (Optional)
var wallet = await ConnectWallet(
new WalletOptions(
provider: WalletProvider.InAppWallet,
chainId: {{chainId}},
inAppWalletOptions: new InAppWalletOptions(
authprovider: AuthProvider.Google,
executionMode: ExecutionMode.EIP7702Sponsored
)
)
);
// Connect the wallet
string address = await sdk.wallet.Connect(connection);
string address = await wallet.GetAddress();
}`,
},
},
@@ -490,6 +659,9 @@ export function formatSnippet(
: "event",
});
break;
case "api":
// For API, we don't need special extension handling, just use the standard templates
break;
}
}
// end hacks on hacks on hacks -- now just hacks on hacks from here on out
@@ -533,7 +705,7 @@ export function CodeOverview(props: {
| undefined;
const [environment, setEnvironment] = useState<CodeEnvironment>(
defaultEnvironment || "javascript",
defaultEnvironment || "api",
);
const [tab, setTab] = useState("write");
@@ -677,7 +849,13 @@ export function CodeOverview(props: {
</div>
)}
<div className="flex flex-col gap-2">
{environment === "react-native" || environment === "unity" ? (
{environment === "api" || environment === "curl" ? (
<p className="text-sm text-muted-foreground">
No SDK install required for API/cURL. Call the HTTP endpoints
directly. Keep your secret key server-side; use client IDs in
frontends.
</p>
) : environment === "react-native" || environment === "unity" ? (
<p className="text-sm text-muted-foreground">
Install the latest version of the SDK.{" "}
<UnderlineLink
@@ -800,32 +978,76 @@ export function CodeOverview(props: {
<CodeSegment
environment={environment}
setEnvironment={setEnvironment}
snippet={formatSnippet(
// biome-ignore lint/suspicious/noExplicitAny: FIXME
COMMANDS[tab as keyof typeof COMMANDS] as any,
{
snippet={(() => {
const selectedFn =
tab === "read" ? read : tab === "write" ? write : event;
const commandsKey = tab as "read" | "write" | "events";
const baseSnippet = formatSnippet(COMMANDS[commandsKey], {
args:
abi
?.filter(
(f) => f.type === "function" || f.type === "event",
)
?.find(
(f) =>
f.name ===
(tab === "read"
? read?.name
: tab === "write"
? write?.name
: event?.name),
)
?.find((f) => f.name === selectedFn?.name)
?.inputs.map((i) => i.name || "") || [],
chainId,
contractAddress,
extensionNamespace,
fn: tab === "read" ? read : tab === "write" ? write : event,
},
)}
fn: selectedFn,
});
// Add API snippet if it exists
const apiSnippet = COMMANDS.api[commandsKey]
? formatSnippet(
{ api: COMMANDS.api[commandsKey] },
{
args:
abi
?.filter(
(f) =>
f.type === "function" || f.type === "event",
)
?.find((f) => f.name === selectedFn?.name)
?.inputs.map((i) => i.name || "") || [],
chainId,
contractAddress,
extensionNamespace,
fn: selectedFn,
},
)
: {};
// Add cURL snippet if it exists
const curlSnippet = COMMANDS.curl[commandsKey]
? formatSnippet(
{ curl: COMMANDS.curl[commandsKey] },
{
args:
abi
?.filter(
(f) =>
f.type === "function" || f.type === "event",
)
?.find((f) => f.name === selectedFn?.name)
?.inputs.map((i) => i.name || "") || [],
chainId,
contractAddress,
extensionNamespace,
fn: selectedFn,
},
)
: {};
return {
...baseSnippet,
...apiSnippet,
...curlSnippet,
};
})()}
/>
</div>
</div>
@@ -68,7 +68,7 @@ const ContractFunction: React.FC<{
function ContractFunctionInner(props: ContractFunctionProps) {
const { contract, fn } = props;
const [environment, setEnvironment] = useState<CodeEnvironment>("javascript");
const [environment, setEnvironment] = useState<CodeEnvironment>("api");
const functionSelectorQuery = useContractFunctionSelectors(contract);
const functionSelectors = functionSelectorQuery.data || [];
const isERC721Query = useReadContract(ERC721Ext.isERC721, { contract });
@@ -112,7 +112,7 @@ function ContractFunctionInner(props: ContractFunctionProps) {
: "write"
: ("events" as const);
const codeSnippet = formatSnippet(COMMANDS[commandsKey], {
const baseSnippet = formatSnippet(COMMANDS[commandsKey], {
args: fn.inputs?.map((i) => i.name || ""),
chainId: contract.chain.id,
contractAddress: contract.address,
@@ -120,6 +120,40 @@ function ContractFunctionInner(props: ContractFunctionProps) {
fn,
});
// Safety check: Only include API snippet if the command exists in COMMANDS.api
const apiSnippet = COMMANDS.api[commandsKey]
? formatSnippet(
{ api: COMMANDS.api[commandsKey] },
{
args: fn.inputs?.map((i) => i.name || ""),
chainId: contract.chain.id,
contractAddress: contract.address,
extensionNamespace,
fn,
},
)
: {};
// Safety check: Only include cURL snippet if the command exists in COMMANDS.curl
const curlSnippet = COMMANDS.curl[commandsKey]
? formatSnippet(
{ curl: COMMANDS.curl[commandsKey] },
{
args: fn.inputs?.map((i) => i.name || ""),
chainId: contract.chain.id,
contractAddress: contract.address,
extensionNamespace,
fn,
},
)
: {};
const codeSnippet = {
...baseSnippet,
...apiSnippet,
...curlSnippet,
};
return (
<div className="flex flex-col gap-1.5">
<div className="flex items-center flex-wrap gap-2 border-b pb-3 mb-3">
@@ -20,7 +20,7 @@ import {
toSerializableTransaction,
toWei,
} from "thirdweb";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { parseAbiParams, stringify, toFunctionSelector } from "thirdweb/utils";
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { SolidityInput } from "@/components/solidity-inputs";
@@ -34,6 +34,7 @@ import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/Spinner";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
function formatResponseData(data: unknown): {
type: "json" | "text";
@@ -264,7 +265,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = (
data: mutationData,
error: mutationError,
isPending: mutationLoading,
} = useSendAndConfirmTransaction();
} = useSendAndConfirmTx();
const {
mutate: readFn,
@@ -231,13 +231,13 @@ function ImportForm(props: {
</Button>
) : (
<Button className="gap-2" type="submit">
{addContractToProject.isPending ? (
{form.formState.isSubmitting ? (
<Spinner className="size-4" />
) : (
<ArrowDownToLineIcon className="size-4" />
)}
{addContractToProject.isPending ? "Importing" : "Import"}
{form.formState.isSubmitting ? "Importing" : "Import"}
</Button>
)}
</div>
@@ -1,7 +1,79 @@
import type {
ListUserWalletsData,
ListUserWalletsResponses,
} from "@thirdweb-dev/api";
import { configure, listUserWallets } from "@thirdweb-dev/api";
import type { WalletUser } from "thirdweb/wallets";
import { THIRDWEB_EWS_API_HOST } from "@/constants/urls";
import { THIRDWEB_API_HOST } from "@/constants/urls";
import type { SearchType } from "./types";
// Configure the API client to use the correct base URL
configure({
override: {
baseUrl: THIRDWEB_API_HOST,
},
});
// Extract types from the generated API
type APIWallet = ListUserWalletsResponses[200]["result"]["wallets"][0];
type APIProfile = APIWallet["profiles"][0];
// Transform API response to match existing WalletUser format
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
return {
id: getProfileId(apiWallet.profiles[0]) || "",
linkedAccounts: apiWallet.profiles.map((profile) => {
// Create details object based on the profile data
let details:
| { email: string; [key: string]: string }
| { phone: string; [key: string]: string }
| { address: string; [key: string]: string }
| { id: string; [key: string]: string };
const profileId = getProfileId(profile);
if ("email" in profile && profile.email) {
details = { email: profile.email, id: profileId };
} else if ("phone" in profile && profile.phone) {
details = { phone: profile.phone, id: profileId };
} else if ("walletAddress" in profile && profile.walletAddress) {
details = { address: profile.walletAddress, id: profileId };
} else {
details = { id: profileId };
}
return {
type: profile.type,
details,
};
}),
wallets: apiWallet.address
? [
{
address: apiWallet.address,
createdAt: apiWallet.createdAt || new Date().toISOString(),
type: "enclave" as const,
},
]
: [],
};
}
// Helper function to safely get ID from any profile type
function getProfileId(profile: APIProfile | undefined): string {
if (!profile) return "";
if ("id" in profile) {
return profile.id;
} else if ("credentialId" in profile) {
return profile.credentialId;
} else if ("identifier" in profile) {
return profile.identifier;
}
return "";
}
export async function searchUsers(
authToken: string,
clientId: string | undefined,
@@ -10,50 +82,57 @@ export async function searchUsers(
searchType: SearchType,
query: string,
): Promise<WalletUser[]> {
const url = new URL(`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`);
try {
// Prepare query parameters
const queryParams: ListUserWalletsData["query"] = {
limit: 50,
};
// Add clientId or ecosystemSlug parameter
if (ecosystemSlug) {
url.searchParams.append("ecosystemSlug", `ecosystem.${ecosystemSlug}`);
} else if (clientId) {
url.searchParams.append("clientId", clientId);
// Add search parameter based on search type
switch (searchType) {
case "email":
queryParams.email = query;
break;
case "phone":
queryParams.phone = query;
break;
case "id":
queryParams.id = query;
break;
case "address":
queryParams.address = query;
break;
case "externalWallet":
queryParams.externalWalletAddress = query;
break;
}
// Use the generated API function with Bearer authentication
const response = await listUserWallets({
query: queryParams,
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-thirdweb-team-id": teamId,
...(clientId && { "x-client-id": clientId }),
...(ecosystemSlug && {
"x-ecosystem-id": `ecosystem.${ecosystemSlug}`,
}),
},
});
// Handle response
if (response.error || !response.data) {
console.error(
"Error searching users:",
response.error || "No data returned",
);
return [];
}
return response.data.result.wallets.map(transformToWalletUser);
} catch (error) {
console.error("Error searching users:", error);
return [];
}
// Add search parameter based on search type
switch (searchType) {
case "email":
url.searchParams.append("email", query);
break;
case "phone":
url.searchParams.append("phone", query);
break;
case "id":
url.searchParams.append("id", query);
break;
case "address":
url.searchParams.append("address", query);
break;
case "externalWallet":
url.searchParams.append("externalWalletAddress", query);
break;
}
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-thirdweb-team-id": teamId,
...(clientId && { "x-client-id": clientId }),
},
method: "GET",
});
if (!response.ok) {
throw new Error(
`Failed to search users: ${response.status} ${response.statusText}`,
);
}
const data = await response.json();
return data.users as WalletUser[];
}
@@ -3,7 +3,6 @@ import type { ThirdwebClient } from "thirdweb";
import type { UseNetworkSwitcherModalOptions } from "thirdweb/react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner";
import { OPSponsoredChains } from "@/constants/chains";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { ChainIconClient } from "@/icons/ChainIcon";
import { cn } from "@/lib/utils";
@@ -32,7 +31,6 @@ export const CustomChainRenderer = ({
const { idToChain } = useAllChainsData();
const storedChain = idToChain.get(chain.id);
const isDeprecated = storedChain?.status === "deprecated";
const isSponsored = OPSponsoredChains.includes(chain.id);
return (
<div className="flex min-h-[48px] w-full cursor-pointer justify-start rounded-lg px-2 py-1 hover:bg-accent">
@@ -70,16 +68,6 @@ export const CustomChainRenderer = ({
Deprecated
</div>
)}
{isSponsored && (
<div
className="flex shrink-0 cursor-not-allowed items-center overflow-hidden rounded-xl px-2 py-1 font-medium text-xs"
style={{
background: "linear-gradient(to right, #701953, #5454B2)",
}}
>
Sponsored
</div>
)}
</div>
{switching && (
<div className="flex items-center gap-1 font-medium text-link-foreground text-xs">
@@ -56,7 +56,8 @@ export function NotificationsButton(props: { accountId: string }) {
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent
align="end"
className="max-h-[90vh] min-h-[500px] w-[400px] max-w-md p-0"
sideOffset={8}
className="max-h-[90vh] min-h-[550px] w-[450px] max-w-md p-0 flex flex-col rounded-xl overflow-hidden"
>
<NotificationList {...manager} />
</PopoverContent>
@@ -7,7 +7,7 @@ import {
parseISO,
subDays,
} from "date-fns";
import { ArchiveIcon } from "lucide-react";
import { ArchiveIcon, ArrowUpRightIcon, BellIcon } from "lucide-react";
import { useMemo } from "react";
import type { Notification } from "@/api/notifications";
import { Button } from "@/components/ui/button";
@@ -42,39 +42,48 @@ export function NotificationEntry({
}, [notification.createdAt]);
return (
<div className="flex flex-row py-1.5">
{onMarkAsRead && (
<div className="min-h-full w-1 shrink-0 rounded-r-lg bg-primary" />
)}
<div className="flex w-full flex-row justify-between gap-2 border-b px-4 py-2 transition-colors last:border-b-0">
<div className="flex items-start gap-3 p-4 border-b group hover:bg-card last:border-b-0">
<div className="p-2.5 rounded-full border bg-card">
<BellIcon className="size-4 text-muted-foreground" />
</div>
<div className="flex w-full flex-row justify-between gap-2 transition-colors">
<div className="flex items-start gap-3">
<div className="flex-1 space-y-1">
<div className="flex-1 space-y-1.5">
<p className="text-sm">{notification.description}</p>
{timeAgo && (
<p className="text-muted-foreground text-xs">{timeAgo}</p>
)}
<div className="flex flex-row justify-between gap-2 pt-1">
<Button asChild className="px-0" size="sm" variant="link">
<div className="flex flex-row justify-between gap-2 pt-2">
<Button
asChild
size="sm"
variant="outline"
className="rounded-full bg-card gap-2"
>
<a
href={notification.ctaUrl}
rel="noopener noreferrer"
target="_blank"
>
{notification.ctaText}
<ArrowUpRightIcon className="size-4 text-muted-foreground" />
</a>
</Button>
</div>
</div>
</div>
{onMarkAsRead && (
<Button
className="text-muted-foreground hover:text-foreground"
onClick={() => onMarkAsRead(notification.id)}
size="icon"
variant="ghost"
>
<ArchiveIcon className="h-4 w-4" />
</Button>
<div className="flex items-center">
<Button
className="p-2 h-auto opacity-0 group-hover:opacity-100 rounded-full text-muted-foreground hover:text-foreground"
onClick={() => onMarkAsRead(notification.id)}
size="sm"
variant="ghost"
aria-label="Archive notification"
>
<ArchiveIcon className="size-4" />
</Button>
</div>
)}
</div>
</div>
@@ -21,15 +21,15 @@ export function NotificationList(props: ReturnType<typeof useNotifications>) {
const scrollContainerRef = useRef<HTMLDivElement>(null);
return (
<div className="flex w-full flex-1 flex-col">
<div className="flex flex-col overflow-hidden grow">
<TabButtons
tabClassName="!text-sm hover:!bg-transparent !px-0 !py-1"
tabContainerClassName="space-x-6 px-4 pt-2"
tabClassName="hover:bg-transparent"
tabContainerClassName="pt-1.5 px-4"
tabs={[
{
isActive: activeTab === "inbox",
name: (
<div className="flex items-center gap-1">
<div className="flex items-center gap-2">
Inbox
{props.unreadNotificationsCount > 0 && (
<Badge variant="secondary">
@@ -49,7 +49,7 @@ export function NotificationList(props: ReturnType<typeof useNotifications>) {
/>
<div
className="h-full w-full flex-1 overflow-y-auto"
className="flex-1 overflow-y-auto overscroll-contain flex flex-col grow"
ref={scrollContainerRef}
>
{activeTab === "inbox" ? (
@@ -93,9 +93,9 @@ function InboxTab(
},
) {
return props.unreadNotifications.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
<BellIcon className="h-4 w-4 text-muted-foreground" />
<div className="flex grow flex-col items-center justify-center gap-4 p-4 text-center">
<div className="p-2 bg-card rounded-full border">
<BellIcon className="size-5 text-muted-foreground" />
</div>
<p className="text-muted-foreground text-sm">No new notifications</p>
</div>
@@ -131,9 +131,9 @@ function ArchiveTab(
},
) {
return props.archivedNotifications.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-4 p-8 text-center">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
<ArchiveIcon className="h-4 w-4 text-muted-foreground" />
<div className="flex grow flex-col items-center justify-center gap-4 p-4 text-center">
<div className="p-2 bg-card rounded-full border">
<ArchiveIcon className="size-5 text-muted-foreground" />
</div>
<p className="text-muted-foreground text-sm">No archived notifications</p>
</div>
@@ -12,6 +12,7 @@ import { forwardRef, useCallback, useRef, useState } from "react";
import { toast } from "sonner";
import {
prepareTransaction,
// eslint-disable-next-line no-restricted-imports
sendTransaction,
type ThirdwebClient,
toWei,
@@ -0,0 +1,45 @@
"use client";
import type React from "react";
import { useRef } from "react";
import { Button } from "@/components/ui/button";
interface ImageUploadProps {
value: File | undefined;
onChange?: (files: File[]) => void;
children?: React.ReactNode;
variant?: React.ComponentProps<typeof Button>["variant"];
className?: string;
multiple?: boolean;
accept: string;
}
export function ImageUploadButton(props: ImageUploadProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
props.onChange?.(files);
};
return (
<div>
<Button
className={props.className}
onClick={() => fileInputRef.current?.click()}
variant={props.variant}
>
{props.children}
</Button>
<input
accept={props.accept}
aria-label="Upload image"
className="hidden"
multiple={props.multiple}
onChange={handleFileChange}
ref={fileInputRef}
type="file"
/>
</div>
);
}
-23
View File
@@ -1,23 +0,0 @@
// TODO: move to API
export const OPSponsoredChains = [
// Optimism
10,
// Base
8453,
// Zora
7777777,
// Mode
34443,
// Frax
252,
// Cyber
7560,
// Redstone
690,
// Ancient8
888888888,
//Donatuz
42026,
//Mantle
5000,
];
@@ -29,3 +29,6 @@ export const NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET =
export const NEXT_PUBLIC_DEMO_ENGINE_URL =
process.env.NEXT_PUBLIC_DEMO_ENGINE_URL || "";
export const NEXT_PUBLIC_THIRDWEB_AI_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_AI_HOST || "https://nebula-api.thirdweb.com";
+2 -3
View File
@@ -1,6 +1,5 @@
export const THIRDWEB_EWS_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_EWS_API_HOST ||
"https://in-app-wallet.thirdweb.com";
export const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb-dev.com";
export const THIRDWEB_PAY_DOMAIN =
process.env.NEXT_PUBLIC_PAY_URL || "pay.thirdweb-dev.com";
+114 -29
View File
@@ -1,9 +1,25 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type {
ListUserWalletsData,
ListUserWalletsResponses,
} from "@thirdweb-dev/api";
import { configure, listUserWallets } from "@thirdweb-dev/api";
import { useActiveAccount } from "thirdweb/react";
import type { WalletUser } from "thirdweb/wallets";
import { THIRDWEB_EWS_API_HOST } from "@/constants/urls";
import { THIRDWEB_API_HOST } from "@/constants/urls";
import { embeddedWalletsKeys } from "../query-keys/cache-keys";
// Configure the API client to use the correct base URL
configure({
override: {
baseUrl: THIRDWEB_API_HOST,
},
});
// Extract types from the generated API
type APIWallet = ListUserWalletsResponses[200]["result"]["wallets"][0];
type APIProfile = APIWallet["profiles"][0];
const fetchAccountList = ({
jwt,
clientId,
@@ -18,37 +34,104 @@ const fetchAccountList = ({
pageNumber: number;
}) => {
return async () => {
const url = new URL(`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`);
try {
// Prepare query parameters for the new API
const queryParams: ListUserWalletsData["query"] = {
page: pageNumber,
limit: 50, // Keep the same page size
};
// Add clientId or ecosystemSlug parameter
if (ecosystemSlug) {
url.searchParams.append("ecosystemSlug", `ecosystem.${ecosystemSlug}`);
} else if (clientId) {
url.searchParams.append("clientId", clientId);
// Use the generated API function with Bearer authentication
const response = await listUserWallets({
query: queryParams,
headers: {
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/json",
"x-thirdweb-team-id": teamId,
...(clientId && { "x-client-id": clientId }),
...(ecosystemSlug && {
"x-ecosystem-id": `ecosystem.${ecosystemSlug}`,
}),
},
});
// Handle response
if (response.error || !response.data) {
const errorMessage =
typeof response.error === "string"
? response.error
: "No data returned";
throw new Error(errorMessage);
}
// Transform the response to match the expected format
return {
users: response.data.result.wallets.map(transformToWalletUser),
hasMore: response.data.result.pagination.hasMore ?? false,
};
} catch (error) {
console.error("Failed to fetch wallets:", error);
throw error;
}
url.searchParams.append("page", pageNumber.toString());
const res = await fetch(url.href, {
headers: {
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/json",
"x-thirdweb-team-id": teamId,
...(clientId && { "x-client-id": clientId }),
},
method: "GET",
});
if (!res.ok) {
throw new Error(`Failed to fetch wallets: ${await res.text()}`);
}
const json = await res.json();
return json as {
users: WalletUser[];
};
};
};
// Transform API response to match existing WalletUser format
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
return {
id: getProfileId(apiWallet.profiles[0]) || "",
linkedAccounts: apiWallet.profiles.map((profile) => {
// Create details object based on the profile data
let details:
| { email: string; [key: string]: string }
| { phone: string; [key: string]: string }
| { address: string; [key: string]: string }
| { id: string; [key: string]: string };
const profileId = getProfileId(profile);
if ("email" in profile && profile.email) {
details = { email: profile.email, id: profileId };
} else if ("phone" in profile && profile.phone) {
details = { phone: profile.phone, id: profileId };
} else if ("walletAddress" in profile && profile.walletAddress) {
details = { address: profile.walletAddress, id: profileId };
} else {
details = { id: profileId };
}
return {
type: profile.type,
details,
};
}),
wallets: apiWallet.address
? [
{
address: apiWallet.address,
createdAt: apiWallet.createdAt || new Date().toISOString(),
type: "enclave" as const,
},
]
: [],
};
}
// Helper function to safely get ID from any profile type
function getProfileId(profile: APIProfile | undefined): string {
if (!profile) return "";
if ("id" in profile) {
return profile.id;
} else if ("credentialId" in profile) {
return profile.credentialId;
} else if ("identifier" in profile) {
return profile.identifier;
}
return "";
}
export function useEmbeddedWallets(params: {
clientId?: string;
ecosystemSlug?: string;
@@ -98,6 +181,7 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
while (true) {
const res = await queryClient.fetchQuery<{
users: WalletUser[];
hasMore: boolean;
}>({
queryFn: fetchAccountList({
clientId,
@@ -113,12 +197,13 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
),
});
if (res.users.length === 0) {
responses.push(...res.users);
if (!res.hasMore) {
break;
}
page++;
responses.push(...res.users);
}
return responses;
+19
View File
@@ -0,0 +1,19 @@
import { useTheme } from "next-themes";
import {
type SendTransactionConfig,
// eslint-disable-next-line no-restricted-imports
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { getSDKTheme } from "@/utils/sdk-component-theme";
export function useSendAndConfirmTx(config?: SendTransactionConfig) {
const { theme } = useTheme();
const sendAndConfirmTransaction = useSendAndConfirmTransaction({
payModal: {
theme: getSDKTheme(theme === "light" ? "light" : "dark"),
},
...config,
});
return sendAndConfirmTransaction;
}
+3 -5
View File
@@ -9,7 +9,6 @@ import {
type Chain,
getAddress,
NATIVE_TOKEN_ADDRESS,
sendAndConfirmTransaction,
type ThirdwebClient,
type ThirdwebContract,
} from "thirdweb";
@@ -21,6 +20,7 @@ import { getWalletBalance } from "thirdweb/wallets";
import invariant from "tiny-invariant";
import { parseError } from "../utils/errorParser";
import { tryCatch } from "../utils/try-catch";
import { useSendAndConfirmTx } from "./useSendTx";
function getTokenBalancesQuery(params: {
ownerAddress: string;
@@ -79,6 +79,7 @@ export function useOwnedTokenBalances(params: {
export function useSplitDistributeFunds(contract: ThirdwebContract) {
const account = useActiveAccount();
const queryClient = useQueryClient();
const sendAndConfirmTx = useSendAndConfirmTx();
const params = {
ownerAddress: contract.address, // because we want to fetch the balance of split contract
@@ -106,10 +107,7 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) {
contract,
tokenAddress: currency.tokenAddress,
});
const promise = sendAndConfirmTransaction({
account,
transaction,
});
const promise = sendAndConfirmTx.mutateAsync(transaction);
toast.promise(promise, {
error: (err) => ({
+3 -3
View File
@@ -8,9 +8,9 @@ import {
} from "thirdweb";
import * as ERC20Ext from "thirdweb/extensions/erc20";
import * as VoteExt from "thirdweb/extensions/vote";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import type { Account } from "thirdweb/wallets";
import invariant from "tiny-invariant";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
export async function tokensDelegated(
options: BaseTransactionOptions<{ account: Account | undefined }>,
@@ -81,7 +81,7 @@ export async function votingTokenDecimals(options: BaseTransactionOptions) {
}
export function useDelegateMutation() {
const { mutateAsync } = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
return useMutation({
mutationFn: async (contract: ThirdwebContract) => {
@@ -97,7 +97,7 @@ export function useDelegateMutation() {
contract: tokenContract,
delegatee: contract.address,
});
return await mutateAsync(transaction);
return await sendAndConfirmTx.mutateAsync(transaction);
},
});
}
@@ -0,0 +1 @@
export { BringToFrontIcon as BridgeIcon } from "lucide-react";
+1 -1
View File
@@ -147,4 +147,4 @@ export type ApiKeyPayConfigValidationSchema = z.infer<
>;
// FIXME: Remove
export const HIDDEN_SERVICES = ["relayer", "chainsaw"];
export const HIDDEN_SERVICES = ["relayer", "chainsaw", "engineCloud"];
+29
View File
@@ -279,3 +279,32 @@ export function newAccountStub(overrides?: Partial<Account>): Account {
...overrides,
};
}
export function randomLorem(length: number) {
const loremWords = [
"lorem",
"ipsum",
"dolor",
"sit",
"amet",
"consectetur",
"adipiscing",
"elit",
"sed",
"do",
"eiusmod",
"tempor",
"incididunt",
"ut",
"labore",
"et",
"dolore",
"magna",
"aliqua",
];
return Array.from({ length }, () => {
const randomIndex = Math.floor(Math.random() * loremWords.length);
return loremWords[randomIndex];
}).join(" ");
}
@@ -17,7 +17,6 @@ import type { Chain, ChainMetadata } from "thirdweb/chains";
import {
useActiveAccount,
useActiveWalletChain,
useSendTransaction,
useSwitchActiveWalletChain,
useWalletBalance,
} from "thirdweb/react";
@@ -48,6 +47,7 @@ import {
NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET,
NEXT_PUBLIC_TURNSTILE_SITE_KEY,
} from "@/constants/public-envs";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
@@ -310,7 +310,7 @@ function SendFundsToFaucetModalContent(props: {
const account = useActiveAccount();
const activeChain = useActiveWalletChain();
const switchActiveWalletChain = useSwitchActiveWalletChain();
const sendTxMutation = useSendTransaction({
const sendAndConfirmTx = useSendAndConfirmTx({
payModal: false,
});
const switchChainMutation = useMutation({
@@ -334,7 +334,7 @@ function SendFundsToFaucetModalContent(props: {
value: toWei(values.amount.toString()),
});
const promise = sendTxMutation.mutateAsync(sendNativeTokenTx);
const promise = sendAndConfirmTx.mutateAsync(sendNativeTokenTx);
toast.promise(promise, {
error: `Failed to send ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
@@ -404,11 +404,11 @@ function SendFundsToFaucetModalContent(props: {
{activeChain.id === props.chain.id ? (
<Button
className="mt-4 w-full gap-2"
disabled={sendTxMutation.isPending}
disabled={sendAndConfirmTx.isPending}
key="submit"
type="submit"
>
{sendTxMutation.isPending && <Spinner className="size-4" />}
{sendAndConfirmTx.isPending && <Spinner className="size-4" />}
Send funds to faucet
</Button>
) : (
@@ -2,8 +2,8 @@
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { cancelAuction, cancelListing } from "thirdweb/extensions/marketplace";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import { TransactionButton } from "@/components/tx-button";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
interface CancelTabProps {
id: string;
@@ -21,15 +21,15 @@ export const CancelTab: React.FC<CancelTabProps> = ({
const transaction = isAuction
? cancelAuction({ auctionId: BigInt(id), contract })
: cancelListing({ contract, listingId: BigInt(id) });
const cancelQuery = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
return (
<TransactionButton
className="self-end"
client={contract.client}
isLoggedIn={isLoggedIn}
isPending={cancelQuery.isPending}
isPending={sendAndConfirmTx.isPending}
onClick={() => {
const promise = cancelQuery.mutateAsync(transaction, {
const promise = sendAndConfirmTx.mutateAsync(transaction, {
onError: (error) => {
console.error(error);
},
@@ -28,7 +28,7 @@ import type {
CreateListingParams,
} from "thirdweb/extensions/marketplace";
import { createAuction, createListing } from "thirdweb/extensions/marketplace";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { shortenAddress } from "thirdweb/utils";
import { CurrencySelector } from "@/components/blocks/CurrencySelector";
import { NFTMediaWithEmptyState } from "@/components/blocks/nft-media";
@@ -54,6 +54,7 @@ import {
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useDashboardOwnedNFTs } from "@/hooks/useDashboardOwnedNFTs";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import { useOwnedNFTsInsight } from "@/hooks/useWalletNFTs";
import { cn } from "@/lib/utils";
@@ -122,7 +123,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
chain: contract.chain,
});
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const listingNotifications = useTxNotifications(
"NFT listed Successfully",
"Failed to list NFT",
@@ -1,4 +1,6 @@
import { UploadIcon } from "lucide-react";
import { ArrowDownToLineIcon, UploadIcon } from "lucide-react";
import Papa from "papaparse";
import { handleDownload } from "@/components/blocks/download-file-button";
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { Button } from "@/components/ui/button";
import {
@@ -9,6 +11,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import type { SnapshotEntry } from "../../legacy-zod-schema";
import { useClaimConditionsFormContext } from "../index";
/**
@@ -118,10 +121,10 @@ export const ClaimerSelection = () => {
{/* Edit or See Snapshot */}
{snapshot ? (
<div className="flex items-center gap-3">
<div className="flex flex-col md:flex-row md:items-center gap-3">
{/* disable the "Edit" button when form is disabled, but not when it's a "See" button */}
<Button
className="gap-2 rounded-md"
className="gap-2"
disabled={disabledSnapshotButton}
onClick={() => setOpenIndex(phaseIndex)}
size="sm"
@@ -130,6 +133,21 @@ export const ClaimerSelection = () => {
<UploadIcon className="size-4" />
</Button>
{snapshot && snapshot.length > 0 && (
<Button
className="gap-2 rounded bg-background"
variant="outline"
disabled={disabledSnapshotButton}
onClick={() => {
downloadSnapshotAsCSV(snapshot);
}}
size="sm"
>
<ArrowDownToLineIcon className="size-4" />
Download
</Button>
)}
<div
className={cn(
"flex gap-2 items-center",
@@ -151,3 +169,25 @@ export const ClaimerSelection = () => {
</FormFieldSetup>
);
};
function downloadSnapshotAsCSV(snapshot: SnapshotEntry[]) {
const csvData = snapshot.map((entry) => ({
address: entry.address,
maxClaimable:
entry.maxClaimable === "0" ? "" : entry.maxClaimable?.toString() || "",
price: entry.price?.toString() || "",
currencyAddress: entry.currencyAddress || "",
}));
const csvContent = Papa.unparse(csvData, {
header: true,
delimiter: ",",
quotes: false,
});
handleDownload({
fileContent: csvContent,
fileNameWithExtension: "snapshot.csv",
fileFormat: "text/csv",
});
}
@@ -1,11 +1,6 @@
"use client";
import {
ArrowDownToLineIcon,
CircleAlertIcon,
CircleHelpIcon,
PlusIcon,
} from "lucide-react";
import { CircleAlertIcon, CircleHelpIcon, PlusIcon } from "lucide-react";
import {
createContext,
Fragment,
@@ -28,11 +23,7 @@ import {
ZERO_ADDRESS,
} from "thirdweb";
import { decimals } from "thirdweb/extensions/erc20";
import {
useActiveAccount,
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import invariant from "tiny-invariant";
import * as z from "zod";
import { ZodError } from "zod";
@@ -49,6 +40,7 @@ import { Form } from "@/components/ui/form";
import { Spinner } from "@/components/ui/Spinner";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useIsAdmin } from "@/hooks/useContractRoles";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import {
type ClaimConditionInput,
@@ -222,7 +214,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
Record<number, SnapshotEntry[] | undefined>
>({});
const sendTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const tokenDecimals = useReadContract(decimals, {
contract,
@@ -305,7 +297,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
const isFetchingData =
claimConditionsQuery.isFetching ||
sendTx.isPending ||
sendAndConfirmTx.isPending ||
// Need to make sure the tokenDecimals.data is present when interacting with ERC20 claim conditions
(isErc20 && tokenDecimals.isLoading);
@@ -428,7 +420,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
},
phasesWithSnapshots,
);
await sendTx.mutateAsync(tx);
await sendAndConfirmTx.mutateAsync(tx);
saveClaimPhaseNotification.onSuccess();
} catch (error) {
@@ -559,7 +551,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
>
<ClaimConditionsPhase
contract={contract}
isPending={sendTx.isPending}
isPending={sendAndConfirmTx.isPending}
onRemove={() => {
formFields.remove(index);
// Clean up snapshot when phase is removed
@@ -594,7 +586,8 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
<DropdownMenuTrigger asChild>
<Button
disabled={
sendTx.isPending || (!isMultiPhase && phases?.length > 0)
sendAndConfirmTx.isPending ||
(!isMultiPhase && phases?.length > 0)
}
size="sm"
variant="outline"
@@ -649,12 +642,11 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
client={contract.client}
disabled={claimConditionsQuery.isPending}
isLoggedIn={isLoggedIn}
isPending={sendTx.isPending}
isPending={sendAndConfirmTx.isPending}
transactionCount={undefined}
txChainID={contract.chain.id}
type="submit"
>
<ArrowDownToLineIcon className="size-3.5" />
{claimConditionsQuery.isPending
? "Saving Phases"
: "Save Phases"}
@@ -5,9 +5,9 @@ import type { ThirdwebContract } from "thirdweb";
import * as ERC20Ext from "thirdweb/extensions/erc20";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import { TransactionButton } from "@/components/tx-button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
export function ResetClaimEligibility({
@@ -23,7 +23,7 @@ export function ResetClaimEligibility({
isLoggedIn: boolean;
isMultiphase: boolean;
}) {
const sendTxMutation = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const txNotification = useTxNotifications(
"Successfully reset claim eligibility",
@@ -52,7 +52,7 @@ export function ResetClaimEligibility({
}
})();
sendTxMutation.mutate(tx, {
sendAndConfirmTx.mutate(tx, {
onError: (error) => {
txNotification.onError(error);
},
@@ -70,7 +70,7 @@ export function ResetClaimEligibility({
<TransactionButton
client={contract.client}
isLoggedIn={isLoggedIn}
isPending={sendTxMutation.isPending}
isPending={sendAndConfirmTx.isPending}
onClick={handleResetClaimEligibility}
size="sm"
variant="outline"
@@ -79,7 +79,7 @@ export function ResetClaimEligibility({
type="button"
className="text-destructive-text bg-card"
>
{sendTxMutation.isPending ? (
{sendAndConfirmTx.isPending ? (
"Resetting Eligibility"
) : (
<div className="flex items-center gap-2">
@@ -35,7 +35,7 @@ export function ContractPageLayout(props: {
} = props;
return (
<div>
<div className="flex flex-col grow">
<div className="border-border border-b py-8">
<div className="container flex flex-col gap-4">
<div className="flex flex-col justify-between gap-4 md:flex-row">
@@ -19,6 +19,12 @@ export async function isInsightSupportedForChain(
);
if (!res.ok) {
const errorText = await res.text();
console.error(
"failed to fetch chain services for chain",
chainId,
errorText,
);
return false;
}
@@ -5,9 +5,9 @@ import { useState } from "react";
import { toast } from "sonner";
import { prepareTransaction, type ThirdwebClient, toWei } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import { TransactionButton } from "@/components/tx-button";
import { Input } from "@/components/ui/input";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
export function DepositNative(props: {
@@ -17,7 +17,7 @@ export function DepositNative(props: {
isLoggedIn: boolean;
client: ThirdwebClient;
}) {
const sendTransaction = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const [amount, setAmount] = useState("");
return (
@@ -36,7 +36,7 @@ export function DepositNative(props: {
disabled={amount.length === 0 || Number.parseFloat(amount) <= 0}
isLoggedIn={props.isLoggedIn}
className="border-l-0 rounded-l-none px-6"
isPending={sendTransaction.isPending}
isPending={sendAndConfirmTx.isPending}
onClick={() => {
const transaction = prepareTransaction({
// eslint-disable-next-line no-restricted-syntax
@@ -45,7 +45,7 @@ export function DepositNative(props: {
to: props.address,
value: toWei(amount),
});
sendTransaction.mutate(transaction, {
sendAndConfirmTx.mutate(transaction, {
onSuccess: () => {
toast.success("Deposit successful");
setAmount("");
@@ -7,20 +7,17 @@ import {
getAccountsOfSigner,
isAccountDeployed,
} from "thirdweb/extensions/erc4337";
import {
useActiveAccount,
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { TransactionButton } from "@/components/tx-button";
import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
export function CreateAccountButton(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
}) {
const sendTxMutation = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const address = useActiveAccount()?.address;
@@ -63,14 +60,14 @@ export function CreateAccountButton(props: {
isLoggedIn={props.isLoggedIn}
variant="default"
size="sm"
isPending={sendTxMutation.isPending}
isPending={sendAndConfirmTx.isPending}
onClick={() => {
const tx = createAccount({
admin: address,
contract: props.contract,
data: "0x",
});
sendTxMutation.mutate(tx);
sendAndConfirmTx.mutate(tx);
}}
transactionCount={undefined}
txChainID={props.contract.chain.id}
@@ -3,8 +3,8 @@ import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
import { getVercelEnv } from "@/utils/vercel";
type InsightAggregationEntry = {
event_signature: string;
time: string;
topic_0: string;
day: string;
count: number;
};
@@ -26,12 +26,18 @@ export async function getContractEventBreakdown(params: {
startDate?: Date;
endDate?: Date;
}): Promise<EventBreakdownEntry[]> {
const daysDifference =
params.startDate && params.endDate
? Math.ceil(
(params.endDate.getTime() - params.startDate.getTime()) /
(1000 * 60 * 60 * 24),
)
: 30;
const queryParams = [
`chain=${params.chainId}`,
"group_by=time",
"group_by=topic_0 as event_signature",
"aggregate=toStartOfDay(toDate(block_timestamp)) as time",
"aggregate=count(*) as count",
"group_by=day",
"group_by=topic_0",
`limit=${daysDifference * 10}`, // at most 10 topics per day
params.startDate
? `filter_block_timestamp_gte=${getUnixTime(params.startDate)}`
: "",
@@ -42,14 +48,13 @@ export async function getContractEventBreakdown(params: {
.filter(Boolean)
.join("&");
const res = await fetch(
`https://insight.${thirdwebDomain}.com/v1/events/${params.contractAddress}?${queryParams}`,
{
headers: {
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
},
const url = `https://insight.${thirdwebDomain}.com/v1/events/${params.contractAddress}?${queryParams}`;
const res = await fetch(url, {
headers: {
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
},
);
});
if (!res.ok) {
throw new Error("Failed to fetch analytics data");
@@ -63,17 +68,16 @@ export async function getContractEventBreakdown(params: {
if (
typeof value === "object" &&
value !== null &&
"time" in value &&
"day" in value &&
"count" in value &&
"event_signature" in value &&
typeof value.event_signature === "string" &&
typeof value.time === "string" &&
typeof value.count === "number"
"topic_0" in value &&
typeof value.topic_0 === "string" &&
typeof value.day === "string"
) {
collectedAggregations.push({
count: value.count,
event_signature: value.event_signature,
time: value.time,
count: Number(value.count),
topic_0: value.topic_0,
day: value.day,
});
}
}
@@ -84,15 +88,15 @@ export async function getContractEventBreakdown(params: {
> = new Map();
for (const value of collectedAggregations) {
const mapKey = value.time;
const mapKey = value.day;
let valueForDay = dayToFunctionBreakdownMap.get(mapKey);
if (!valueForDay) {
valueForDay = {};
dayToFunctionBreakdownMap.set(mapKey, valueForDay);
}
valueForDay[value.event_signature] =
(valueForDay[value.event_signature] || 0) + value.count;
valueForDay[value.topic_0] =
(valueForDay[value.topic_0] || 0) + value.count;
}
const values: EventBreakdownEntry[] = [];
@@ -30,11 +30,17 @@ export async function getContractEventAnalytics(params: {
startDate?: Date;
endDate?: Date;
}): Promise<AnalyticsEntry[]> {
const daysDifference =
params.startDate && params.endDate
? Math.ceil(
(params.endDate.getTime() - params.startDate.getTime()) /
(1000 * 60 * 60 * 24),
)
: 30;
const queryParams = [
`chain=${params.chainId}`,
"group_by=time",
"aggregate=toStartOfDay(toDate(block_timestamp)) as time",
"aggregate=count(block_timestamp) as count",
"group_by=day",
`limit=${daysDifference}`,
params.startDate
? `filter_block_timestamp_gte=${getUnixTime(params.startDate)}`
: "",
@@ -45,17 +51,17 @@ export async function getContractEventAnalytics(params: {
.filter(Boolean)
.join("&");
const res = await fetch(
`https://insight.${thirdwebDomain}.com/v1/events/${params.contractAddress}?${queryParams}`,
{
headers: {
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
},
const url = `https://insight.${thirdwebDomain}.com/v1/events/${params.contractAddress}?${queryParams}`;
const res = await fetch(url, {
headers: {
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
},
);
});
if (!res.ok) {
throw new Error("Failed to fetch analytics data");
const errorText = await res.text();
throw new Error(`Failed to fetch analytics data: ${errorText}`);
}
const json = (await res.json()) as InsightResponse;
@@ -67,14 +73,13 @@ export async function getContractEventAnalytics(params: {
if (
typeof value === "object" &&
value !== null &&
"time" in value &&
"day" in value &&
"count" in value &&
typeof value.time === "string" &&
typeof value.count === "number"
typeof value.day === "string"
) {
values.push({
count: value.count,
time: new Date(value.time),
count: Number(value.count),
time: new Date(value.day),
});
}
}
@@ -6,7 +6,7 @@ type InsightResponse = {
aggregations: [
{
0: {
total: number;
count: number;
};
},
];
@@ -19,10 +19,9 @@ export async function getTotalContractEvents(params: {
contractAddress: string;
chainId: number;
}): Promise<{ count: number }> {
const queryParams = [
`chain=${params.chainId}`,
"aggregate=count(block_number) as total",
].join("&");
const queryParams = [`chain=${params.chainId}`, "aggregate=count()"].join(
"&",
);
const res = await fetch(
`https://insight.${thirdwebDomain}.com/v1/events/${params.contractAddress}?${queryParams}`,
@@ -40,6 +39,6 @@ export async function getTotalContractEvents(params: {
const json = (await res.json()) as InsightResponse;
return {
count: json.aggregations[0][0].total,
count: json.aggregations[0][0].count,
};
}
@@ -18,7 +18,6 @@ import {
getRpcClient,
prepareContractCall,
prepareTransaction,
sendAndConfirmTransaction,
type ThirdwebClient,
} from "thirdweb";
import type {
@@ -52,6 +51,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import {
@@ -178,6 +178,8 @@ export function DataTable({
return Array.from(chainMap.values());
}, [data, customChainData]);
const sendAndConfirmTx = useSendAndConfirmTx();
const form = useForm<FormSchema>({
defaultValues: {
amounts: {
@@ -216,10 +218,7 @@ export function DataTable({
],
});
await sendAndConfirmTransaction({
account: activeAccount,
transaction: sendErc20Tx,
});
await sendAndConfirmTx.mutateAsync(sendErc20Tx);
};
const crossChainTransferNotifications = useTxNotifications(
@@ -368,10 +367,7 @@ export function DataTable({
to: "0x4e59b44847b379578588920cA78FbF26c0B4956C",
});
await sendAndConfirmTransaction({
account: activeAccount,
transaction: tx,
});
await sendAndConfirmTx.mutateAsync(tx);
const code = await eth_getCode(
getRpcClient({
@@ -4,7 +4,7 @@ import { useMutation } from "@tanstack/react-query";
import { CircleAlertIcon } from "lucide-react";
import { useCallback } from "react";
import { useForm } from "react-hook-form";
import { sendAndConfirmTransaction, type ThirdwebClient } from "thirdweb";
import type { ThirdwebClient } from "thirdweb";
import { BatchMetadataERC721, BatchMetadataERC1155 } from "thirdweb/modules";
import { z } from "zod";
import { TransactionButton } from "@/components/tx-button";
@@ -25,6 +25,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import { parseAttributes } from "@/utils/parseAttributes";
import { fileBufferOrStringSchema } from "../zod-schemas";
@@ -63,7 +64,7 @@ export type UploadMetadataFormValues = z.infer<typeof uploadMetadataFormSchema>;
function BatchMetadataModule(props: ModuleInstanceProps) {
const { contract, ownerAccount } = props;
const sendAndConfirmTx = useSendAndConfirmTx();
const uploadMetadata = useCallback(
async (values: UploadMetadataFormValues) => {
if (!ownerAccount) {
@@ -80,12 +81,14 @@ function BatchMetadataModule(props: ModuleInstanceProps) {
metadatas: [nft],
});
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: uploadMetadataTx,
});
await sendAndConfirmTx.mutateAsync(uploadMetadataTx);
},
[contract, ownerAccount, props.contractInfo.name],
[
contract,
ownerAccount,
props.contractInfo.name,
sendAndConfirmTx.mutateAsync,
],
);
return (
@@ -15,7 +15,6 @@ import {
getContract,
NATIVE_TOKEN_ADDRESS,
type PreparedTransaction,
sendAndConfirmTransaction,
type ThirdwebClient,
toTokens,
ZERO_ADDRESS,
@@ -53,6 +52,7 @@ import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import { addressSchema } from "../zod-schemas";
import { CurrencySelector } from "./CurrencySelector";
@@ -76,7 +76,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
const { contract, ownerAccount } = props;
const account = useActiveAccount();
const [tokenId, setTokenId] = useState<string>("");
const sendAndConfirmTx = useSendAndConfirmTx();
const isValidTokenId = positiveIntegerRegex.test(tokenId);
const primarySaleRecipientQuery = useReadContract(
@@ -189,12 +189,9 @@ function ClaimableModule(props: ModuleInstanceProps) {
throw new Error("Invalid tokenId");
}
await sendAndConfirmTransaction({
account: account,
transaction: mintTx,
});
await sendAndConfirmTx.mutateAsync(mintTx);
},
[contract, account, props.contractInfo.name],
[contract, account, props.contractInfo.name, sendAndConfirmTx.mutateAsync],
);
const setClaimCondition = useCallback(
@@ -236,12 +233,14 @@ function ClaimableModule(props: ModuleInstanceProps) {
throw new Error("Invalid tokenId");
}
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: setClaimConditionTx,
});
await sendAndConfirmTx.mutateAsync(setClaimConditionTx);
},
[contract, ownerAccount, props.contractInfo.name],
[
contract,
ownerAccount,
props.contractInfo.name,
sendAndConfirmTx.mutateAsync,
],
);
const setPrimarySaleRecipient = useCallback(
@@ -260,12 +259,14 @@ function ClaimableModule(props: ModuleInstanceProps) {
primarySaleRecipient: values.primarySaleRecipient,
});
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: setSaleConfigTx,
});
await sendAndConfirmTx.mutateAsync(setSaleConfigTx);
},
[contract, ownerAccount, props.contractInfo.name],
[
contract,
ownerAccount,
props.contractInfo.name,
sendAndConfirmTx.mutateAsync,
],
);
return (
@@ -4,11 +4,7 @@ import { useMutation } from "@tanstack/react-query";
import { CircleAlertIcon } from "lucide-react";
import { useCallback } from "react";
import { useForm } from "react-hook-form";
import {
type PreparedTransaction,
sendAndConfirmTransaction,
type ThirdwebClient,
} from "thirdweb";
import type { PreparedTransaction, ThirdwebClient } from "thirdweb";
import {
grantRoles,
hasAllRoles,
@@ -40,6 +36,7 @@ import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import type { NFTMetadataInputLimited } from "@/types/modified-types";
import { parseAttributes } from "@/utils/parseAttributes";
@@ -73,7 +70,7 @@ const MINTER_ROLE = 1n;
function MintableModule(props: ModuleInstanceProps) {
const { contract, ownerAccount } = props;
const sendAndConfirmTx = useSendAndConfirmTx();
const isErc721 = props.contractInfo.name === "MintableERC721";
const primarySaleRecipientQuery = useReadContract(
@@ -106,10 +103,7 @@ function MintableModule(props: ModuleInstanceProps) {
user: ownerAccount.address,
});
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: grantRoleTx,
});
await sendAndConfirmTx.mutateAsync(grantRoleTx);
}
let mintTx: PreparedTransaction;
@@ -137,12 +131,15 @@ function MintableModule(props: ModuleInstanceProps) {
throw new Error("Invalid token ID");
}
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: mintTx,
});
await sendAndConfirmTx.mutateAsync(mintTx);
},
[contract, ownerAccount, hasMinterRole.data, props.contractInfo.name],
[
contract,
ownerAccount,
hasMinterRole.data,
props.contractInfo.name,
sendAndConfirmTx.mutateAsync,
],
);
const update = useCallback(
@@ -163,12 +160,14 @@ function MintableModule(props: ModuleInstanceProps) {
primarySaleRecipient: values.primarySaleRecipient,
});
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: setSaleConfigTx,
});
await sendAndConfirmTx.mutateAsync(setSaleConfigTx);
},
[contract, ownerAccount, props.contractInfo.name],
[
contract,
ownerAccount,
props.contractInfo.name,
sendAndConfirmTx.mutateAsync,
],
);
return (
@@ -7,10 +7,8 @@ import { toast } from "sonner";
import {
type Chain,
getContract,
sendTransaction,
type ThirdwebClient,
type ThirdwebContract,
waitForReceipt,
} from "thirdweb";
import {
checkModulesCompatibility,
@@ -40,6 +38,7 @@ import {
useAllVersions,
usePublishedContractsQuery,
} from "@/hooks/contract-hooks";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import {
ModuleInstallParams,
useModuleInstallParams,
@@ -76,7 +75,7 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
const { register, watch, formState, resetField, reset, setValue } = form;
const { contract, account } = props;
const { errors } = formState;
const sendAndConfirmTx = useSendAndConfirmTx();
const { data, isPending, isFetching } = usePublishedContractsQuery({
address: watch("publisherAddress"),
client: contract.client,
@@ -124,12 +123,7 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
version: watch("version"),
});
const txResult = await sendTransaction({
account,
transaction: installTransaction,
});
await waitForReceipt(txResult);
await sendAndConfirmTx.mutateAsync(installTransaction);
},
onError(err) {
toast.error("Failed to install module");
@@ -4,7 +4,7 @@ import { useMutation } from "@tanstack/react-query";
import { CircleAlertIcon } from "lucide-react";
import { useCallback } from "react";
import { useForm } from "react-hook-form";
import { sendAndConfirmTransaction, type ThirdwebClient } from "thirdweb";
import type { ThirdwebClient } from "thirdweb";
import { OpenEditionMetadataERC721 } from "thirdweb/modules";
import { z } from "zod";
import { TransactionButton } from "@/components/tx-button";
@@ -25,6 +25,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import { ModuleCardUI, type ModuleCardUIProps } from "./module-card";
import type { ModuleInstanceProps } from "./module-instance";
@@ -42,7 +43,7 @@ export type SetSharedMetadataFormValues = z.infer<
function OpenEditionMetadataModule(props: ModuleInstanceProps) {
const { contract, ownerAccount } = props;
const sendAndConfirmTx = useSendAndConfirmTx();
const setSharedMetadata = useCallback(
async (values: SetSharedMetadataFormValues) => {
if (!ownerAccount) {
@@ -59,12 +60,9 @@ function OpenEditionMetadataModule(props: ModuleInstanceProps) {
},
});
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: setSharedMetadataTx,
});
await sendAndConfirmTx.mutateAsync(setSharedMetadataTx);
},
[contract, ownerAccount],
[contract, ownerAccount, sendAndConfirmTx.mutateAsync],
);
return (
@@ -4,7 +4,7 @@ import { useMutation } from "@tanstack/react-query";
import { CircleAlertIcon } from "lucide-react";
import { useCallback } from "react";
import { useForm } from "react-hook-form";
import { sendAndConfirmTransaction, type ThirdwebClient } from "thirdweb";
import type { ThirdwebClient } from "thirdweb";
import { RoyaltyERC721, RoyaltyERC1155 } from "thirdweb/modules";
import { useReadContract } from "thirdweb/react";
import { z } from "zod";
@@ -26,6 +26,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import { addressSchema } from "../zod-schemas";
import { ModuleCardUI, type ModuleCardUIProps } from "./module-card";
@@ -33,7 +34,7 @@ import type { ModuleInstanceProps } from "./module-instance";
function RoyaltyModule(props: ModuleInstanceProps) {
const { contract, ownerAccount } = props;
const sendAndConfirmTx = useSendAndConfirmTx();
const isErc721 = props.contractInfo.name === "RoyaltyERC721";
const defaultRoyaltyInfoQuery = useReadContract(
@@ -70,12 +71,9 @@ function RoyaltyModule(props: ModuleInstanceProps) {
tokenId: BigInt(values.tokenId),
});
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: setRoyaltyForTokenTx,
});
await sendAndConfirmTx.mutateAsync(setRoyaltyForTokenTx);
},
[contract, ownerAccount, isErc721],
[contract, ownerAccount, isErc721, sendAndConfirmTx.mutateAsync],
);
const setTransferValidator = useCallback(
@@ -95,13 +93,16 @@ function RoyaltyModule(props: ModuleInstanceProps) {
validator: values.transferValidator,
});
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: setTransferValidatorTx,
});
await sendAndConfirmTx.mutateAsync(setTransferValidatorTx);
}
},
[contract, ownerAccount, transferValidatorQuery.data, isErc721],
[
contract,
ownerAccount,
transferValidatorQuery.data,
isErc721,
sendAndConfirmTx.mutateAsync,
],
);
const setDefaultRoyaltyInfo = useCallback(
@@ -128,13 +129,16 @@ function RoyaltyModule(props: ModuleInstanceProps) {
royaltyRecipient: values.recipient,
});
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: setSaleConfigTx,
});
await sendAndConfirmTx.mutateAsync(setSaleConfigTx);
}
},
[contract, ownerAccount, defaultRoyaltyInfoQuery.data, isErc721],
[
contract,
ownerAccount,
defaultRoyaltyInfoQuery.data,
isErc721,
sendAndConfirmTx.mutateAsync,
],
);
return (
@@ -6,7 +6,6 @@ import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react";
import { useCallback } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { sendAndConfirmTransaction } from "thirdweb";
import {
TransferableERC20,
TransferableERC721,
@@ -29,6 +28,7 @@ import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { addressSchema } from "../zod-schemas";
import { ModuleCardUI, type ModuleCardUIProps } from "./module-card";
import type { ModuleInstanceProps } from "./module-instance";
@@ -46,7 +46,7 @@ export type TransferableModuleFormValues = z.infer<typeof formSchema>;
function TransferableModule(props: ModuleInstanceProps) {
const { contract, ownerAccount } = props;
const sendAndConfirmTx = useSendAndConfirmTx();
const isTransferEnabledQuery = useReadContract(
props.contractInfo.name === "TransferableERC721"
? TransferableERC721.isTransferEnabled
@@ -79,10 +79,7 @@ function TransferableModule(props: ModuleInstanceProps) {
enableTransfer: transferEnabled,
});
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: setTransferableTx,
});
await sendAndConfirmTx.mutateAsync(setTransferableTx);
}
await Promise.all(
@@ -99,10 +96,7 @@ function TransferableModule(props: ModuleInstanceProps) {
target: address,
});
await sendAndConfirmTransaction({
account: ownerAccount,
transaction: setTransferableForTx,
});
await sendAndConfirmTx.mutateAsync(setTransferableForTx);
}),
);
},
@@ -111,6 +105,7 @@ function TransferableModule(props: ModuleInstanceProps) {
ownerAccount,
isTransferEnabledQuery.data,
props.contractInfo.name,
sendAndConfirmTx.mutateAsync,
],
);
@@ -8,10 +8,8 @@ import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
import { toast } from "sonner";
import {
getContract,
sendTransaction,
type ThirdwebClient,
type ThirdwebContract,
waitForReceipt,
} from "thirdweb";
import { uninstallModuleByProxy } from "thirdweb/modules";
import type { Account } from "thirdweb/wallets";
@@ -30,6 +28,7 @@ import {
} from "@/components/ui/dialog";
import { Spinner } from "@/components/ui/Spinner";
import { Skeleton } from "@/components/ui/skeleton";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { ModuleInstance } from "./module-instance";
import { useModuleContractInfo } from "./moduleContractInfo";
@@ -50,6 +49,7 @@ type ModuleCardProps = {
export function ModuleCard(props: ModuleCardProps) {
const { contract, moduleAddress, ownerAccount } = props;
const [isUninstallModalOpen, setIsUninstallModalOpen] = useState(false);
const sendAndConfirmTx = useSendAndConfirmTx();
const contractInfo = useModuleContractInfo(
getContract({
@@ -60,7 +60,7 @@ export function ModuleCard(props: ModuleCardProps) {
);
const uninstallMutation = useMutation({
mutationFn: async (account: Account) => {
mutationFn: async () => {
const uninstallTransaction = uninstallModuleByProxy({
chain: contract.chain,
client: contract.client,
@@ -69,12 +69,7 @@ export function ModuleCard(props: ModuleCardProps) {
moduleProxyAddress: moduleAddress,
});
const txResult = await sendTransaction({
account,
transaction: uninstallTransaction,
});
await waitForReceipt(txResult);
await sendAndConfirmTx.mutateAsync(uninstallTransaction);
},
onError(error) {
toast.error("Failed to uninstall module");
@@ -87,12 +82,7 @@ export function ModuleCard(props: ModuleCardProps) {
});
const handleRemove = async () => {
if (!ownerAccount) {
toast.error("Wallet is not connected");
return;
}
uninstallMutation.mutate(ownerAccount);
uninstallMutation.mutate();
};
if (!contractInfo) {
@@ -7,7 +7,7 @@ import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { multicall } from "thirdweb/extensions/common";
import { balanceOf, encodeSafeTransferFrom } from "thirdweb/extensions/erc1155";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { TransactionButton } from "@/components/tx-button";
import { Button } from "@/components/ui/button";
import {
@@ -17,6 +17,7 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import { cn } from "@/lib/utils";
import {
@@ -44,7 +45,7 @@ const AirdropTab: React.FC<AirdropTabProps> = ({
}>({
defaultValues: { addresses: [] },
});
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const addresses = watch("addresses");
const [open, setOpen] = useState(false);
const airdropNotifications = useTxNotifications(
@@ -8,7 +8,6 @@ import {
useActiveAccount,
useInvalidateContractQuery,
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { TransactionButton } from "@/components/tx-button";
import {
@@ -21,6 +20,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
type BurnTabProps = {
@@ -48,7 +48,7 @@ function BurnTab({ contract, tokenId, isLoggedIn }: BurnTabProps) {
isERC1155,
{ contract },
);
const { mutate, isPending } = useSendAndConfirmTransaction();
const { mutate, isPending } = useSendAndConfirmTx();
const onSubmit = (data: { to: string; amount: string }) => {
const transaction = isErc721
@@ -6,7 +6,7 @@ import { toast } from "sonner";
import { isAddress, type ThirdwebContract } from "thirdweb";
import { getApprovalForTransaction } from "thirdweb/extensions/erc20";
import { claimTo } from "thirdweb/extensions/erc1155";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { z } from "zod";
import { TransactionButton } from "@/components/tx-button";
import {
@@ -18,6 +18,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
const claimFormSchema = z.object({
@@ -54,7 +55,7 @@ const ClaimTabERC1155: React.FC<ClaimTabProps> = ({
resolver: zodResolver(claimFormSchema),
defaultValues: { amount: "1", to: address },
});
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const account = useActiveAccount();
return (
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { isAddress, type ThirdwebContract } from "thirdweb";
import { mintAdditionalSupplyTo } from "thirdweb/extensions/erc1155";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { z } from "zod";
import { TransactionButton } from "@/components/tx-button";
import {
@@ -18,6 +18,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
interface MintSupplyTabProps {
contract: ThirdwebContract;
@@ -47,7 +48,7 @@ const MintSupplyTab: React.FC<MintSupplyTabProps> = ({
resolver: zodResolver(mintAdditionalSupplyFormSchema),
});
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
function onSubmit(values: z.input<typeof mintAdditionalSupplyFormSchema>) {
const transaction = mintAdditionalSupplyTo({
@@ -4,11 +4,7 @@ import { useForm } from "react-hook-form";
import type { ThirdwebContract } from "thirdweb";
import { transferFrom } from "thirdweb/extensions/erc721";
import { isERC1155, safeTransferFrom } from "thirdweb/extensions/erc1155";
import {
useActiveAccount,
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { SolidityInput } from "@/components/solidity-inputs";
import { TransactionButton } from "@/components/tx-button";
@@ -21,6 +17,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
type TransferTabProps = {
@@ -47,7 +44,7 @@ function TransferTab({ contract, tokenId, isLoggedIn }: TransferTabProps) {
{ contract },
);
const sendTxAndConfirm = useSendAndConfirmTransaction();
const sendTxAndConfirm = useSendAndConfirmTx();
if (checking1155) {
return <GenericLoadingPage />;
@@ -12,7 +12,7 @@ import {
updateMetadata as updateMetadata1155,
updateTokenURI as updateTokenURI1155,
} from "thirdweb/extensions/erc1155";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import type { NFTMetadata } from "thirdweb/utils";
import { OpenSeaPropertyBadge } from "@/components/badges/opensea";
import { FileInput } from "@/components/blocks/FileInput";
@@ -36,6 +36,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import type { NFTMetadataInputLimited } from "@/types/modified-types";
import { parseAttributes } from "@/utils/parseAttributes";
@@ -110,7 +111,7 @@ export function UpdateNftMetadata({
external_url,
} = getUploadedNFTMediaMeta(form);
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const updateMetadataNotifications = useTxNotifications(
"NFT metadata updated successfully",
"Failed to update NFT metadata",
@@ -5,7 +5,7 @@ import { useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import { useReadContract, useSendAndConfirmTransaction } from "thirdweb/react";
import { useReadContract } from "thirdweb/react";
import { MinterOnly } from "@/components/contracts/roles/minter-only";
import { Button } from "@/components/ui/button";
import {
@@ -15,6 +15,7 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import { BatchLazyMint } from "./batch-upload/batch-lazy-mint";
@@ -40,7 +41,7 @@ export const BatchLazyMintButton: React.FC<BatchLazyMintButtonProps> = ({
},
);
const sendTxMutation = useSendAndConfirmTransaction();
const sendTxMutation = useSendAndConfirmTx();
const txNotifications = useTxNotifications(
"Batch uploaded successfully",
@@ -8,7 +8,7 @@ import { toast } from "sonner";
import { isAddress, type ThirdwebContract } from "thirdweb";
import { getApprovalForTransaction } from "thirdweb/extensions/erc20";
import { claimTo } from "thirdweb/extensions/erc721";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { z } from "zod";
import { TransactionButton } from "@/components/tx-button";
import { Button } from "@/components/ui/button";
@@ -29,6 +29,7 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
const claimFormSchema = z.object({
@@ -61,7 +62,7 @@ export function NFTClaimButton({ contract, isLoggedIn }: NFTClaimButtonProps) {
resolver: zodResolver(claimFormSchema),
defaultValues: { amount: "1", to: address },
});
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const account = useActiveAccount();
const [open, setOpen] = useState(false);
@@ -6,7 +6,7 @@ import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { lazyMint as lazyMint721 } from "thirdweb/extensions/erc721";
import { lazyMint as lazyMint1155 } from "thirdweb/extensions/erc1155";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { OpenSeaPropertyBadge } from "@/components/badges/opensea";
import { FileInput } from "@/components/blocks/FileInput";
import { PropertiesFormControl } from "@/components/contracts/properties.shared";
@@ -29,6 +29,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import type { NFTMetadataInputLimited } from "@/types/modified-types";
import { parseAttributes } from "@/utils/parseAttributes";
@@ -53,7 +54,7 @@ export function LazyMintNftForm({
isLoggedIn,
}: LazyMintNftFormParams) {
const address = useActiveAccount()?.address;
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const form = useForm<NFTMetadataInputLimited>();
@@ -6,7 +6,7 @@ import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { mintTo as erc721MintTo } from "thirdweb/extensions/erc721";
import { mintTo as erc1155MintTo } from "thirdweb/extensions/erc1155";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { OpenSeaPropertyBadge } from "@/components/badges/opensea";
import { FileInput } from "@/components/blocks/FileInput";
import { PropertiesFormControl } from "@/components/contracts/properties.shared";
@@ -29,6 +29,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import type { NFTMetadataInputLimited } from "@/types/modified-types";
import { parseAttributes } from "@/utils/parseAttributes";
@@ -81,7 +82,7 @@ export function NFTMintForm({
external_url,
} = getUploadedNFTMediaMeta(form);
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const nftMintNotifications = useTxNotifications(
"NFT minted successfully",
"Failed to mint NFT",
@@ -7,7 +7,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { getBatchesToReveal, reveal } from "thirdweb/extensions/erc721";
import { useReadContract, useSendAndConfirmTransaction } from "thirdweb/react";
import { useReadContract } from "thirdweb/react";
import * as z from "zod";
import { MinterOnly } from "@/components/contracts/roles/minter-only";
import { TransactionButton } from "@/components/tx-button";
@@ -36,6 +36,7 @@ import {
SheetTrigger,
} from "@/components/ui/sheet";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
const revealFormSchema = z.object({
@@ -56,7 +57,7 @@ export function NFTRevealButton({
contract,
});
const sendTxMutation = useSendAndConfirmTransaction();
const sendTxMutation = useSendAndConfirmTx();
const form = useForm<RevealFormData>({
resolver: zodResolver(revealFormSchema),
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { setSharedMetadata } from "thirdweb/extensions/erc721";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { FileInput } from "@/components/blocks/FileInput";
import { TransactionButton } from "@/components/tx-button";
import {
@@ -26,6 +26,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import type { NFTMetadataInputLimited } from "@/types/modified-types";
import { parseError } from "@/utils/errorParser";
import { parseAttributes } from "@/utils/parseAttributes";
@@ -44,7 +45,7 @@ export function SharedMetadataForm({
isLoggedIn: boolean;
}) {
const address = useActiveAccount()?.address;
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const form = useForm<NFTMetadataInputLimited>();
const setFile = (file: File) => {
@@ -5,11 +5,7 @@ import { useMemo } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import {
useActiveAccount,
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { TransactionButton } from "@/components/tx-button";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
@@ -18,6 +14,7 @@ import {
createSetAllRoleMembersTx,
getAllRoleMembers,
} from "@/hooks/contract-ui/permissions";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
import { ContractPermission } from "./contract-permission";
@@ -36,7 +33,7 @@ export function Permissions({
const allRoleMembers = useReadContract(getAllRoleMembers, {
contract,
});
const sendTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const transformedQueryData = useMemo(() => {
if (!allRoleMembers.data) {
@@ -86,7 +83,7 @@ export function Permissions({
roleMemberMap: cleanedValues,
});
sendTx.mutate(tx, {
sendAndConfirmTx.mutate(tx, {
onError: (error) => {
toast.error("Failed to update permissions", {
description: parseError(error),
@@ -115,7 +112,7 @@ export function Permissions({
<div className="flex justify-end gap-3 p-4 lg:p-6">
<Button
variant="outline"
disabled={!allRoleMembers.data || sendTx.isPending}
disabled={!allRoleMembers.data || sendAndConfirmTx.isPending}
onClick={() => form.reset(allRoleMembers.data)}
className="gap-2 bg-background"
>
@@ -125,13 +122,15 @@ export function Permissions({
<TransactionButton
client={contract.client}
isLoggedIn={isLoggedIn}
isPending={sendTx.isPending}
isPending={sendAndConfirmTx.isPending}
transactionCount={undefined}
txChainID={contract.chain.id}
type="submit"
variant="default"
>
{sendTx.isPending ? "Updating permissions" : "Update permissions"}
{sendAndConfirmTx.isPending
? "Updating permissions"
: "Update permissions"}
</TransactionButton>
</div>
</form>
@@ -6,7 +6,6 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import * as VoteExt from "thirdweb/extensions/vote";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import { TransactionButton } from "@/components/tx-button";
import { Button } from "@/components/ui/button";
import {
@@ -25,6 +24,7 @@ import {
SheetTrigger,
} from "@/components/ui/sheet";
import { Textarea } from "@/components/ui/textarea";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
export function ProposalButton({
@@ -35,7 +35,7 @@ export function ProposalButton({
isLoggedIn: boolean;
}) {
const [open, setOpen] = useState(false);
const sendTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const form = useForm<{ description: string }>({
defaultValues: {
description: "",
@@ -51,7 +51,7 @@ export function ProposalButton({
values: [0n],
});
await sendTx.mutateAsync(tx, {
await sendAndConfirmTx.mutateAsync(tx, {
onError: (error) => {
toast.error("Failed to create proposal", {
description: parseError(error),
@@ -105,7 +105,7 @@ export function ProposalButton({
<div className="mt-6 flex flex-row justify-end gap-3">
<Button
disabled={sendTx.isPending}
disabled={sendAndConfirmTx.isPending}
onClick={() => setOpen(false)}
variant="outline"
>
@@ -114,7 +114,7 @@ export function ProposalButton({
<TransactionButton
client={contract.client}
isLoggedIn={isLoggedIn}
isPending={sendTx.isPending}
isPending={sendAndConfirmTx.isPending}
transactionCount={1}
txChainID={contract.chain.id}
type="submit"
@@ -6,16 +6,13 @@ import { useState } from "react";
import { toast } from "sonner";
import { type ThirdwebContract, toTokens } from "thirdweb";
import * as VoteExt from "thirdweb/extensions/vote";
import {
useActiveAccount,
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { TransactionButton } from "@/components/tx-button";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { tokensDelegated, votingTokenDecimals } from "@/hooks/useVote";
import { parseError } from "@/utils/errorParser";
@@ -62,7 +59,7 @@ export function Proposal({ proposal, contract, isLoggedIn }: ProposalProps) {
queryOptions: { enabled: !!account },
});
const sendTx = useSendAndConfirmTransaction();
const sendTx = useSendAndConfirmTx();
const [voteType, setVoteType] = useState<VoteExt.VoteType | null>(null);
@@ -9,23 +9,16 @@ import {
XIcon,
} from "lucide-react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { useId, useState } from "react";
import { toast } from "sonner";
import {
padHex,
sendAndConfirmTransaction,
type ThirdwebContract,
toTokens,
waitForReceipt,
} from "thirdweb";
import { padHex, type ThirdwebContract, toTokens } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import {
claimTo,
type getActiveClaimCondition,
getApprovalForTransaction,
} from "thirdweb/extensions/erc20";
import { useActiveAccount, useSendTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { getClaimParams, maxUint256 } from "thirdweb/utils";
import {
reportAssetBuyFailed,
@@ -37,9 +30,9 @@ import { DecimalInput } from "@/components/ui/decimal-input";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/Spinner";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { cn } from "@/lib/utils";
import { parseError } from "@/utils/errorParser";
import { getSDKTheme } from "@/utils/sdk-component-theme";
import { tryCatch } from "@/utils/try-catch";
import { PublicPageConnectButton } from "../../../_components/PublicPageConnectButton";
import { SupplyClaimedProgress } from "../../../_components/supply-claimed-progress";
@@ -65,13 +58,8 @@ export function TokenDropClaim(props: {
const [quantity, setQuantity] = useState("1");
const account = useActiveAccount();
const { theme } = useTheme();
const sendClaimTx = useSendTransaction({
payModal: {
theme: getSDKTheme(theme === "light" ? "light" : "dark"),
},
});
const sendAndConfirmApproveTx = useSendAndConfirmTx();
const sendAndConfirmClaimTx = useSendAndConfirmTx();
const [successScreen, setSuccessScreen] = useState<
| undefined
@@ -116,10 +104,7 @@ export function TokenDropClaim(props: {
});
const approveTxResult = await tryCatch(
sendAndConfirmTransaction({
account,
transaction: approveTx,
}),
sendAndConfirmApproveTx.mutateAsync(approveTx),
);
if (approveTxResult.error) {
@@ -152,8 +137,8 @@ export function TokenDropClaim(props: {
}
async function sendAndConfirm() {
const result = await sendClaimTx.mutateAsync(transaction);
return await waitForReceipt(result);
const result = await sendAndConfirmClaimTx.mutateAsync(transaction);
return result;
}
setStepsUI({
@@ -28,7 +28,7 @@ export async function ERC20PublicPage(props: {
contractMetadata,
activeClaimCondition,
tokenDecimals,
tokenInfo,
tokenInfoFromUB,
functionSelectors,
tokenPriceData,
] = await Promise.all([
@@ -51,8 +51,8 @@ export async function ERC20PublicPage(props: {
}),
]);
if (!contractMetadata.image && tokenInfo) {
contractMetadata.image = tokenInfo.iconUri;
if (!contractMetadata.image && tokenInfoFromUB) {
contractMetadata.image = tokenInfoFromUB.iconUri;
}
const [contractCreator, claimConditionCurrencyMeta] = await Promise.all([
@@ -70,24 +70,15 @@ export async function ERC20PublicPage(props: {
const cookieStore = await cookies();
const isDashboardUser = cookieStore.has(HAS_USED_DASHBOARD);
const buyEmbed = (
<BuyEmbed
chainMetadata={props.chainMetadata}
claimConditionMeta={
activeClaimCondition && claimConditionCurrencyMeta
? {
activeClaimCondition,
claimConditionCurrency: claimConditionCurrencyMeta,
}
: undefined
}
clientContract={props.clientContract}
tokenAddress={props.clientContract.address}
tokenDecimals={tokenDecimals}
tokenName={contractMetadata.name}
tokenSymbol={contractMetadata.symbol}
/>
);
const claimConditionMeta =
activeClaimCondition && claimConditionCurrencyMeta
? {
activeClaimCondition,
claimConditionCurrency: claimConditionCurrencyMeta,
}
: undefined;
const isUBSupported = !!tokenInfoFromUB;
const showBuyEmbed = isUBSupported || claimConditionMeta;
return (
<div className="flex grow flex-col">
@@ -116,21 +107,33 @@ export async function ERC20PublicPage(props: {
<div className="container flex max-w-5xl grow flex-col pt-8 pb-10">
<div className="flex grow flex-col gap-8">
<div className="sm:flex sm:justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 rounded-lg overflow-hidden relative">
<GridPattern
width={30}
height={30}
x={-1}
y={-1}
strokeDasharray={"4 2"}
className="text-border dark:text-border/70"
style={{
maskImage:
"linear-gradient(to bottom right,white,transparent,transparent)",
}}
/>
<div className="sm:w-[420px] z-10">{buyEmbed}</div>
</div>
{showBuyEmbed && (
<div className="sm:flex sm:justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 rounded-lg overflow-hidden relative">
<GridPattern
width={30}
height={30}
x={-1}
y={-1}
strokeDasharray={"4 2"}
className="text-border dark:text-border/70"
style={{
maskImage:
"linear-gradient(to bottom right,white,transparent,transparent)",
}}
/>
<div className="sm:w-[420px] z-10">
<BuyEmbed
chainMetadata={props.chainMetadata}
claimConditionMeta={claimConditionMeta}
clientContract={props.clientContract}
tokenAddress={props.clientContract.address}
tokenDecimals={tokenDecimals}
tokenName={contractMetadata.name}
tokenSymbol={contractMetadata.symbol}
/>
</div>
</div>
)}
{tokenPriceData ? (
<>
@@ -9,7 +9,7 @@ import { type ThirdwebContract, toTokens } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { getApprovalForTransaction } from "thirdweb/extensions/erc20";
import { claimTo } from "thirdweb/extensions/erc1155";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { maxUint256 } from "thirdweb/utils";
import * as z from "zod";
import {
@@ -28,6 +28,7 @@ import {
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
import { PublicPageConnectButton } from "../../../_components/PublicPageConnectButton";
import { SupplyClaimedProgress } from "../../../_components/supply-claimed-progress";
@@ -57,7 +58,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
reValidateMode: "onChange",
});
const nftAmountToClaim = Number(form.watch("amount"));
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const account = useActiveAccount();
const queryClient = useQueryClient();
@@ -4,12 +4,13 @@ import type { NFT, ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { getApprovalForTransaction } from "thirdweb/extensions/erc20";
import { claimTo, getNFT } from "thirdweb/extensions/erc721";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { getClaimParams } from "thirdweb/utils";
import {
reportAssetBuyFailed,
reportAssetBuySuccessful,
} from "@/analytics/report";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
import { getCurrencyMeta } from "../../../erc20/_utils/getCurrencyMeta";
import {
@@ -27,7 +28,7 @@ export type BuyNFTDropProps = Omit<
>;
export function BuyNFTDrop(props: BuyNFTDropProps) {
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const account = useActiveAccount();
const handleSubmit = async (form: BuyNFTDropForm) => {
@@ -10,7 +10,7 @@ import {
getContractMetadata,
setContractMetadata,
} from "thirdweb/extensions/common";
import { useReadContract, useSendAndConfirmTransaction } from "thirdweb/react";
import { useReadContract } from "thirdweb/react";
import { resolveScheme } from "thirdweb/storage";
import { z } from "zod";
import { FileInput } from "@/components/blocks/FileInput";
@@ -28,6 +28,7 @@ import {
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { CommonContractSchema } from "@/schema/schemas";
import type { ExtensionDetectedState } from "@/types/ExtensionDetectedState";
import { parseError } from "@/utils/errorParser";
@@ -68,7 +69,7 @@ export const SettingsMetadata = ({
isLoggedIn: boolean;
}) => {
const metadata = useReadContract(getContractMetadata, { contract });
const sendTransaction = useSendAndConfirmTransaction();
const sendTransaction = useSendAndConfirmTx();
const transformedQueryData = useMemo(() => {
let socialUrls: z.infer<typeof SocialUrlSchema> = {
@@ -7,11 +7,7 @@ import {
getPlatformFeeInfo,
setPlatformFeeInfo,
} from "thirdweb/extensions/common";
import {
useActiveAccount,
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import z from "zod";
import { BasisPointsInput } from "@/components/blocks/BasisPointsInput";
import { AdminOnly } from "@/components/contracts/roles/admin-only";
@@ -26,6 +22,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Skeleton } from "@/components/ui/skeleton";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { AddressOrEnsSchema, BasisPointsSchema } from "@/schema/schemas";
import type { ExtensionDetectedState } from "@/types/ExtensionDetectedState";
import { parseError } from "@/utils/errorParser";
@@ -53,7 +50,7 @@ export function SettingsPlatformFees({
isLoggedIn: boolean;
}) {
const address = useActiveAccount()?.address;
const sendAndConfirmTx = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const platformFeesQuery = useReadContract(getPlatformFeeInfo, { contract });
const platformFeeInfo = platformFeesQuery.data
? {
@@ -7,11 +7,7 @@ import {
primarySaleRecipient,
setPrimarySaleRecipient,
} from "thirdweb/extensions/common";
import {
useActiveAccount,
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { z } from "zod";
import { AdminOnly } from "@/components/contracts/roles/admin-only";
import { SolidityInput } from "@/components/solidity-inputs";
@@ -25,6 +21,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Skeleton } from "@/components/ui/skeleton";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { AddressOrEnsSchema } from "@/schema/schemas";
import type { ExtensionDetectedState } from "@/types/ExtensionDetectedState";
import { parseError } from "@/utils/errorParser";
@@ -48,7 +45,7 @@ export const SettingsPrimarySale = ({
const query = useReadContract(primarySaleRecipient, {
contract,
});
const mutation = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const transformedQueryData = {
primary_sale_recipient: query.data,
@@ -70,7 +67,7 @@ export const SettingsPrimarySale = ({
saleRecipient,
});
mutation.mutate(transaction, {
sendAndConfirmTx.mutate(transaction, {
onError: (error) => {
toast.error("Error updating primary sale address", {
description: parseError(error),
@@ -131,7 +128,7 @@ export const SettingsPrimarySale = ({
client={contract.client}
disabled={query.isPending}
isLoggedIn={isLoggedIn}
isPending={mutation.isPending}
isPending={sendAndConfirmTx.isPending}
transactionCount={undefined}
txChainID={contract.chain.id}
type="submit"
@@ -6,11 +6,8 @@ import {
getDefaultRoyaltyInfo,
setDefaultRoyaltyInfo,
} from "thirdweb/extensions/common";
import {
useActiveAccount,
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { z } from "zod";
import { BasisPointsInput } from "@/components/blocks/BasisPointsInput";
import { AdminOnly } from "@/components/contracts/roles/admin-only";
@@ -25,6 +22,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Skeleton } from "@/components/ui/skeleton";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import { AddressOrEnsSchema, BasisPointsSchema } from "@/schema/schemas";
import type { ExtensionDetectedState } from "@/types/ExtensionDetectedState";
@@ -70,7 +68,7 @@ export function SettingsRoyalties({
const royaltyInfo = query.data
? { fee_recipient: query.data[0], seller_fee_basis_points: query.data[1] }
: undefined;
const mutation = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const form = useForm<z.input<typeof CommonRoyaltySchema>>({
defaultValues: royaltyInfo,
resolver: zodResolver(CommonRoyaltySchema),
@@ -96,7 +94,7 @@ export function SettingsRoyalties({
royaltyBps: BigInt(d.seller_fee_basis_points),
royaltyRecipient: d.fee_recipient,
});
mutation.mutate(transaction, {
sendAndConfirmTx.mutate(transaction, {
onError: (error) => {
console.error(error);
onError(error);
@@ -174,7 +172,7 @@ export function SettingsRoyalties({
variant="default"
disabled={query.isPending || !form.formState.isDirty}
isLoggedIn={isLoggedIn}
isPending={mutation.isPending}
isPending={sendAndConfirmTx.isPending}
transactionCount={undefined}
txChainID={contract.chain.id}
type="submit"
@@ -4,8 +4,8 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { transferBatch } from "thirdweb/extensions/erc20";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import { TransactionButton } from "@/components/tx-button";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
import { type AirdropAddressInput, AirdropUpload } from "./airdrop-upload";
@@ -24,7 +24,7 @@ export function TokenAirdropForm(props: {
defaultValues: { addresses: [] },
});
const sendTransaction = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const addresses = form.watch("addresses");
// The real number should be slightly higher since there's a lil bit of overhead cost
const estimateGasCost =
@@ -41,7 +41,7 @@ export function TokenAirdropForm(props: {
contract,
});
const sendTxPromise = sendTransaction.mutateAsync(tx);
const sendTxPromise = sendAndConfirmTx.mutateAsync(tx);
toast.promise(sendTxPromise, {
success: "Tokens airdropped successfully",
error: (err) => ({
@@ -85,7 +85,7 @@ export function TokenAirdropForm(props: {
<TransactionButton
client={contract.client}
isLoggedIn={isLoggedIn}
isPending={sendTransaction.isPending}
isPending={sendAndConfirmTx.isPending}
transactionCount={1}
txChainID={contract.chain.id}
type="submit"
@@ -6,7 +6,6 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { type ThirdwebContract, toUnits } from "thirdweb";
import * as ERC20Ext from "thirdweb/extensions/erc20";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import { z } from "zod";
import { TransactionButton } from "@/components/tx-button";
import { Button } from "@/components/ui/button";
@@ -27,6 +26,7 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
const burnSchema = z.object({
@@ -45,7 +45,7 @@ export function TokenBurnButton(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
}) {
const sendConfirmation = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const form = useForm<z.infer<typeof burnSchema>>({
defaultValues: { amount: "0" },
resolver: zodResolver(burnSchema),
@@ -65,7 +65,7 @@ export function TokenBurnButton(props: {
contract: props.contract,
});
const txPromise = sendConfirmation.mutateAsync(burnTokensTx);
const txPromise = sendAndConfirmTx.mutateAsync(burnTokensTx);
toast.promise(txPromise, {
error: (error) => ({
@@ -123,7 +123,7 @@ export function TokenBurnButton(props: {
client={props.contract.client}
disabled={!form.formState.isDirty}
isLoggedIn={props.isLoggedIn}
isPending={sendConfirmation.isPending}
isPending={sendAndConfirmTx.isPending}
transactionCount={1}
txChainID={props.contract.chain.id}
type="submit"
@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { isAddress, type ThirdwebContract } from "thirdweb";
import * as ERC20Ext from "thirdweb/extensions/erc20";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { z } from "zod";
import { TransactionButton } from "@/components/tx-button";
import { Button } from "@/components/ui/button";
@@ -28,6 +28,7 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
const claimFormSchema = z.object({
@@ -54,7 +55,7 @@ export function TokenClaimButton(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
}) {
const sendAndConfirmTransaction = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const account = useActiveAccount();
const form = useForm<ClaimFormValues>({
@@ -80,7 +81,7 @@ export function TokenClaimButton(props: {
});
if (approveTx) {
const approveTxPromise = sendAndConfirmTransaction.mutateAsync(approveTx);
const approveTxPromise = sendAndConfirmTx.mutateAsync(approveTx);
toast.promise(approveTxPromise, {
error: (error) => ({
loading: "Approve Spending for claiming tokens",
@@ -93,7 +94,7 @@ export function TokenClaimButton(props: {
await approveTxPromise;
}
const claimTxPromise = sendAndConfirmTransaction.mutateAsync(transaction);
const claimTxPromise = sendAndConfirmTx.mutateAsync(transaction);
toast.promise(claimTxPromise, {
error: (error) => ({
@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import * as ERC20Ext from "thirdweb/extensions/erc20";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { z } from "zod";
import { MinterOnly } from "@/components/contracts/roles/minter-only";
import { TransactionButton } from "@/components/tx-button";
@@ -28,6 +28,7 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
const mintFormSchema = z.object({
@@ -47,7 +48,7 @@ export function TokenMintButton(props: {
isLoggedIn: boolean;
}) {
const address = useActiveAccount()?.address;
const sendAndConfirmTransaction = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
const form = useForm<MintFormValues>({
resolver: zodResolver(mintFormSchema),
defaultValues: { amount: "" },
@@ -66,7 +67,7 @@ export function TokenMintButton(props: {
to: address,
});
const mintTxPromise = sendAndConfirmTransaction.mutateAsync(mintTx);
const mintTxPromise = sendAndConfirmTx.mutateAsync(mintTx);
toast.promise(mintTxPromise, {
error: (err) => ({
message: "Failed to mint tokens",
@@ -118,7 +119,7 @@ export function TokenMintButton(props: {
client={props.contract.client}
disabled={!form.formState.isDirty || !form.formState.isValid}
isLoggedIn={props.isLoggedIn}
isPending={sendAndConfirmTransaction.isPending}
isPending={sendAndConfirmTx.isPending}
transactionCount={1}
txChainID={props.contract.chain.id}
type="submit"
@@ -6,7 +6,6 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { isAddress, type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
import * as ERC20Ext from "thirdweb/extensions/erc20";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import { z } from "zod";
import { SolidityInput } from "@/components/solidity-inputs";
import { TransactionButton } from "@/components/tx-button";
@@ -28,6 +27,7 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { parseError } from "@/utils/errorParser";
const transferFormSchema = z.object({
@@ -62,7 +62,7 @@ export function TokenTransferButton(props: {
defaultValues: { amount: "0", to: "" },
resolver: zodResolver(transferFormSchema),
});
const sendAndConfirmTransaction = useSendAndConfirmTransaction();
const sendAndConfirmTx = useSendAndConfirmTx();
async function onSubmit(values: TransferFormValues) {
const transferTx = ERC20Ext.transfer({
@@ -71,7 +71,7 @@ export function TokenTransferButton(props: {
to: values.to,
});
const transferTxPromise = sendAndConfirmTransaction.mutateAsync(transferTx);
const transferTxPromise = sendAndConfirmTx.mutateAsync(transferTx);
toast.promise(transferTxPromise, {
error: (error) => ({
@@ -149,7 +149,7 @@ export function TokenTransferButton(props: {
<TransactionButton
client={props.contract.client}
isLoggedIn={props.isLoggedIn}
isPending={sendAndConfirmTransaction.isPending}
isPending={sendAndConfirmTx.isPending}
type="submit"
transactionCount={1}
txChainID={props.contract.chain.id}
@@ -62,8 +62,8 @@ export const products = [
description: "The most powerful AI for interacting with the blockchain",
icon: NebulaIcon,
id: "nebula",
link: "https://thirdweb.com/nebula",
name: "Nebula",
link: "https://thirdweb.com/ai",
name: "thirdweb AI",
},
] satisfies Array<{
name: string;
@@ -9,7 +9,7 @@ import {
getContractPublisher,
publishContract,
} from "thirdweb/extensions/thirdweb";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { reportContractPublished } from "@/analytics/report";
import { CustomConnectWallet } from "@/components/connect-wallet";
import { Button } from "@/components/ui/button";
@@ -19,6 +19,7 @@ import {
DASHBOARD_FORWARDER_ADDRESS,
} from "@/constants/misc";
import { useEns, useFunctionParamsFromABI } from "@/hooks/contract-hooks";
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { ContractParamsFieldset } from "./contract-params-fieldset";
@@ -44,7 +45,7 @@ export function ContractPublishForm(props: {
"Failed to publish contract",
);
const account = useActiveAccount();
const sendTx = useSendAndConfirmTransaction({
const sendAndConfirmTx = useSendAndConfirmTx({
gasless: {
experimentalChainlessSupport: true,
provider: "engine",
@@ -179,7 +180,7 @@ export function ContractPublishForm(props: {
form.watch("deployType") === "autoFactory" ? constructorParams : [];
// during loading and after success we should stay in loading state
const isPending = sendTx.isPending || sendTx.isSuccess;
const isPending = sendAndConfirmTx.isPending || sendAndConfirmTx.isSuccess;
const formId = useId();
@@ -252,7 +253,7 @@ export function ContractPublishForm(props: {
previousMetadata: props.publishMetadata,
});
sendTx.mutate(tx, {
sendAndConfirmTx.mutate(tx, {
onError: (err) => {
console.error("Failed to publish contract", err);
onError(err);
@@ -390,9 +391,9 @@ export function ContractPublishForm(props: {
type="submit"
>
{isPending && <Spinner className="size-4" />}
{sendTx.isSuccess
{sendAndConfirmTx.isSuccess
? "Preparing page"
: sendTx.isPending
: sendAndConfirmTx.isPending
? "Publishing contract"
: "Publish Contract"}
</Button>
@@ -4,7 +4,6 @@ import { getInAppWalletUsage } from "@/api/analytics";
import { getAuthToken } from "@/api/auth-token";
import { getProjects, type Project } from "@/api/project/projects";
import { getTeamBySlug } from "@/api/team/get-team";
import { DismissibleAlert } from "@/components/blocks/dismissible-alert";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { loginRedirect } from "@/utils/redirects";
import { Changelog } from "./_components/Changelog";
@@ -63,15 +62,8 @@ export default async function Page(props: {
team={team}
/>
{team.billingPlan === "free" ? (
{team.billingPlan === "free" && (
<FreePlanUpsellBannerUI highlightPlan="growth" teamSlug={team.slug} />
) : (
<DismissibleAlert
preserveState={true}
description="Engines, contracts, project settings, and more are now managed within projects. Open or create a project to access them."
localStorageId={`${team.id}-engines-alert`}
title="Looking for Engines?"
/>
)}
<Changelog />
@@ -1,7 +1,8 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns";
import { ChevronDownIcon, UserIcon } from "lucide-react";
import { ChevronDownIcon, StarIcon, UserIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
@@ -20,6 +21,7 @@ import { Spinner } from "@/components/ui/Spinner";
import { AutoResizeTextarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { ThirdwebMiniLogo } from "../../../../../../components/ThirdwebMiniLogo";
import { checkFeedbackStatus, submitSupportFeedback } from "../apis/feedback";
import { sendMessageToTicket } from "../apis/support";
import type { SupportMessage, SupportTicket } from "../types/tickets";
import {
@@ -37,6 +39,77 @@ export function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
const [isSubmittingReply, setIsSubmittingReply] = useState(false);
const [localMessages, setLocalMessages] = useState(ticket.messages || []);
// rating/feedback
const [rating, setRating] = useState(0);
const [feedback, setFeedback] = useState("");
// Check if feedback has already been submitted for this ticket
const feedbackStatusQuery = useQuery({
queryKey: ["feedbackStatus", ticket.id],
queryFn: async () => {
const result = await checkFeedbackStatus(ticket.id);
if ("error" in result) {
throw new Error(result.error);
}
return result.hasFeedback;
},
enabled: ticket.status === "closed",
staleTime: 60_000,
gcTime: 5 * 60_000,
});
const feedbackSubmitted = feedbackStatusQuery.data ?? false;
const isLoading = feedbackStatusQuery.isLoading;
const hasError = feedbackStatusQuery.isError;
const handleStarClick = (starIndex: number) => {
setRating(starIndex + 1);
};
const queryClient = useQueryClient();
const submitFeedbackMutation = useMutation({
mutationFn: async () => {
if (rating === 0) {
throw new Error("Please select a rating");
}
const result = await submitSupportFeedback({
rating,
feedback,
ticketId: ticket.id,
});
if ("error" in result) {
throw new Error(result.error);
}
return result;
},
onSuccess: () => {
toast.success("Thank you for your feedback!");
setRating(0);
setFeedback("");
// mark as submitted immediately
queryClient.setQueryData(["feedbackStatus", ticket.id], true);
},
onError: (err) => {
console.error("Failed to submit feedback:", err);
const msg = err instanceof Error ? err.message : String(err ?? "");
let message = "Failed to submit feedback. Please try again.";
if (/network|fetch/i.test(msg)) {
message = "Network error. Please check your connection and try again.";
} else if (
/validation|Rating must be|Please select a rating/i.test(msg)
) {
message = msg; // show precise user-facing validation error
} else if (/API Server error/i.test(msg)) {
message = "Server error. Please try again later.";
}
toast.error(message);
},
});
const handleSendFeedback = () => {
submitFeedbackMutation.mutate();
};
const handleSendReply = async () => {
if (!team.unthreadCustomerId) {
toast.error("No unthread customer id found for this team");
@@ -149,11 +222,88 @@ export function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
)}
</div>
{ticket.status === "closed" && (
{ticket.status === "closed" && isLoading && (
<div className="border-t p-6">
<div className="flex items-center gap-2">
<Spinner className="size-4" />
<span className="text-muted-foreground text-sm">
Checking feedback status...
</span>
</div>
</div>
)}
{ticket.status === "closed" && !isLoading && !feedbackSubmitted && (
<div className="border-t p-6">
<p className="text-muted-foreground text-sm">
This ticket is closed. If you need further assistance, please
create a new ticket.
This ticket is closed. Give us a quick rating to let us know how
we did!
</p>
{hasError && (
<p className="text-destructive text-xs mt-2">
Couldn't verify prior feedback right now — you can still submit
a rating.
</p>
)}
<div className="flex gap-2 mb-6 mt-4">
{[1, 2, 3, 4, 5].map((starValue) => (
<button
key={`star-${starValue}`}
type="button"
onClick={() => handleStarClick(starValue - 1)}
className="transition-colors"
aria-label={`Rate ${starValue} out of 5 stars`}
>
<StarIcon
size={32}
className={cn(
"transition-colors",
starValue <= rating
? "text-pink-500 fill-current stroke-current"
: "text-muted-foreground fill-current stroke-current",
"hover:text-pink-500",
)}
strokeWidth={starValue <= rating ? 2 : 1}
/>
</button>
))}
</div>
<div className="relative">
<AutoResizeTextarea
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder="Optional: Tell us how we can improve."
maxLength={1000}
className="text-sm w-full bg-card text-foreground rounded-lg p-4 pr-28 min-h-[100px] resize-none border border-border focus:outline-none placeholder:text-muted-foreground"
/>
<Button
type="button"
onClick={handleSendFeedback}
disabled={submitFeedbackMutation.isPending || rating === 0}
className="absolute bottom-3 right-3 rounded-full h-auto py-2 px-4"
variant="secondary"
size="sm"
>
{submitFeedbackMutation.isPending ? (
<>
<Spinner className="size-4 mr-2" />
Sending...
</>
) : (
"Send Feedback"
)}
</Button>
</div>
</div>
)}
{ticket.status === "closed" && feedbackSubmitted && (
<div className="border-t p-6">
<p className="text-muted-foreground text-sm">
Thank you for your feedback! We appreciate your input and will use
it to improve our service.
</p>
</div>
)}
@@ -0,0 +1,123 @@
"use server";
type FeedbackSubmitResult = { success: true } | { error: string };
type FeedbackStatusResult = { hasFeedback: boolean } | { error: string };
export async function submitSupportFeedback({
rating,
feedback,
ticketId,
}: {
rating: number;
feedback?: string;
ticketId: string;
}): Promise<FeedbackSubmitResult> {
try {
// Fail fast on missing configuration
const siwaUrl =
process.env.SIWA_URL ?? process.env.NEXT_PUBLIC_SIWA_URL ?? "";
if (!siwaUrl) {
throw new Error("SIWA URL not configured");
}
const apiKey = process.env.SERVICE_AUTH_KEY_SIWA;
if (!apiKey) {
throw new Error("SERVICE_AUTH_KEY_SIWA not configured");
}
// Basic input validation/normalization
if (!ticketId?.trim()) {
return { error: "ticketId is required." };
}
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
return { error: "Rating must be an integer between 1 and 5." };
}
const normalizedFeedback = (feedback ?? "")
.toString()
.trim()
.slice(0, 1000); // hard cap length
const payload = {
rating: rating.toString(),
feedback: normalizedFeedback,
ticket_id: ticketId,
};
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), 10_000);
const response = await fetch(`${siwaUrl}/v1/csat/saveCSATFeedback`, {
method: "POST",
cache: "no-store",
headers: {
"Content-Type": "application/json",
"x-service-api-key": apiKey,
},
body: JSON.stringify(payload),
signal: ac.signal,
}).finally(() => clearTimeout(t));
if (!response.ok) {
const errorText = await response.text();
return { error: `API Server error: ${response.status} - ${errorText}` };
}
return { success: true };
} catch (error) {
console.error("Feedback submission error:", error);
return { error: "Internal server error" };
}
}
export async function checkFeedbackStatus(
ticketId: string,
): Promise<FeedbackStatusResult> {
try {
// Basic input validation
if (!ticketId?.trim()) {
return { error: "ticketId is required." };
}
// Fail fast on missing configuration
const siwaUrl =
process.env.SIWA_URL ?? process.env.NEXT_PUBLIC_SIWA_URL ?? "";
if (!siwaUrl) {
throw new Error("SIWA URL not configured");
}
const apiKey = process.env.SERVICE_AUTH_KEY_SIWA;
if (!apiKey) {
throw new Error("SERVICE_AUTH_KEY_SIWA not configured");
}
const fullUrl = `${siwaUrl}/v1/csat/getCSATFeedback?ticket_id=${encodeURIComponent(ticketId)}`;
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), 10_000);
const response = await fetch(fullUrl, {
method: "GET",
cache: "no-store",
headers: {
"Content-Type": "application/json",
"x-service-api-key": apiKey,
},
signal: ac.signal,
}).finally(() => clearTimeout(t));
if (!response.ok) {
const errorText = await response.text();
return { error: `API Server error: ${response.status} - ${errorText}` };
}
const data = await response.json();
return { hasFeedback: data.has_feedback };
} catch (error) {
console.error("Feedback status check error:", error);
return { error: "Internal server error" };
}
}
@@ -0,0 +1,102 @@
import { redirect } from "next/navigation";
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import type { DurationId } from "@/components/analytics/date-range-selector";
import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-filters";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { getFiltersFromSearchParams } from "@/lib/time";
import { loginRedirect } from "@/utils/redirects";
import { AiAnalytics } from "./chart";
import { AiSummary } from "./chart/Summary";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
searchParams: Promise<{
from?: string;
to?: string;
type?: string;
interval?: string;
}>;
}) {
const [searchParams, params] = await Promise.all([
props.searchParams,
props.params,
]);
const { team_slug, project_slug } = params;
const [project, authToken] = await Promise.all([
getProject(team_slug, project_slug),
getAuthToken(),
]);
if (!authToken) {
loginRedirect(`/team/${team_slug}/${project_slug}/ai`);
}
if (!project) {
redirect(`/team/${team_slug}`);
}
const client = getClientThirdwebClient({
jwt: authToken,
teamId: project.teamId,
});
const defaultRange = "last-30" as DurationId;
const { range, interval } = getFiltersFromSearchParams({
defaultRange,
from: searchParams.from,
interval: searchParams.interval,
to: searchParams.to,
});
return (
<ResponsiveSearchParamsProvider value={searchParams}>
<ProjectPage
header={{
icon: NebulaIcon,
client,
title: "AI Analytics",
description: "Track your AI app usage and performance",
actions: null,
links: [
{
href: "https://portal.thirdweb.com/ai/chat",
type: "docs",
},
{
href: "https://api.thirdweb.com/reference#tag/ai/post/ai/chat",
type: "api",
},
{
href: "https://playground.thirdweb.com/ai/chat",
type: "playground",
},
],
}}
>
<div className="flex flex-col gap-4 md:gap-6">
<ResponsiveTimeFilters defaultRange={defaultRange} />
<AiSummary
projectId={project.id}
teamId={project.teamId}
authToken={authToken}
range={range}
/>
<AiAnalytics
interval={interval}
projectId={project.id}
range={range}
teamId={project.teamId}
authToken={authToken}
/>
</div>
</ProjectPage>
</ResponsiveSearchParamsProvider>
);
}
@@ -0,0 +1,290 @@
import { stream } from "fetch-event-stream";
import type { Project } from "@/api/project/projects";
import { NEXT_PUBLIC_THIRDWEB_AI_HOST } from "@/constants/public-envs";
import type {
NebulaContext,
NebulaSwapData,
NebulaTxData,
NebulaUserMessage,
} from "./types";
export async function promptNebula(params: {
message: NebulaUserMessage;
sessionId: string;
authToken: string;
handleStream: (res: ChatStreamedResponse) => void;
abortController: AbortController;
context: undefined | NebulaContext;
project: Project;
}) {
const body: Record<string, string | boolean | object> = {
messages: [params.message],
session_id: params.sessionId,
stream: true,
};
if (params.context) {
body.context = {
chain_ids: params.context.chainIds || [],
networks: params.context.networks,
wallet_address: params.context.walletAddress,
};
}
try {
const events = await stream(`${NEXT_PUBLIC_THIRDWEB_AI_HOST}/chat`, {
body: JSON.stringify(body),
headers: {
Authorization: `Bearer ${params.authToken}`,
"x-team-id": params.project.teamId,
"x-client-id": params.project.publishableKey,
"Content-Type": "application/json",
},
method: "POST",
signal: params.abortController.signal,
});
for await (const _event of events) {
if (!_event.data) {
continue;
}
const event = _event as ChatStreamedEvent;
switch (event.event) {
case "delta": {
params.handleStream({
data: {
v: JSON.parse(event.data).v,
},
event: "delta",
});
break;
}
case "presence": {
params.handleStream({
data: JSON.parse(event.data),
event: "presence",
});
break;
}
case "image": {
const data = JSON.parse(event.data) as {
data: {
width: number;
height: number;
url: string;
};
request_id: string;
};
params.handleStream({
data: data.data,
event: "image",
request_id: data.request_id,
});
break;
}
case "action": {
const data = JSON.parse(event.data);
if (data.type === "sign_transaction") {
try {
const parsedTxData = data.data as NebulaTxData;
params.handleStream({
data: parsedTxData,
event: "action",
request_id: data.request_id,
type: "sign_transaction",
});
} catch (e) {
console.error("failed to parse action data", e, { event });
}
}
if (data.type === "sign_swap") {
try {
const swapData = data.data as NebulaSwapData;
params.handleStream({
data: swapData,
event: "action",
request_id: data.request_id,
type: "sign_swap",
});
} catch (e) {
console.error("failed to parse action data", e, { event });
}
}
break;
}
case "error": {
const data = JSON.parse(event.data) as {
code: number;
error: {
message: string;
};
};
params.handleStream({
data: {
code: data.code,
errorMessage: data.error.message,
},
event: "error",
});
break;
}
case "init": {
const data = JSON.parse(event.data);
params.handleStream({
data: {
request_id: data.request_id,
session_id: data.session_id,
},
event: "init",
});
break;
}
case "context": {
const data = JSON.parse(event.data) as {
data: string;
request_id: string;
session_id: string;
};
const contextData = JSON.parse(data.data) as {
wallet_address: string;
chain_ids: number[];
networks: NebulaContext["networks"];
};
params.handleStream({
data: contextData,
event: "context",
});
break;
}
case "ping": {
break;
}
default: {
console.warn("unhandled event", event);
}
}
}
} catch (error) {
console.error("failed to stream events", error);
params.handleStream({
data: {
code: 500,
errorMessage: `Failed to stream events: ${error instanceof Error ? error.message : "Unknown error"}`,
},
event: "error",
});
params.abortController.abort();
}
}
type ChatStreamedResponse =
| {
event: "init";
data: {
session_id: string;
request_id: string;
};
}
| {
event: "presence";
data: {
session_id: string;
request_id: string;
source: "user" | "reviewer" | (string & {});
data: string;
};
}
| {
event: "delta";
data: {
v: string;
};
}
| {
event: "action";
type: "sign_transaction";
data: NebulaTxData;
request_id: string;
}
| {
event: "action";
type: "sign_swap";
data: NebulaSwapData;
request_id: string;
}
| {
event: "image";
data: {
width: number;
height: number;
url: string;
};
request_id: string;
}
| {
event: "context";
data: {
wallet_address: string;
chain_ids: number[];
networks: NebulaContext["networks"];
};
}
| {
event: "error";
data: {
code: number;
errorMessage: string;
};
};
type ChatStreamedEvent =
| {
event: "init";
data: string;
}
| {
event: "presence";
data: string;
}
| {
event: "delta";
data: string;
}
| {
event: "image";
data: string;
}
| {
event: "action";
type: "sign_transaction" | "sign_swap";
data: string;
}
| {
event: "context";
data: string;
}
| {
event: "error";
data: string;
}
| {
event: "ping";
data: string;
};
@@ -0,0 +1,28 @@
"use server";
import type { Project } from "@/api/project/projects";
import { NEXT_PUBLIC_THIRDWEB_AI_HOST } from "@/constants/public-envs";
import { fetchWithAuthToken } from "./fetchWithAuthToken";
export async function submitFeedback(params: {
project: Project;
sessionId: string;
requestId: string;
rating: "good" | "bad" | "neutral";
}) {
const res = await fetchWithAuthToken({
body: {
feedback_rating:
params.rating === "good" ? 1 : params.rating === "bad" ? -1 : 0,
request_id: params.requestId,
session_id: params.sessionId,
},
endpoint: `${NEXT_PUBLIC_THIRDWEB_AI_HOST}/feedback`,
method: "POST",
project: params.project,
});
if (!res.ok) {
throw new Error("Failed to submit feedback");
}
}
@@ -0,0 +1,62 @@
"use server";
import { getAuthToken } from "@/api/auth-token";
import type { Project } from "@/api/project/projects";
type FetchWithKeyOptions = {
endpoint: string;
project: Project;
timeout?: number;
} & (
| {
method: "POST" | "PUT";
body: Record<string, unknown>;
}
| {
method: "GET" | "DELETE";
}
);
export async function fetchWithAuthToken(options: FetchWithKeyOptions) {
const timeout = options.timeout || 30000;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const authToken = await getAuthToken();
if (!authToken) {
throw new Error("No auth token found");
}
const response = await fetch(options.endpoint, {
body: "body" in options ? JSON.stringify(options.body) : undefined,
headers: {
Accept: "application/json",
Authorization: `Bearer ${authToken}`,
"x-team-id": options.project.teamId,
"x-client-id": options.project.publishableKey,
"Content-Type": "application/json",
},
method: options.method,
signal: controller.signal,
});
if (!response.ok) {
if (response.status === 504) {
throw new Error("Request timed out. Please try again.");
}
const data = await response.text();
throw new Error(`HTTP error! status: ${response.status}: ${data}`);
}
return response;
} catch (error: unknown) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error("Request timed out. Please try again.");
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
@@ -0,0 +1,123 @@
"use server";
import type { Project } from "@/api/project/projects";
import { NEXT_PUBLIC_THIRDWEB_AI_HOST } from "@/constants/public-envs";
import { fetchWithAuthToken } from "./fetchWithAuthToken";
import type {
DeletedSessionInfo,
NebulaContext,
SessionInfo,
TruncatedSessionInfo,
UpdatedSessionInfo,
} from "./types";
export async function createSession(params: {
project: Project;
context: NebulaContext | undefined;
}) {
const body: Record<string, string | boolean | object> = {};
if (params.context) {
body.context = {
chain_ids: params.context.chainIds || [],
networks: params.context.networks,
wallet_address: params.context.walletAddress,
};
}
const res = await fetchWithAuthToken({
project: params.project,
body: body,
endpoint: `${NEXT_PUBLIC_THIRDWEB_AI_HOST}/session`,
method: "POST",
});
if (!res.ok) {
const error = await res.text();
throw new Error(`Failed to create session: ${error}`);
}
const data = await res.json();
return data.result as SessionInfo;
}
export async function updateSession(params: {
project: Project;
sessionId: string;
contextFilters: NebulaContext | undefined;
}) {
const body: Record<string, string | boolean | object> = {};
if (params.contextFilters) {
body.context = {
chain_ids: params.contextFilters.chainIds || [],
networks: params.contextFilters.networks,
wallet_address: params.contextFilters.walletAddress,
};
}
const res = await fetchWithAuthToken({
body: body,
endpoint: `${NEXT_PUBLIC_THIRDWEB_AI_HOST}/session/${params.sessionId}`,
method: "PUT",
project: params.project,
});
if (!res.ok) {
throw new Error("Failed to update session");
}
const data = await res.json();
return data.result as UpdatedSessionInfo;
}
export async function deleteSession(params: {
project: Project;
sessionId: string;
}) {
const res = await fetchWithAuthToken({
endpoint: `${NEXT_PUBLIC_THIRDWEB_AI_HOST}/session/${params.sessionId}`,
method: "DELETE",
project: params.project,
});
if (!res.ok) {
throw new Error("Failed to update session");
}
const data = await res.json();
return data.result as DeletedSessionInfo;
}
export async function getSessions(params: { project: Project }) {
const res = await fetchWithAuthToken({
endpoint: `${NEXT_PUBLIC_THIRDWEB_AI_HOST}/session/list`,
method: "GET",
project: params.project,
});
if (!res.ok) {
throw new Error("Failed to update session");
}
const data = await res.json();
return data.result as TruncatedSessionInfo[];
}
export async function getSessionById(params: {
project: Project;
sessionId: string;
}) {
const res = await fetchWithAuthToken({
endpoint: `${NEXT_PUBLIC_THIRDWEB_AI_HOST}/session/${params.sessionId}`,
method: "GET",
project: params.project,
});
if (!res.ok) {
throw new Error("Failed to update session");
}
const data = await res.json();
return data.result as SessionInfo;
}
@@ -0,0 +1,121 @@
type SessionContextFilter = {
chain_ids: string[] | null;
wallet_address: string | null;
};
type NebulaUserMessageContentItem =
| {
type: "image";
image_url: string | null;
b64: string | null;
}
| {
type: "text";
text: string;
}
| {
type: "transaction";
transaction_hash: string;
chain_id: number;
};
export type NebulaUserMessageContent = NebulaUserMessageContentItem[];
export type NebulaUserMessage = {
role: "user";
content: NebulaUserMessageContent;
};
export type NebulaSessionHistoryMessage =
| {
role: "assistant" | "action" | "image";
content: string;
timestamp: number;
}
| {
role: "user";
content: NebulaUserMessageContent | string;
};
export type SessionInfo = {
id: string;
account_id: string;
model_name: string;
archive_at: string | null;
can_execute: boolean;
created_at: string;
deleted_at: string | null;
history: Array<NebulaSessionHistoryMessage> | null;
updated_at: string;
archived_at: string | null;
title: string | null;
is_public: boolean | null;
context: SessionContextFilter | null;
// memory
// action: array<object> | null; <-- type of this is not available on https://nebula-api.thirdweb-dev.com/docs#/default/get_session_session__session_id__get
};
export type UpdatedSessionInfo = {
title: string;
model_name: string;
account_id: string;
context: SessionContextFilter | null;
};
export type DeletedSessionInfo = {
id: string;
deleted_at: string;
};
export type TruncatedSessionInfo = {
created_at: string;
id: string;
updated_at: string;
title: string | null;
};
export type NebulaTxData = {
chain_id: number;
data: `0x${string}`;
to: string;
value?: string;
};
export type NebulaContext = {
chainIds: string[] | null;
walletAddress: string | null;
networks: "mainnet" | "testnet" | "all" | null;
};
export type NebulaSwapData = {
action: string;
transaction: {
chain_id: number;
to: `0x${string}`;
data: `0x${string}`;
value?: string;
};
to_token: {
address: `0x${string}`;
amount: string;
chain_id: number;
decimals: number;
symbol: string;
};
from_token: {
address: `0x${string}`;
amount: string;
chain_id: number;
decimals: number;
symbol: string;
};
intent: {
amount: string;
destination_chain_id: number;
destination_token_address: `0x${string}`;
origin_chain_id: number;
origin_token_address: `0x${string}`;
receiver: `0x${string}`;
sender: `0x${string}`;
};
};
@@ -0,0 +1,65 @@
import { notFound } from "next/navigation";
import { getAuthToken, getUserThirdwebClient } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { getSessionById, getSessions } from "../../api/session";
import { ChatPageContent } from "../../components/ChatPageContent";
import { ChatPageLayout } from "../../components/ChatPageLayout";
export default async function Page(props: {
params: Promise<{
team_slug: string;
project_slug: string;
session_id: string;
}>;
}) {
const [params] = await Promise.all([props.params]);
const [authToken, project] = await Promise.all([
getAuthToken(),
getProject(params.team_slug, params.project_slug),
]);
if (!authToken) {
notFound();
}
if (!project) {
notFound();
}
const client = await getUserThirdwebClient({ teamId: project.teamId });
const [session, sessions] = await Promise.all([
getSessionById({
project: project,
sessionId: params.session_id,
}).catch(() => undefined),
getSessions({
project: project,
}).catch(() => []),
]);
if (!session) {
notFound();
}
return (
<ChatPageLayout
team_slug={params.team_slug}
project={project}
authToken={authToken}
client={client}
accountAddress={""}
sessions={sessions}
>
<ChatPageContent
project={project}
accountAddress={""}
authToken={authToken}
client={client}
initialParams={undefined}
session={session}
type="new-chat"
/>
</ChatPageLayout>
);
}
@@ -0,0 +1,92 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { subDays } from "date-fns";
import { ThirdwebProvider } from "thirdweb/react";
import { projectStub, randomLorem } from "@/storybook/stubs";
import { storybookThirdwebClient } from "@/storybook/utils";
import { ChatPageLayout } from "../../components/ChatPageLayout";
import { ChatHistoryPageUI } from "./ChatHistoryPage";
const meta = {
component: Variant,
decorators: [
(Story) => (
<ThirdwebProvider>
<Story />
</ThirdwebProvider>
),
],
parameters: {
nextjs: {
appDirectory: true,
},
},
title: "AI/history",
} satisfies Meta<typeof Variant>;
export default meta;
type Story = StoryObj<typeof meta>;
export const TenChats: Story = {
args: {
length: 10,
},
};
export const ZeroChats: Story = {
args: {
length: 0,
},
};
export const OneChat: Story = {
args: {
length: 1,
},
};
export const PrefillSearch: Story = {
args: {
length: 10,
prefillSearch: "xxxxxxxxxxxx",
},
};
function createRandomSessions(length: number) {
const sessions = [];
for (let i = 0; i < length; i++) {
sessions.push({
created_at: new Date().toISOString(),
id: Math.random().toString(),
title: randomLorem(Math.floor(5 + Math.random() * 15)),
updated_at: subDays(
new Date(),
Math.floor(Math.random() * 10),
).toISOString(),
});
}
return sessions;
}
function Variant(props: { length: number; prefillSearch?: string }) {
return (
<ChatPageLayout
accountAddress="0x1234567890"
authToken="xxxxxxxx"
sessions={createRandomSessions(props.length)}
project={projectStub("xxxxx", "team-1")}
client={storybookThirdwebClient}
team_slug="team-1"
>
<ChatHistoryPageUI
deleteSession={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
prefillSearch={props.prefillSearch}
sessions={createRandomSessions(props.length)}
team_slug="team-1"
project={projectStub("xxxxx", "team-1")}
/>
</ChatPageLayout>
);
}
@@ -0,0 +1,243 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { ScrollShadow } from "@workspace/ui/components/scroll-shadow";
import { Spinner } from "@workspace/ui/components/spinner";
import { formatDistance } from "date-fns";
import Fuse from "fuse.js";
import {
EllipsisIcon,
MessageCircleIcon,
SearchIcon,
TrashIcon,
} from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import type { Project } from "@/api/project/projects";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { deleteSession } from "../../api/session";
import type { TruncatedSessionInfo } from "../../api/types";
import { useSessionsWithLocalOverrides } from "../../hooks/useSessionsWithLocalOverrides";
import { deletedSessionsStore } from "../../stores";
export function ChatHistoryPage(props: {
sessions: TruncatedSessionInfo[];
prefillSearch?: string;
team_slug: string;
project: Project;
}) {
return (
<ChatHistoryPageUI
{...props}
deleteSession={async (s) => {
await deleteSession({
project: props.project,
sessionId: s,
});
}}
/>
);
}
export function ChatHistoryPageUI(props: {
sessions: TruncatedSessionInfo[];
prefillSearch?: string;
deleteSession: (sessionId: string) => Promise<void>;
team_slug: string;
project: Project;
}) {
const [searchVal, setSearchVal] = useState(props.prefillSearch ?? "");
const allSessions = useSessionsWithLocalOverrides(props.sessions);
const fuse = useMemo(() => {
return new Fuse(allSessions, {
keys: [
{
name: "title",
weight: 1,
},
],
threshold: 0.5,
});
}, [allSessions]);
const filteredSessions = useMemo(() => {
if (!searchVal) {
return allSessions;
}
return fuse.search(searchVal).map((e) => e.item);
}, [allSessions, searchVal, fuse]);
return (
<div className="flex grow flex-col overflow-hidden">
<header className="border-b py-4">
<h1 className="px-6 font-semibold text-lg tracking-tight">All Chats</h1>
</header>
<SearchInput
onValueChange={setSearchVal}
placeholder="Search for a chat"
value={searchVal}
/>
{filteredSessions.length > 0 && (
<ScrollShadow
className="flex-1"
scrollableClassName="max-h-full py-6"
shadowClassName="z-10"
shadowColor="hsl(var(--background))"
>
{filteredSessions.length > 0 && (
<div className="container flex max-w-[800px] flex-col gap-5">
{filteredSessions.map((session) => (
<SessionCard
project={props.project}
team_slug={props.team_slug}
deleteSession={props.deleteSession}
key={session.id + session.updated_at + session.created_at}
session={session}
/>
))}
</div>
)}
</ScrollShadow>
)}
{/* No Search Results */}
{allSessions.length > 0 && filteredSessions.length === 0 && (
<div className="flex grow flex-col justify-center rounded-lg">
<div className="flex flex-col items-center">
<div className="rounded-xl border bg-card p-2">
<MessageCircleIcon className="size-5 text-muted-foreground" />
</div>
<div className="h-4" />
<h3 className="text-center"> No Chats Found </h3>
<div className="h-1" />
<p className="text-center text-muted-foreground text-sm">
Try a different search term
</p>
</div>
</div>
)}
{/* No Chats at all */}
{allSessions.length === 0 && (
<div className="flex grow flex-col justify-center">
<div className="flex flex-col items-center">
<div className="rounded-xl border bg-card p-2">
<MessageCircleIcon className="size-5 text-muted-foreground" />
</div>
<div className="h-4" />
<h3 className="text-center"> No Chats Created </h3>
<div className="h-1" />
<p className="text-center text-muted-foreground text-sm">
Start a new chat to get started.
</p>
<div className="h-5" />
<Button asChild size="sm">
<Link href={`/team/${props.team_slug}/${props.project.slug}/ai`}>
New Chat
</Link>
</Button>
</div>
</div>
)}
</div>
);
}
function SearchInput(props: {
placeholder: string;
value: string;
onValueChange: (value: string) => void;
}) {
return (
<div className="relative">
<Input
className="!border-b h-auto rounded-none border-0 px-6 py-4 pl-11 focus-visible:ring-0 focus-visible:ring-offset-0"
onChange={(e) => props.onValueChange(e.target.value)}
placeholder={props.placeholder}
value={props.value}
/>
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-5 size-4 text-muted-foreground" />
</div>
);
}
// TODO - add delete chat confirmation dialog
function SessionCard(props: {
session: TruncatedSessionInfo;
project: Project;
team_slug: string;
deleteSession: (sessionId: string) => Promise<void>;
}) {
const deleteChat = useMutation({
mutationFn: () => {
return props.deleteSession(props.session.id);
},
onSuccess: () => {
const prev = deletedSessionsStore.getValue();
deletedSessionsStore.setValue([...prev, props.session.id]);
},
});
return (
<div className="relative overflow-hidden rounded-lg border bg-card p-4">
<h3 className="line-clamp-3 break-all">
<Link
className="before:absolute before:inset-0"
href={`/team/${props.team_slug}/${props.project.slug}/ai/chat/${props.session.id}`}
prefetch={false}
>
{props.session.title || "Untitled"}
</Link>
</h3>
<div className="mt-4 flex items-center justify-between gap-6 border-t pt-3">
<p className="text-muted-foreground text-sm">
Updated{" "}
{formatDistance(new Date(props.session.updated_at), new Date(), {
addSuffix: true,
})}
</p>
<Popover>
<PopoverTrigger asChild>
<Button
className="relative z-10 h-auto w-auto p-1.5"
variant="ghost"
>
<EllipsisIcon className="size-4 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-44 p-1" side="top">
<Button
className="!text-destructive-text flex w-full justify-start gap-2.5 px-2"
onClick={() => {
const promise = deleteChat.mutateAsync();
toast.promise(promise, {
error: "Failed to delete chat",
success: "Chat deleted successfully",
});
}}
variant="ghost"
>
{deleteChat.isPending ? (
<Spinner className="size-4" />
) : (
<TrashIcon className="size-4" />
)}
Delete
</Button>
</PopoverContent>
</Popover>
</div>
</div>
);
}
@@ -0,0 +1,25 @@
import { notFound } from "next/navigation";
import { getProject } from "@/api/project/projects";
import { getSessions } from "../../api/session";
import { ChatHistoryPage } from "./ChatHistoryPage";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
}) {
const [params] = await Promise.all([props.params]);
const project = await getProject(params.team_slug, params.project_slug);
if (!project) {
notFound();
}
const sessions = await getSessions({ project }).catch(() => []);
return (
<ChatHistoryPage
sessions={sessions}
project={project}
team_slug={params.team_slug}
/>
);
}
@@ -0,0 +1,101 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { storybookThirdwebClient } from "@/storybook/utils";
import { type AssetBalance, AssetsSectionUI } from "./AssetsSection";
const meta = {
component: AssetsSectionUI,
decorators: [
(Story) => (
<div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
<Story />
</div>
),
],
title: "AI/AssetsSection",
} satisfies Meta<typeof AssetsSectionUI>;
export default meta;
type Story = StoryObj<typeof meta>;
const tokensStub: AssetBalance[] = [
{
balance: "10000000000000000000000",
chain_id: 8453,
decimals: 18,
name: "Broge",
symbol: "BROGE",
token_address: "0xe8e55a847bb446d967ef92f4580162fb8f2d3f38",
},
{
balance: "2",
chain_id: 8453,
decimals: 18,
name: "clBTC",
symbol: "clBTC",
token_address: "0x8d2757ea27aabf172da4cca4e5474c76016e3dc5",
},
{
balance: "1000000000000000000",
chain_id: 8453,
decimals: 18,
name: "BASED USA",
symbol: "USA",
token_address: "0xb56d0839998fd79efcd15c27cf966250aa58d6d3",
},
{
balance: "48888800000000000000",
chain_id: 8453,
decimals: 18,
name: "BearPaw",
symbol: "PAW",
token_address: "0x600c9b69a65fb6d2551623a53ddef17b050233cd",
},
{
balance: "69000000000000000000",
chain_id: 8453,
decimals: 18,
name: "Degen Point Of View",
symbol: "POV",
token_address: "0x4c96a67b0577358894407af7bc3158fc1dffbeb5",
},
{
balance: "6237535850425",
chain_id: 8453,
decimals: 18,
name: "Wrapped Ether",
symbol: "WETH",
token_address: "0x4200000000000000000000000000000000000006",
},
];
export const MultipleAssets: Story = {
args: {
client: storybookThirdwebClient,
data: tokensStub,
isPending: false,
},
};
export const SingleAsset: Story = {
args: {
client: storybookThirdwebClient,
data: tokensStub.slice(0, 1),
isPending: false,
},
};
export const EmptyAssets: Story = {
args: {
client: storybookThirdwebClient,
data: [],
isPending: false,
},
};
export const Loading: Story = {
args: {
client: storybookThirdwebClient,
data: [],
isPending: true,
},
};
@@ -0,0 +1,254 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { XIcon } from "lucide-react";
import Link from "next/link";
import {
defineChain,
getAddress,
NATIVE_TOKEN_ADDRESS,
type ThirdwebClient,
toTokens,
} from "thirdweb";
import {
Blobbie,
TokenIcon,
TokenProvider,
useActiveAccount,
useActiveWalletChain,
} from "thirdweb/react";
import { getWalletBalance } from "thirdweb/wallets";
import { Skeleton } from "@/components/ui/skeleton";
import { isProd } from "@/constants/env-utils";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { ChainIconClient } from "@/icons/ChainIcon";
export type AssetBalance = {
chain_id: number;
token_address: string;
balance: string;
name: string;
symbol: string;
decimals: number;
};
export function AssetsSectionUI(props: {
data: AssetBalance[];
isPending: boolean;
client: ThirdwebClient;
}) {
if (props.data.length === 0 && !props.isPending) {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 px-2 py-1">
<div className="rounded-full border p-1">
<XIcon className="size-4" />
</div>
<div className="text-muted-foreground text-sm">No Assets </div>
</div>
);
}
return (
<div className="flex flex-col gap-1">
{!props.isPending &&
props.data.map((asset) => (
<AssetItem
asset={asset}
client={props.client}
key={`${asset.chain_id}-${asset.token_address}`}
/>
))}
{props.isPending &&
new Array(10).fill(null).map((_, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: for the placeholder this is explicitly the key
<SkeletonAssetItem key={index} />
))}
</div>
);
}
function SkeletonAssetItem() {
return (
<div className="flex h-[48px] items-center gap-2 px-2 py-1">
<Skeleton className="size-8 rounded-full" />
<div className="flex flex-col gap-1">
<Skeleton className="h-3 w-32 bg-muted" />
<Skeleton className="h-3 w-24 bg-muted" />
</div>
</div>
);
}
function AssetItem(props: { asset: AssetBalance; client: ThirdwebClient }) {
const { idToChain } = useAllChainsData();
const chainMeta = idToChain.get(props.asset.chain_id);
const isNativeToken = props.asset.token_address === NATIVE_TOKEN_ADDRESS;
const chain = useV5DashboardChain(props.asset.chain_id);
return (
<TokenProvider
address={props.asset.token_address}
chain={chain}
// eslint-disable-next-line no-restricted-syntax
client={props.client}
>
<div className="relative flex h-[48px] items-center gap-2.5 rounded-lg px-2 py-1 hover:bg-accent">
<div className="relative">
<TokenIcon
className="size-8 rounded-full border"
fallbackComponent={
<Blobbie
address={props.asset.token_address}
className="size-8 rounded-full"
/>
}
loadingComponent={
<Blobbie
address={props.asset.token_address}
className="size-8 rounded-full"
/>
}
/>
{!isNativeToken && (
<div className="-right-0.5 -bottom-0.5 absolute rounded-full border bg-background p-0.5">
<ChainIconClient
className="size-3.5"
client={props.client}
src={chainMeta?.icon?.url || ""}
/>
</div>
)}
</div>
<div className="flex min-w-0 flex-col text-sm">
<Link
className="truncate font-medium before:absolute before:inset-0"
href={
isNativeToken
? `https://thirdweb.com/${props.asset.chain_id}`
: `https://thirdweb.com/${props.asset.chain_id}/${props.asset.token_address}`
}
target="_blank"
>
{props.asset.name}
</Link>
<p className="truncate text-muted-foreground text-sm">
{`${toTokens(BigInt(props.asset.balance), props.asset.decimals)} ${props.asset.symbol}`}
</p>
</div>
</div>
</TokenProvider>
);
}
export function AssetsSection(props: { client: ThirdwebClient }) {
const account = useActiveAccount();
const activeChain = useActiveWalletChain();
const assetsQuery = useQuery({
enabled: !!account && !!activeChain,
queryFn: async () => {
if (!account || !activeChain) {
return [];
}
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
const url = new URL(
`https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/erc20/${account?.address}`,
);
url.searchParams.set("limit", "50");
url.searchParams.set("metadata", "true");
url.searchParams.set("include_spam", "false");
url.searchParams.set("clientId", props.client.clientId);
for (const chain of chains) {
url.searchParams.append("chain", chain.toString());
}
const response = await fetch(url.toString());
const json = (await response.json()) as {
data: AssetBalance[];
};
const tokensToShowOnTop = new Set(
[
// base
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", // usdc,
"0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", // wbtc
"0x4200000000000000000000000000000000000006", // wrapped eth
// ethereum
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // usdc
"0xdac17f958d2ee523a2206206994597c13d831ec7", // usdt
"0xB8c77482e45F1F44dE1745F52C74426C631bDD52", // bnb
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // weth
"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", // wbtc
// optimism
"0x4200000000000000000000000000000000000042", // op token
"0xdc6ff44d5d932cbd77b52e5612ba0529dc6226f1", // world coin
"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58", // usdt
"0x0b2c639c533813f4aa9d7837caf62653d097ff85", // usdc
"0x4200000000000000000000000000000000000006", // wrapped eth
// polygon
"0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", // weth
"0xc2132d05d31c914a87c6611c10748aeb04b58e8f", // usdt
"0x3BA4c387f786bFEE076A58914F5Bd38d668B42c3", // bnb
"0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // usdc
"0x2791bca1f2de4661ed88a30c99a7a9449aa84174", // usdc.e
].map((x) => getAddress(x)),
);
return json.data.sort((a, b) => {
if (tokensToShowOnTop.has(getAddress(a.token_address))) {
return -1;
}
if (tokensToShowOnTop.has(getAddress(b.token_address))) {
return 1;
}
return 0;
});
},
queryKey: ["v1/tokens/erc20", account?.address, activeChain?.id],
});
const nativeBalances = useQuery({
queryFn: async () => {
if (!account || !activeChain) {
return [];
}
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
const result = await Promise.allSettled(
chains.map((chain) =>
getWalletBalance({
address: account.address,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(chain),
client: props.client,
}),
),
);
return result
.filter((r) => r.status === "fulfilled")
.map((r) => ({
balance: r.value.value.toString(),
chain_id: r.value.chainId,
decimals: r.value.decimals,
name: r.value.name,
symbol: r.value.symbol,
token_address: r.value.tokenAddress,
}))
.filter((x) => x.balance !== "0") as AssetBalance[];
},
queryKey: ["getWalletBalance", account?.address, activeChain?.id],
});
const isPending = assetsQuery.isPending || nativeBalances.isPending;
const data = [...(nativeBalances.data ?? []), ...(assetsQuery.data ?? [])];
return (
<AssetsSectionUI client={props.client} data={data} isPending={isPending} />
);
}

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