Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f3c55d7139 Version Packages (#8240)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-11 19:31:58 +07:00
Prithvish BaidyaandGitHub 9545b43675 Engine SDK updates (#8238) 2025-10-11 19:24:30 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
869bbcfa68 Version Packages (#8237)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-11 07:42:22 +07:00
Prithvish BaidyaandGitHub 35c7d21e36 [Vault SDK] Add Solana operations support (#8168) 2025-10-11 07:37:27 +07:00
MananTank edf3aad8c4 [BLD-396] Dashboard: Fix changing tabs in /nfts/<id> page shows full page loading indicator (#8224)
<!--

## 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 `TokenIdPage` component by introducing `Suspense` for improved loading state handling and adjusting the layout of the loading spinner.

### Detailed summary
- Added `Suspense` for handling loading states.
- Updated the loading spinner layout with additional padding (`py-20`).
- Wrapped the tab rendering logic within the `Suspense` component for better user experience during data fetching.

> ✨ 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 loading experience for NFT tab content using Suspense, providing a fallback UI while content loads.
  - Improves responsiveness when switching tabs by deferring heavy content until ready.

- Style
  - Increased vertical padding for the pending NFT state to enhance visual spacing and readability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-09 22:14:06 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ab4190e23a Version Packages (#8230)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-09 14:19:19 -07:00
Joaquim VergesandGitHub 9086cdba20 [Playground] Add payTo parameter to X402 payment API (#8231) 2025-10-09 14:19:00 -07:00
Joaquim VergesandGitHub 0082e996ca [SDK] Add x402 recipientAddress to payment request and replace deprecated payTo (#8226) 2025-10-09 13:51:19 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
28955e0ac5 Version Packages (#8229)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-10 03:07:52 +07:00
FirekeeperandGitHub 05550d045f Changeset for a0c45cb25d (#8228) 2025-10-10 03:01:01 +07:00
FirekeeperandGitHub a0c45cb25d Export ERC7702 MinimalAccount helpers and events (#8227) 2025-10-10 02:57:14 +07:00
MananTank 6fc4d838ff [MNY-246] Dashboard: Add Buy button in tokens table, update supported tokens count (#8222)
<!--

## 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 token-related information and enhancing the `TokensTable` component in the dashboard, including the increase in the number of supported tokens and the addition of a buy button for each token.

### Detailed summary
- Updated token count from `3200+` to `4500+` in `page.tsx`.
- Changed description in `page.tsx` to reflect the new token count.
- Modified `TokensTable` to display the updated token count.
- Added a new button in `TokensTable` for buying tokens, including a link to the token's page.

> ✨ 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 a Buy action column with a button in the tokens table, including proper skeleton alignment for the new column.

- Style
  - Rounded token avatars for a refined look.
  - Simplified token name display by removing inline linking.

- Documentation
  - Updated copy to reflect “4500+ tokens” on the tokens page and bridge page, including Open Graph/description and UI labels.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-09 18:42:18 +00:00
MananTank fe940b3c66 [BLD-395] Dashboard: Show error UI when parsing CSV fails (#8223)
<!--

## 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 improves error handling in the `useCsvUpload` hook by adding a `try-catch` block around the CSV parsing logic. It ensures that if an error occurs during parsing or if the parsed data lacks an address, the component sets an error state and logs the error.

### Detailed summary
- Introduced a `try-catch` block around the CSV parsing logic.
- Added error handling to set `noCsv` to `true` if the parsed data's first entry does not have an `address`.
- Ensured that any errors during parsing are logged to the console.

> ✨ 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**
  * Prevents crashes during CSV uploads by handling parse errors gracefully.
  * Ensures invalid or malformed CSV files are detected and flagged without updating data.
  * Maintains existing behavior for valid files, only updating data when required fields are present.
  * Improves reliability of the upload flow with clearer invalid-file handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-09 18:31:34 +00:00
FirekeeperandGitHub 080d7f2122 Refactor ProjectFTUX API key integration section (#8225) 2025-10-10 00:08:54 +07:00
FirekeeperandGitHub ae75073a4a Refactor project wallet setup and controls UI (#8220) 2025-10-09 22:09:20 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
781a626461 Version Packages (#8217)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-09 01:27:39 -07:00
Joaquim VergesandGitHub f630912b9b [SDK] Export missing x402 types (#8218) 2025-10-09 01:14:37 -07:00
163ab8afe1 Project Wallet (Default Server Wallet) (#8212)
Co-authored-by: Joaquim Verges <[email protected]>
2025-10-08 20:57:32 +07:00
MananTank 62cfbb7441 SDK: Show error in BuyWidget and SwapWidget UI if fetching token details fails (#8215)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces error handling in the `BuyWidget` and `SwapWidget` UIs, displaying appropriate error messages when fetching token details fails.

### Detailed summary
- Added `isError` state to handle errors in token fetching.
- Updated `SelectedTokenButton` to check for errors before rendering.
- Enhanced `SwapUI` to show error messages based on different query states.
- Implemented error display in `FundWallet` for unsupported tokens.
- Modified balance display logic to accommodate new error handling.

> ✨ 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
  - Inline error banners in Buy and Swap widgets when token details fail to load.
  - Clear, specific messages for quote, buy-token, and sell-token fetch failures.
- Improvements
  - Selected token UI now falls back gracefully on errors instead of showing partial data.
  - Balance display uses richer token info (value, decimals, symbol, name) for accuracy.
  - More consistent handling of unsupported tokens across the flow.
- Bug Fixes
  - Prevents stale token icons/details from appearing during error states.
  - Disables auto-retry on token price fetches to reduce UI flicker.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-07 21:36:48 +00:00
Joaquim VergesandGitHub d7104f69f4 [Dashboard] Add engineCloud to ActiveStatus and reduce revalidation time to 30 seconds (#8216) 2025-10-07 14:22:32 -07:00
Joaquim VergesandGitHub 23545cb999 [Docs] Add ERC-7702 Smart EOA session keys documentation (#8214) 2025-10-07 13:35:46 -07:00
MananTank f9283f389b Portal: Add missing redirects for bridge pages (#8201)
<!--

## 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 redirect paths in the `redirects.mjs` file to streamline payment-related navigation by adding new routes that point to a `bridge` structure.

### Detailed summary
- Added redirect for `"/payments/sell"` to `"/bridge/sell"`
- Added redirect for `"/payments/swap"` to `"/bridge/swap"`
- Added redirect for `"/payments/tokens"` to `"/bridge/tokens"`
- Added redirect for `"/payments/routes"` to `"/bridge/routes"`

> ✨ 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 redirects so payment URLs now route to corresponding bridge destinations:
    * /payments/sell → /bridge/sell
    * /payments/swap → /bridge/swap
    * /payments/tokens → /bridge/tokens
    * /payments/routes → /bridge/routes
  * Existing bookmarks and shared links to payment pages will continue working and automatically route to the updated locations, with no UI changes required.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-07 11:30:51 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
9fbb29dc25 Version Packages (#8202)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-07 02:05:50 -07:00
Joaquim VergesandGitHub 71a1e58eaf [AI-SDK-provider] Add default in-memory session store with custom store option (#8211) 2025-10-07 02:00:38 -07:00
MananTank 68b48f4faa [MNY-232] SDK: Fix ExecutingTxScreen missing back button in TransactionWidget (#8193)
<!--

## 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 `onBack` functionality in the `TransactionWidget` and `TransactionModal` components, ensuring that the back navigation behavior is more consistent across these components.

### Detailed summary
- In `TransactionWidget.tsx`, `onBack` now sets the screen to `init-ui`.
- In `TransactionModal.tsx`, `onBack` sets the screen to `deposit`.
- In `ExecutingScreen.tsx`, the `onBack` prop is changed from optional to required.

> ✨ 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 back navigation from the executing transaction screen, allowing users to return to the previous step without closing the modal.
  * Unified back-button behavior across transaction flows for a more consistent experience.
  * Provides clearer control during long-running executions with an explicit back option.
  * Navigation enhancement applies to both bridge and transaction modal flows, improving step-to-step control while keeping transaction processing unchanged.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 22:33:31 +00:00
MananTank 1c660d4712 [MNY-202] Dashboard: Update resource links in project > bridge page (#8196)
<!--

## 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 `QuickstartSection.client.tsx` and `page.tsx` files in the bridge section of the dashboard. It modifies features, links, and documentation references to align with the new bridge functionality.

### Detailed summary
- Updated `setupTime` to `5` in `QuickstartSection.client.tsx`.
- Changed `features` from `["Swap any token", "Cross-chain swaps"]` to `["Swap any token", "Cross-chain swap"]`.
- Updated link `href` from `https://portal.thirdweb.com/payments/swap` to `https://portal.thirdweb.com/bridge/swap` and changed label to "Setup Swap".
- Added new documentation link to `page.tsx`: `https://portal.thirdweb.com/bridge`.
- Added new playground link to `page.tsx`: `https://playground.thirdweb.com/bridge/swap-widget`.
- Removed old commented-out documentation links related to payments.
- Removed several links under the footer sections for payments and replaced them with new links relevant to the bridge.

> ✨ 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 a direct link to Bridge docs in the header for quicker access.
  - Added a Playground link in the header to enable easy testing of the Bridge swap widget.
- Style
  - Removed footer sections from the Bridge page to streamline the layout and reduce clutter.
- Chores
  - Updated the “Setup Swap” feature card and its link to point to the Bridge setup URL for improved navigation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 21:22:22 +00:00
MananTank 30b5c30ba0 [PRO-27] Dashboard: copy changes in bridge page (#8191)
<!--

## 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 `description` in the `page.tsx` file for the `bridge` component, enhancing its clarity and accuracy regarding token capabilities. It also modifies the text of a `PillLink` to better reflect its functionality.

### Detailed summary
- Updated the `description` to specify "3,200+ tokens" instead of "9M+ token routes".
- Changed the `PillLink` text from "Integrate Bridge in your apps in minutes, and start generating revenue" to "Accept Payments in Any Token Right Inside Your App".

> ✨ 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 page metadata to reflect 3,200+ tokens across 85+ chains, with clearer phrasing around finality and pricing for improved SEO and share previews.

- **Style**
  - Refined call-to-action on the Bridge page to “Accept Payments in Any Token Right Inside Your App,” improving clarity and alignment with user goals.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 21:12:26 +00:00
MananTank 86461f9a07 [PRO-25] Dashboard: Update bridge page header to show supported tokens, chains and routes count (#8195)
<!--

## 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 content of the `Page` component in the `page.tsx` file by removing a `Link` element and replacing it with a `p` element that provides information about token and chain support.

### Detailed summary
- Removed the `Link` component that directed users to the thirdweb bridge.
- Added a `p` element stating "85+ chains, 3200+ tokens and 9+ million routes supported".

> ✨ 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 the Tokens dashboard header to display marketing stats: “85+ chains, 3200+ tokens, and 9+ million routes supported.”
  - Replaced the previous header link with a plain text paragraph for a cleaner presentation.
  - No changes to data, performance, or underlying functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 20:01:56 +00:00
MananTank 2ebba6139a [MNY-235] Dashboard: Fix payment page theme issues (#8199)
<!--

## 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 integrating the `ThemeProvider` from `next-themes` into multiple components within the payment module, enhancing theming support across the application.

### Detailed summary
- Removed `ThemeProvider` from `PayLayout` in `layout.tsx`.
- Added `ThemeProvider` wrapping to `PayPage` in `page.tsx`.
- Integrated `ThemeProvider` in `PayPageWidget` to manage theme settings.
- Updated theme handling logic in `PayPageWidget` to use `getSDKTheme`.
- Wrapped the main structure in `ThemeProvider` in `[id]/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
  - Consistent light/dark theme across all Pay pages, with support for forcing a theme per link and reduced visual flicker from system theme changes.
- Style
  - Refined header layout: project image, name, and description grouped; “Secured by thirdweb” badge retained.
  - Improved details on large screens: clearer “Network” label with icon and chain name; seller shown as ENS or shortened address, reordered for readability.
- Refactor
  - Consolidated theming at the page level for consistent behavior across views.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 19:51:09 +00:00
MananTank f3bacc16cb [MNY-203] Dashboard: ERC20 token page title, metadata and OG updates (#8194)
<!--

## 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 logic for generating `title` and `description` variables in the `shared-layout.tsx` file, particularly for different types of smart contracts. It enhances the information displayed for ERC20 contracts and adds fallback values for when contract metadata is not available.

### Detailed summary
- Changed `const title` to `let title` to allow reassignment.
- Updated `title` and `description` for `ERC20` contracts using `contractMetadata`.
- Added fallback values for `title` and `description` when an error occurs, including `openGraph` properties.

> ✨ 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**
  * Richer link previews for contract pages via Open Graph metadata, improving social sharing and discoverability.
  * More accurate, branded titles and descriptions: ERC20 pages use token name and symbol; non-ERC20 pages use contract name, with references to Buy, Swap, Bridge & Price.
  * Robust fallbacks ensure meaningful titles, descriptions, and previews even if metadata loading fails.
  * Consistent metadata across success and fallback states for a more reliable sharing experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 19:41:13 +00:00
MananTank 968913d55b [MNY-231] SDK: Fix TransactionWidget not updating on currency prop change after initial render (#8192)
<!--

## 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 fixing the `TransactionWidget` to ensure it updates correctly when the `currency` prop changes after the initial render. It also refines how the total cost and display values are calculated based on the selected currency.

### Detailed summary
- Updated `PaymentOverview` to remove the `currency` prop from `TransactionOverViewCompact`.
- Enhanced `TransactionPayment` to calculate `totalFiatCost` and `costToDisplay` based on the selected currency.
- Adjusted the display of USD value to use `costToDisplay`.
- Removed the `currency` type from `UseTransactionDetailsOptions`.
- Simplified `useTransactionDetails` by removing currency-related calculations.

> ✨ 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**
  * Transaction widget now updates correctly when the selected currency changes after initial render.
  * Payment and transaction details display accurate fiat totals, with graceful fallback when pricing data is unavailable.

* **Improvements**
  * Streamlined pricing display to focus on the selected currency, improving clarity and consistency across payment views.

* **Chores**
  * Added a patch-level changeset entry documenting the bug fix.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 19:22:28 +00:00
MananTank bd70db4a5d [MNY-211] Dashboard: Add chainId and is_tesnet properties in reported events (#8198)
<!--

## 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 handling of testnet-related functionality across various components in the application. It introduces the `is_testnet` and `isTestnet` props to multiple components and functions, ensuring that the application can properly differentiate between mainnet and testnet environments.

### Detailed summary
- Added `isTestnet` prop to `UniversalBridgeEmbed`.
- Introduced `is_testnet` to the `create-nft-page` and `launch-token` components.
- Updated `ChainPageView` to accept `is_testnet`.
- Included `is_testnet` in the `BuyFundsSection` and various asset page components.
- Modified `reportAssetPageview` and `reportChainPageview` to include `is_testnet`.
- Enhanced `reportContractDeployed` to capture `is_testnet` and chain ID.
- Adjusted functions in `report.ts` to utilize `is_testnet` for analytics tracking.

> ✨ 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
  - Analytics now capture testnet status across chain and asset pageviews, buy flows, contract deployments, and token/NFT creation.
  - Asset creation reports now include chain ID on success and failure for improved tracking accuracy.
- Chores
  - Standardized analytics event reporting to pass complete property objects, ensuring consistent payloads.
  - Propagated testnet indicators through relevant components to enrich analytics without altering UI or workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 19:12:07 +00:00
MananTank d9a138a90a [MNY-236] Portal: Update BridgeWidget script docs to reflect new typescript type (#8200)
<!--

## 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 making the `buy` properties optional in the configuration for the "Buy" tab UI, enhancing flexibility by allowing users to omit certain parameters. It also updates the documentation to reflect these changes.

### Detailed summary
- Changed `buy` properties (`amount`, `chainId`) from required to optional in type definitions.
- Updated documentation to remove "Required" tags for `amount` and `chainId`.
- Clarified descriptions for `amount`, `chainId`, and `tokenAddress` in the documentation.

> ✨ 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 configuration is now optional in the bridge widget; amount and chain selection can be omitted and will use defaults.
  * Removed forced preset buy values, enabling more flexible initialization.

* **Documentation**
  * Updated docs to reflect optional Buy fields and default behaviors.
  * Revised examples and type descriptions to indicate optional amount and chain settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 19:06:30 +00:00
Joaquim VergesandGitHub 777aed1e5f [Docs] Add metadata to x402 documentation pages (#8190) 2025-10-06 04:15:16 -07:00
Joaquim VergesandGitHub fbed4be959 [Docs] Update MCP tool filtering to use comma-separated list (#8189) 2025-10-06 04:07:32 -07:00
Joaquim VergesandGitHub 5a26217a43 [Docs] Add MCP tool filtering, agent examples, and available tools list (#8188) 2025-10-06 03:23:24 -07:00
111 changed files with 7952 additions and 762 deletions
@@ -0,0 +1,70 @@
"use server";
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
interface VaultWalletListItem {
id: string;
address: string;
metadata?: {
label?: string;
projectId?: string;
teamId?: string;
type?: string;
} | null;
}
export async function listProjectServerWallets(params: {
managementAccessToken: string;
projectId: string;
pageSize?: number;
}): Promise<ProjectWalletSummary[]> {
const { managementAccessToken, projectId, pageSize = 100 } = params;
if (!managementAccessToken || !NEXT_PUBLIC_THIRDWEB_VAULT_URL) {
return [];
}
try {
const vaultClient = await createVaultClient({
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
});
const response = await listEoas({
client: vaultClient,
request: {
auth: {
accessToken: managementAccessToken,
},
options: {
page: 0,
// @ts-expect-error - Vault SDK expects snake_case pagination fields
page_size: pageSize,
},
},
});
if (!response.success || !response.data?.items) {
return [];
}
const items = response.data.items as VaultWalletListItem[];
return items
.filter((item) => {
return (
item.metadata?.projectId === projectId &&
(!item.metadata?.type || item.metadata.type === "server-wallet")
);
})
.map<ProjectWalletSummary>((item) => ({
id: item.id,
address: item.address,
label: item.metadata?.label ?? undefined,
}));
} catch (error) {
console.error("Failed to list project server wallets", error);
return [];
}
}
@@ -0,0 +1,74 @@
"use server";
import { configure, sendTokens } from "@thirdweb-dev/api";
import { THIRDWEB_API_HOST } from "@/constants/urls";
configure({
override: {
baseUrl: THIRDWEB_API_HOST,
},
});
export async function sendProjectWalletTokens(options: {
walletAddress: string;
recipientAddress: string;
chainId: number;
quantityWei: string;
publishableKey: string;
teamId: string;
tokenAddress?: string;
secretKey: string;
vaultAccessToken?: string;
}) {
const {
walletAddress,
recipientAddress,
chainId,
quantityWei,
publishableKey,
teamId,
tokenAddress,
secretKey,
vaultAccessToken,
} = options;
if (!secretKey) {
return {
error: "A project secret key is required to send funds.",
ok: false,
} as const;
}
const response = await sendTokens({
body: {
chainId,
from: walletAddress,
recipients: [
{
address: recipientAddress,
quantity: quantityWei,
},
],
...(tokenAddress ? { tokenAddress } : {}),
},
headers: {
"Content-Type": "application/json",
"x-client-id": publishableKey,
"x-secret-key": secretKey,
"x-team-id": teamId,
...(vaultAccessToken ? { "x-vault-access-token": vaultAccessToken } : {}),
},
});
if (response.error || !response.data) {
return {
error: response.error || "Failed to submit transfer request.",
ok: false,
} as const;
}
return {
ok: true,
transactionIds: response.data.result?.transactionIds ?? [],
} as const;
}
+27 -45
View File
@@ -24,6 +24,7 @@ export function reportContractDeployed(properties: {
publisher: string | undefined;
contractName: string | undefined;
deploymentType?: "asset";
is_testnet: boolean | undefined;
}) {
posthog.capture("contract deployed", properties);
}
@@ -40,6 +41,7 @@ export function reportContractDeployed(properties: {
export function reportContractDeployFailed(properties: {
errorMessage: string;
chainId: number;
is_testnet: boolean | undefined;
publisher: string | undefined;
contractName: string | undefined;
}) {
@@ -171,9 +173,7 @@ export function reportOnboardingCompleted() {
*
*/
export function reportFaucetUsed(properties: { chainId: number }) {
posthog.capture("faucet used", {
chainId: properties.chainId,
});
posthog.capture("faucet used", properties);
}
// ----------------------------
@@ -197,12 +197,7 @@ export function reportChainConfigurationAdded(properties: {
decimals: number;
};
}) {
posthog.capture("chain configuration added", {
chainId: properties.chainId,
chainName: properties.chainName,
nativeCurrency: properties.nativeCurrency,
rpcURLs: properties.rpcURLs,
});
posthog.capture("chain configuration added", properties);
}
// ----------------------------
@@ -227,12 +222,9 @@ export function reportAssetBuySuccessful(properties: {
chainId: number;
contractType: AssetContractType | undefined;
assetType: "nft" | "coin";
is_testnet: boolean | undefined;
}) {
posthog.capture("asset buy successful", {
assetType: properties.assetType,
chainId: properties.chainId,
contractType: properties.contractType,
});
posthog.capture("asset buy successful", properties);
}
type TokenSwapParams = {
@@ -349,16 +341,12 @@ export function reportTokenSwapCancelled(properties: TokenSwapParams) {
*/
export function reportAssetBuyFailed(properties: {
chainId: number;
is_testnet: boolean | undefined;
contractType: AssetContractType | undefined;
assetType: "nft" | "coin";
error: string;
}) {
posthog.capture("asset buy failed", {
assetType: properties.assetType,
chainId: properties.chainId,
contractType: properties.contractType,
error: properties.error,
});
posthog.capture("asset buy failed", properties);
}
/**
@@ -371,14 +359,11 @@ export function reportAssetBuyFailed(properties: {
*/
export function reportAssetBuyCancelled(properties: {
chainId: number;
is_testnet: boolean | undefined;
contractType: AssetContractType | undefined;
assetType: "nft" | "coin";
}) {
posthog.capture("asset buy cancelled", {
assetType: properties.assetType,
chainId: properties.chainId,
contractType: properties.contractType,
});
posthog.capture("asset buy cancelled", properties);
}
// Assets Landing Page ----------------------------
@@ -423,10 +408,7 @@ export function reportAssetCreationStepConfigured(
step: "coin-info" | "token-distribution" | "launch-coin";
},
) {
posthog.capture("asset creation step configured", {
assetType: properties.assetType,
step: properties.step,
});
posthog.capture("asset creation step configured", properties);
}
/**
@@ -440,11 +422,10 @@ export function reportAssetCreationStepConfigured(
export function reportAssetCreationSuccessful(properties: {
assetType: "nft" | "coin";
contractType: AssetContractType;
chainId: number;
is_testnet: boolean | undefined;
}) {
posthog.capture("asset creation successful", {
assetType: properties.assetType,
contractType: properties.contractType,
});
posthog.capture("asset creation successful", properties);
}
type CoinCreationStep =
@@ -466,7 +447,12 @@ type CoinCreationStep =
* @MananTank
*/
export function reportAssetCreationFailed(
properties: { contractType: AssetContractType; error: string } & (
properties: {
contractType: AssetContractType;
error: string;
is_testnet: boolean | undefined;
chainId: number;
} & (
| {
assetType: "nft";
step:
@@ -481,12 +467,7 @@ export function reportAssetCreationFailed(
}
),
) {
posthog.capture("asset creation failed", {
assetType: properties.assetType,
contractType: properties.contractType,
error: properties.error,
step: properties.step,
});
posthog.capture("asset creation failed", properties);
}
type UpsellParams = {
@@ -582,6 +563,7 @@ export function reportPaymentLinkBuyFailed(properties: {
export function reportAssetPageview(properties: {
assetType: "nft" | "coin";
chainId: number;
is_testnet: boolean | undefined;
}) {
posthog.capture("asset pageview", properties);
}
@@ -593,7 +575,10 @@ export function reportAssetPageview(properties: {
* ### Who is responsible for this event?
* @MananTank
*/
export function reportChainPageview(properties: { chainId: number }) {
export function reportChainPageview(properties: {
chainId: number;
is_testnet: boolean | undefined;
}) {
posthog.capture("chain pageview", properties);
}
@@ -686,10 +671,7 @@ export function reportProductFeedback(properties: {
feedback: string;
source: "desktop" | "mobile";
}) {
posthog.capture("product feedback submitted", {
feedback: properties.feedback,
source: properties.source,
});
posthog.capture("product feedback submitted", properties);
}
/**
+3 -1
View File
@@ -448,6 +448,7 @@ type ActiveStatus = {
pay: boolean;
inAppWallet: boolean;
ecosystemWallet: boolean;
engineCloud: boolean;
};
export const isProjectActive = unstable_cache(
@@ -479,6 +480,7 @@ export const isProjectActive = unstable_cache(
pay: false,
rpc: false,
sdk: false,
engineCloud: false,
storage: false,
} as ActiveStatus;
}
@@ -488,7 +490,7 @@ export const isProjectActive = unstable_cache(
},
["isProjectActive"],
{
revalidate: 60 * 60, // 1 hour
revalidate: 30, // 30 seconds
},
);
@@ -34,6 +34,7 @@ export function BuyAndSwapEmbed(props: {
tokenAddress: string | undefined;
buyAmount: string | undefined;
pageType: PageType;
isTestnet: boolean | undefined;
}) {
const { theme } = useTheme();
const [tab, setTab] = useState<"buy" | "swap">("swap");
@@ -116,6 +117,7 @@ export function BuyAndSwapEmbed(props: {
chainId: props.chain.id,
error: errorMessage,
contractType: undefined,
is_testnet: props.isTestnet,
});
}
}}
@@ -141,6 +143,7 @@ export function BuyAndSwapEmbed(props: {
assetType: "coin",
chainId: props.chain.id,
contractType: undefined,
is_testnet: props.isTestnet,
});
}
}}
@@ -166,6 +169,7 @@ export function BuyAndSwapEmbed(props: {
assetType: "coin",
chainId: props.chain.id,
contractType: undefined,
is_testnet: props.isTestnet,
});
}
}}
@@ -5,6 +5,7 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { useLocalStorage } from "@/hooks/useLocalStorage";
// biome-ignore lint/correctness/noUnusedVariables: banner is toggled on-demand via API content changes
function AnnouncementBannerUI(props: {
href: string;
label: string;
@@ -64,7 +64,7 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
<CreateProjectDialogUI
createProject={async (params) => {
const res = await createProjectClient(props.teamId, params);
await createVaultAccountAndAccessToken({
const vaultTokens = await createVaultAccountAndAccessToken({
project: res.project,
projectSecretKey: res.secret,
}).catch((error) => {
@@ -74,6 +74,13 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
);
throw error;
});
const managementAccessToken = vaultTokens.managementToken?.accessToken;
if (!managementAccessToken) {
throw new Error("Missing management access token for project wallet");
}
return {
project: res.project,
secret: res.secret,
+9 -4
View File
@@ -119,12 +119,17 @@ export function useCsvUpload<
}
Papa.parse(csv, {
complete: (results) => {
const data = props.csvParser(results.data as T[]);
if (!data[0]?.address) {
try {
const data = props.csvParser(results.data as T[]);
if (!data[0]?.address) {
setNoCsv(true);
return;
}
setRawData(data);
} catch (error) {
console.error(error);
setNoCsv(true);
return;
}
setRawData(data);
},
header: true,
});
@@ -0,0 +1,21 @@
const PROJECT_WALLET_LABEL_SUFFIX = " Wallet";
const PROJECT_WALLET_LABEL_MAX_LENGTH = 32;
/**
* Builds the default label for a project's primary server wallet.
* Ensures a stable naming convention so the wallet can be identified across flows.
*/
export function getProjectWalletLabel(projectName: string | undefined) {
const baseName = projectName?.trim() || "Project";
const maxBaseLength = Math.max(
1,
PROJECT_WALLET_LABEL_MAX_LENGTH - PROJECT_WALLET_LABEL_SUFFIX.length,
);
const normalizedBase =
baseName.length > maxBaseLength
? baseName.slice(0, maxBaseLength).trimEnd()
: baseName;
return `${normalizedBase}${PROJECT_WALLET_LABEL_SUFFIX}`;
}
@@ -0,0 +1,99 @@
import "server-only";
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
import type { Project } from "@/api/project/projects";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
import { getProjectWalletLabel } from "@/lib/project-wallet";
export type ProjectWalletSummary = {
id: string;
address: string;
label?: string;
};
type VaultWalletListItem = {
id: string;
address: string;
metadata?: {
label?: string;
projectId?: string;
teamId?: string;
type?: string;
};
};
export async function getProjectWallet(
project: Project,
): Promise<ProjectWalletSummary | undefined> {
const engineCloudService = project.services.find(
(service) => service.name === "engineCloud",
);
const managementAccessToken =
engineCloudService?.managementAccessToken || undefined;
const projectWalletAddress = (
engineCloudService as { projectWalletAddress?: string } | undefined
)?.projectWalletAddress;
if (
!managementAccessToken ||
!NEXT_PUBLIC_THIRDWEB_VAULT_URL ||
!projectWalletAddress
) {
return undefined;
}
try {
const vaultClient = await createVaultClient({
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
});
const response = await listEoas({
client: vaultClient,
request: {
auth: {
accessToken: managementAccessToken,
},
options: {
page: 0,
// @ts-expect-error - SDK expects snake_case for pagination arguments
page_size: 100,
},
},
});
if (!response.success || !response.data) {
return undefined;
}
const items = response.data.items as VaultWalletListItem[] | undefined;
if (!items?.length) {
return undefined;
}
const defaultLabel = getProjectWalletLabel(project.name);
const serverWallets = items.filter(
(item) => item.metadata?.projectId === project.id,
);
const defaultWallet = serverWallets.find(
(item) =>
item.address.toLowerCase() === projectWalletAddress.toLowerCase(),
);
if (!defaultWallet) {
return undefined;
}
return {
id: defaultWallet.id,
address: defaultWallet.address,
label: defaultWallet.metadata?.label ?? defaultLabel,
};
} catch (error) {
console.error("Failed to load project wallet", error);
return undefined;
}
}
@@ -8,6 +8,7 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
return (
<GridPatternEmbedContainer>
<BuyAndSwapEmbed
isTestnet={props.chain.testnet}
// eslint-disable-next-line no-restricted-syntax
chain={defineDashboardChain(props.chain.chainId, props.chain)}
buyAmount={undefined}
@@ -2,10 +2,11 @@
import { reportChainPageview } from "@/analytics/report";
import { useEffectOnce } from "@/hooks/useEffectOnce";
export function ChainPageView(props: { chainId: number }) {
export function ChainPageView(props: { chainId: number; is_testnet: boolean }) {
useEffectOnce(() => {
reportChainPageview({
chainId: props.chainId,
is_testnet: props.is_testnet,
});
});
@@ -86,7 +86,7 @@ export default async function ChainPageLayout(props: {
return (
<div className="flex grow flex-col">
<ChainPageView chainId={chain.chainId} />
<ChainPageView chainId={chain.chainId} is_testnet={chain.testnet} />
<div className="border-border border-b bg-card">
<TeamHeader />
</div>
@@ -2,7 +2,7 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { Suspense, useState } from "react";
import type { NFT, ThirdwebClient, ThirdwebContract } from "thirdweb";
import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
@@ -89,7 +89,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
if (isPending) {
return (
<div className="flex h-[400px] items-center justify-center">
<div className="flex h-[400px] items-center justify-center py-20">
<Spinner className="size-10" />
</div>
);
@@ -179,15 +179,26 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
<NFTDetailsTab client={contract.client} nft={nft} />
)}
{tabs.map((tb) => {
return (
tb.title === tab && (
<div className="w-full" key={tb.title}>
{tb.children}
<Suspense
fallback={
<div className="h-full">
<div className="flex items-center gap-1.5">
<Spinner className="size-4" />
<p className="text-sm text-muted-foreground"> Loading </p>
</div>
)
);
})}
</div>
}
>
{tabs.map((tb) => {
return (
tb.title === tab && (
<div className="w-full" key={tb.title}>
{tb.children}
</div>
)
);
})}
</Suspense>
</div>
</div>
</div>
@@ -5,11 +5,13 @@ import { useEffectOnce } from "@/hooks/useEffectOnce";
export function AssetPageView(props: {
assetType: "nft" | "coin";
chainId: number;
is_testnet: boolean;
}) {
useEffectOnce(() => {
reportAssetPageview({
assetType: props.assetType,
chainId: props.chainId,
is_testnet: props.is_testnet,
});
});
@@ -122,6 +122,7 @@ export function TokenDropClaim(props: {
chainId: props.contract.chain.id,
contractType: "DropERC20",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
toast.error("Failed to approve spending", {
@@ -160,6 +161,7 @@ export function TokenDropClaim(props: {
chainId: props.contract.chain.id,
contractType: "DropERC20",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
toast.error("Failed to buy tokens", {
@@ -172,6 +174,7 @@ export function TokenDropClaim(props: {
assetType: "coin",
chainId: props.contract.chain.id,
contractType: "DropERC20",
is_testnet: props.chainMetadata.testnet,
});
setStepsUI({
@@ -76,7 +76,11 @@ export async function ERC20PublicPage(props: {
return (
<div className="flex grow flex-col">
<AssetPageView assetType="coin" chainId={props.chainMetadata.chainId} />
<AssetPageView
assetType="coin"
chainId={props.chainMetadata.chainId}
is_testnet={props.chainMetadata.testnet}
/>
<PageHeader containerClassName="max-w-7xl" />
<div className="border-b border-dashed">
@@ -199,6 +203,7 @@ function BuyEmbed(props: {
tokenAddress={props.clientContract.address}
buyAmount={undefined}
pageType="asset"
isTestnet={props.chainMetadata.testnet}
/>
);
}
@@ -18,7 +18,11 @@ export function NFTPublicPageLayout(props: {
}) {
return (
<div className="flex grow flex-col">
<AssetPageView assetType="nft" chainId={props.chainMetadata.chainId} />
<AssetPageView
assetType="nft"
chainId={props.chainMetadata.chainId}
is_testnet={props.chainMetadata.testnet}
/>
<PageHeader containerClassName="max-w-8xl" />
<div className="border-b">
<div className="container max-w-8xl">
@@ -120,6 +120,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC1155",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
console.error(errorMessage);
@@ -147,6 +148,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
assetType: "nft",
chainId: props.contract.chain.id,
contractType: "DropERC1155",
is_testnet: props.chainMetadata.testnet,
});
props.onSuccess?.();
@@ -163,6 +165,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC1155",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
return;
@@ -180,6 +183,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC1155",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
}
});
@@ -77,6 +77,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC721",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
return;
@@ -103,6 +104,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
assetType: "nft",
chainId: props.contract.chain.id,
contractType: "DropERC721",
is_testnet: props.chainMetadata.testnet,
});
props.onSuccess?.();
@@ -115,6 +117,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC721",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
return;
@@ -131,6 +134,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC721",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
}
};
@@ -190,25 +190,37 @@ export async function generateContractLayoutMetadata(params: {
.replace("Testnet", "")
.trim();
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
let title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
let description = "";
if (isERC721 || isERC1155) {
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
} else if (isERC20) {
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
title = `${contractMetadata.name} (${contractMetadata.symbol}) on ${cleanedChainName} | Buy, Swap, Bridge & Price`;
description = `Buy, swap & bridge ${contractMetadata.name} (${contractMetadata.symbol}) on ${cleanedChainName} with thirdweb Bridge. View contract address, holders, analytics, transactions and live price.`;
} else {
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
description = `View tokens, transactions, balances, source code, and analytics for the ${contractMetadata.name} smart contract on ${cleanedChainName}`;
}
return {
description: description,
title: title,
openGraph: {
description: description,
title: title,
},
};
} catch {
const fallbackTitle = `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`;
const fallbackDescription = `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`;
return {
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`,
title: `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`,
description: fallbackDescription,
title: fallbackTitle,
openGraph: {
description: fallbackDescription,
title: fallbackTitle,
},
};
}
}
@@ -50,6 +50,7 @@ import {
} from "@/constants/addresses";
import { LAST_USED_PROJECT_ID, LAST_USED_TEAM_ID } from "@/constants/cookie";
import { ZERO_FEE_CHAINS } from "@/constants/fee-config";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import {
useCustomFactoryAbi,
useFunctionParamsFromABI,
@@ -206,6 +207,8 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
const activeAccount = useActiveAccount();
const walletChain = useActiveWalletChain();
const walletChainMetadata = useV5DashboardChain(walletChain?.id);
const { onError } = useTxNotifications(
"Successfully deployed contract",
"Failed to deploy contract",
@@ -656,6 +659,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
chainId: walletChain.id,
contractName: metadata.name,
publisher: rewriteTwPublisher(metadata.publisher),
is_testnet: walletChainMetadata?.testnet,
});
deployStatusModal.nextStep();
@@ -694,6 +698,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
contractName: metadata.name,
errorMessage: parsedError,
publisher: rewriteTwPublisher(metadata.publisher),
is_testnet: walletChain.testnet,
});
deployStatusModal.close();
@@ -39,6 +39,7 @@ export function TokensTable(props: {
<TableHead>Price</TableHead>
<TableHead>Market cap</TableHead>
<TableHead>Volume (24h)</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -64,6 +65,9 @@ export function TokensTable(props: {
<TableCell>
<Skeleton className="ml-auto h-4 w-20" />
</TableCell>
<TableCell>
<Skeleton className="ml-auto h-4 w-20" />
</TableCell>
</TableRow>
))
: tokens.length > 0
@@ -81,6 +85,7 @@ export function TokensTable(props: {
<AvatarImage
src={token.iconUri ?? undefined}
alt={token.symbol}
className="rounded-full"
/>
<AvatarFallback>
{token.symbol?.slice(0, 2)?.toUpperCase()}
@@ -90,20 +95,9 @@ export function TokensTable(props: {
<span className="font-medium leading-none text-base">
{token.symbol}
</span>
<Link
href={
token.address.toLowerCase() ===
NATIVE_TOKEN_ADDRESS.toLowerCase()
? `/${token.chainId}`
: `/${token.chainId}/${token.address}`
}
prefetch={false}
className="before:absolute before:inset-0"
>
<span className="text-sm text-muted-foreground leading-none">
{token.name}
</span>
</Link>
<span className="text-sm text-muted-foreground leading-none">
{token.name}
</span>
</div>
</div>
</TableCell>
@@ -120,6 +114,29 @@ export function TokensTable(props: {
? formatUsdCompact(token.volume24hUsd)
: "N/A"}
</TableCell>
<TableCell>
<Button
asChild
variant="outline"
size="sm"
className="gap-2 rounded-full bg-card hover:border-foreground/50 text-muted-foreground hover:bg-inverted hover:text-inverted-foreground"
>
<Link
aria-label={`Buy ${token.symbol}`}
href={
token.address.toLowerCase() ===
NATIVE_TOKEN_ADDRESS.toLowerCase()
? `/${token.chainId}`
: `/${token.chainId}/${token.address}`
}
prefetch={false}
className="before:absolute before:inset-0"
>
Buy
<ArrowRightIcon className="size-3" />
</Link>
</Button>
</TableCell>
</TableRow>
);
})
@@ -1,7 +1,6 @@
import { BringToFrontIcon } from "lucide-react";
import type { Metadata } from "next";
import { unstable_cache } from "next/cache";
import Link from "next/link";
import { Bridge } from "thirdweb";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { PageHeader } from "./components/header";
@@ -35,13 +34,10 @@ export default async function Page() {
<h1 className="text-4xl font-bold tracking-tighter mb-1">
Discover and swap any tokens on any chain, instantly
</h1>
<Link
className="text-muted-foreground hover:text-foreground"
href="https://thirdweb.com/monetize/bridge"
target="_blank"
>
Powered by thirdweb bridge
</Link>
<p className="text-muted-foreground">
85+ chains, 4500+ tokens and 9+ million routes supported
</p>
</div>
</div>
<TokenPage chains={chains} />
@@ -55,7 +55,7 @@ export function CreateProjectFormOnboarding(props: {
<CreateProjectForm
createProject={async (params) => {
const res = await createProjectClient(props.teamId, params);
await createVaultAccountAndAccessToken({
const vaultTokens = await createVaultAccountAndAccessToken({
project: res.project,
projectSecretKey: res.secret,
}).catch((error) => {
@@ -65,6 +65,16 @@ export function CreateProjectFormOnboarding(props: {
);
throw error;
});
const managementAccessToken =
vaultTokens.managementToken?.accessToken;
if (!managementAccessToken) {
throw new Error(
"Missing management access token for project wallet",
);
}
return {
project: res.project,
secret: res.secret,
@@ -28,11 +28,11 @@ export function QuickStartSection(props: {
icon={ArrowRightLeftIcon}
setupTime={5}
id="swap_tokens"
features={["Swap any token", "Cross-chain swaps"]}
features={["Swap any token", "Cross-chain swap"]}
description="Swap tokens cross-chain with dedicated swapping endpoints."
link={{
href: `https://portal.thirdweb.com/payments/swap`,
label: "Setup Swaps",
href: `https://portal.thirdweb.com/bridge/swap`,
label: "Setup Swap",
}}
/>
@@ -69,70 +69,20 @@ export default async function Page(props: {
},
},
links: [
// TODO - add docs when bridge docs are added in portal
// {
// type: "docs",
// href: "https://portal.thirdweb.com/payments",
// },
{
type: "docs",
href: "https://portal.thirdweb.com/bridge",
},
{
type: "playground",
href: "https://playground.thirdweb.com/bridge/swap-widget",
},
{
type: "api",
href: "https://api.thirdweb.com/reference#tag/bridge",
},
],
}}
footer={{
center: {
links: [
{
href: "https://playground.thirdweb.com/payments/ui-components",
label: "UI Component",
},
{
href: "https://playground.thirdweb.com/connect/payments/fund-wallet",
label: "Buy Crypto",
},
{
href: "https://playground.thirdweb.com/connect/payments/commerce",
label: "Checkout",
},
{
href: "https://playground.thirdweb.com/connect/payments/transactions",
label: "Transactions",
},
],
title: "Demos",
},
left: {
links: [
{
href: "https://portal.thirdweb.com/payments",
label: "Overview",
},
{
href: "https://portal.thirdweb.com/typescript/v5/convertCryptoToFiat",
label: "TypeScript",
},
{
href: "https://portal.thirdweb.com/react/v5/pay/fund-wallets",
label: "React",
},
{
href: "https://portal.thirdweb.com/dotnet/universal-bridge/quickstart",
label: ".NET",
},
],
title: "Documentation",
},
right: {
links: [
{
href: "https://www.youtube.com/watch?v=aBu175-VsNY",
label: "Implement cross-chain payments in any app",
},
],
title: "Tutorials",
},
}}
>
<div className="flex flex-col gap-12">
<ResponsiveSearchParamsProvider value={searchParams}>
@@ -1,45 +0,0 @@
"use client";
import { useState } from "react";
import { TabButtons } from "@/components/ui/tabs";
type TabKey =
| "ts"
| "react"
| "react-native"
| "dotnet"
| "unity"
| "unreal"
| "api"
| "curl";
const tabNames: Record<TabKey, string> = {
api: "API",
ts: "TypeScript",
react: "React",
"react-native": "React Native",
dotnet: ".NET",
unity: "Unity",
unreal: "Unreal Engine",
curl: "cURL",
};
export function IntegrateAPIKeyCodeTabs(props: {
tabs: Record<TabKey, React.ReactNode>;
}) {
const [tab, setTab] = useState<TabKey>("api");
return (
<div>
<TabButtons
tabs={Object.entries(tabNames).map(([key, name]) => ({
isActive: tab === key,
name,
onClick: () => setTab(key as TabKey),
}))}
/>
<div className="h-2" />
{props.tabs[tab]}
</div>
);
}
@@ -31,5 +31,6 @@ export const Default: Story = {
],
},
teamSlug: "bar",
managementAccessToken: undefined,
},
};
@@ -1,14 +1,11 @@
import {
ArrowLeftRightIcon,
ChevronRightIcon,
CircleAlertIcon,
ExternalLinkIcon,
} from "lucide-react";
import Link from "next/link";
import type { Project } from "@/api/project/projects";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CodeServer } from "@/components/ui/code/code.server";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { DotNetIcon } from "@/icons/brand-icons/DotNetIcon";
import { GithubIcon } from "@/icons/brand-icons/GithubIcon";
import { ReactIcon } from "@/icons/brand-icons/ReactIcon";
@@ -18,17 +15,34 @@ import { UnrealIcon } from "@/icons/brand-icons/UnrealIcon";
import { ContractIcon } from "@/icons/ContractIcon";
import { InsightIcon } from "@/icons/InsightIcon";
import { PayIcon } from "@/icons/PayIcon";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
import { getProjectWalletLabel } from "@/lib/project-wallet";
import {
getProjectWallet,
type ProjectWalletSummary,
} from "@/lib/server/project-wallet";
import { ClientIDSection } from "./ClientIDSection";
import { IntegrateAPIKeyCodeTabs } from "./IntegrateAPIKeyCodeTabs";
import { ProjectWalletControls } from "./ProjectWalletControls.client";
import { ProjectWalletSetup } from "./ProjectWalletSetup.client";
import { SecretKeySection } from "./SecretKeySection";
export function ProjectFTUX(props: { project: Project; teamSlug: string }) {
export async function ProjectFTUX(props: {
project: Project;
teamSlug: string;
wallet?: ProjectWalletSummary | undefined;
managementAccessToken: string | undefined;
}) {
const projectWallet = props.wallet ?? (await getProjectWallet(props.project));
return (
<div className="flex flex-col gap-10">
<IntegrateAPIKeySection
<ProjectWalletSection
project={props.project}
teamSlug={props.teamSlug}
wallet={projectWallet}
managementAccessToken={props.managementAccessToken}
/>
<IntegrateAPIKeySection project={props.project} />
<ProductsSection
projectSlug={props.project.slug}
teamSlug={props.teamSlug}
@@ -39,15 +53,67 @@ export function ProjectFTUX(props: { project: Project; teamSlug: string }) {
);
}
// Integrate API key section ------------------------------------------------------------
function IntegrateAPIKeySection({
project,
teamSlug,
}: {
export function ProjectWalletSection(props: {
project: Project;
teamSlug: string;
wallet: ProjectWalletSummary | undefined;
managementAccessToken: string | undefined;
}) {
const defaultLabel = getProjectWalletLabel(props.project.name);
const walletAddress = props.wallet?.address;
const label = props.wallet?.label ?? defaultLabel;
return (
<section>
<div className="rounded-lg border border-border bg-card p-4">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<div className="rounded-full border border-border bg-background p-2">
<WalletProductIcon className="size-5 text-muted-foreground" />
</div>
<div>
<p className="font-semibold text-lg tracking-tight">
Project Wallet
</p>
<p className="text-muted-foreground text-sm">
Use it for deployments, payments, and API integrations.
</p>
</div>
</div>
{walletAddress ? (
<>
<ProjectWalletControls
label={label}
project={props.project}
walletAddress={walletAddress}
/>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
<Link
className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
href={`/team/${props.teamSlug}/${props.project.slug}/transactions`}
>
View Transactions
<ChevronRightIcon className="size-4" />
</Link>
</div>
</>
) : (
<ProjectWalletSetup
managementAccessToken={props.managementAccessToken}
project={props.project}
teamSlug={props.teamSlug}
/>
)}
</div>
</div>
</section>
);
}
// Integrate API key section ------------------------------------------------------------
function IntegrateAPIKeySection({ project }: { project: Project }) {
const secretKeyMasked = project.secretKeys[0]?.masked;
const clientId = project.publishableKey;
@@ -69,194 +135,38 @@ function IntegrateAPIKeySection({
</div>
<div className="h-5" />
<IntegrationCodeExamples project={project} teamSlug={teamSlug} />
<div className="flex flex-col gap-3">
<p className="text-muted-foreground text-sm">
Run this command in your terminal to send a test transaction using
your Project Wallet.
</p>
<CodeServer
className="bg-background"
code={curlCodeExample(project)}
lang="bash"
/>
</div>
</div>
</section>
);
}
function IntegrationCodeExamples(props: {
project: Project;
teamSlug: string;
}) {
return (
<IntegrateAPIKeyCodeTabs
tabs={{
dotnet: (
<div className="flex flex-col gap-3">
<CodeServer
className="bg-background"
code={dotNotCodeExample(props.project)}
lang="csharp"
/>
<Alert className="bg-background" variant="info">
<CircleAlertIcon className="size-5" />
<AlertTitle>
Configure your app's bundle ID in "Allowed Bundle IDs" in
Project
</AlertTitle>
<AlertDescription className="leading-loose">
Go to{" "}
<UnderlineLink
href={`/team/${props.teamSlug}/${props.project.slug}/settings`}
target="_blank"
>
Project settings
</UnderlineLink>{" "}
and add your app's bundle ID to the "Allowed Bundle IDs" list.
</AlertDescription>
</Alert>
</div>
),
react: (
<CodeServer
className="bg-background"
code={reactCodeExample(props.project)}
lang="ts"
/>
),
"react-native": (
<CodeServer
className="bg-background"
code={reactCodeExample(props.project)}
lang="ts"
/>
),
ts: (
<CodeServer
className="bg-background"
code={typescriptCodeExample(props.project)}
lang="ts"
/>
),
unity: (
<Alert className="bg-background" variant="info">
<CircleAlertIcon className="size-5" />
<AlertTitle>
Configure Client ID in Thirdweb Manager prefab
</AlertTitle>
<AlertDescription className="leading-loose">
Configure "Client ID" and "Bundle ID" in{" "}
<UnderlineLink
href="https://portal.thirdweb.com/unity/v5/thirdwebmanager"
rel="noopener noreferrer"
target="_blank"
>
Thirdweb Manager prefab
</UnderlineLink>
<span className="block text-sm">
Make sure to configure your app's bundle ID in "Allowed Bundle
IDs" in{" "}
<UnderlineLink
href={`/team/${props.teamSlug}/${props.project.slug}/settings`}
rel="noopener noreferrer"
target="_blank"
>
Project settings
</UnderlineLink>
</span>
</AlertDescription>
</Alert>
),
unreal: (
<Alert className="bg-background" variant="info">
<CircleAlertIcon className="size-5" />
<AlertTitle>
Configure Client ID in Thirdweb Unreal Plugin{" "}
</AlertTitle>
<AlertDescription className="leading-loose">
Configure "Client ID" and "Bundle ID" in{" "}
<UnderlineLink
href="https://portal.thirdweb.com/unreal-engine/getting-started"
rel="noopener noreferrer"
target="_blank"
>
thirdweb plugin settings
</UnderlineLink>
<span className="block text-sm">
Make sure to configure your app's bundle ID in "Allowed Bundle
IDs" in{" "}
<UnderlineLink
href={`/team/${props.teamSlug}/${props.project.slug}/settings`}
rel="noopener noreferrer"
target="_blank"
>
Project settings
</UnderlineLink>
</span>
</AlertDescription>
</Alert>
),
api: (
<CodeServer
className="bg-background"
code={apiCodeExample(props.project)}
lang="javascript"
/>
),
curl: (
<CodeServer
className="bg-background"
code={curlCodeExample(props.project)}
lang="bash"
/>
),
}}
/>
);
}
const typescriptCodeExample = (project: Project) => `\
import { createThirdwebClient } from "thirdweb";
const client = createThirdwebClient({
// use clientId for client side usage
clientId: "${project.publishableKey}",
// use secretKey for server side usage
secretKey: "${project.secretKeys[0]?.masked}", // replace this with full secret key
});`;
const reactCodeExample = (project: Project) => `\
import { createThirdwebClient } from "thirdweb";
const client = createThirdwebClient({
clientId: "${project.publishableKey}",
});`;
const dotNotCodeExample = (project: Project) => `\
using Thirdweb;
// For web applications
var client = ThirdwebClient.Create(clientId: "${project.publishableKey}");
// For native applications - Replace "yourBundleId" with your app's bundle ID
var client = ThirdwebClient.Create(clientId: "${project.publishableKey}", bundleId: "yourBundleId");
// For backend applications (Note: below shown secret key is not the full secret key)
var client = ThirdwebClient.Create(secretKey: "${project.secretKeys[0]?.masked}");`;
const apiCodeExample = (project: Project): string => `\
// Server-side only: replace with your full secret key. Never expose in browser code.
fetch('https://api.thirdweb.com/v1/wallets/server', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-secret-key': '${project.secretKeys[0]?.masked ?? "<YOUR_SECRET_KEY>"}'
},
body: JSON.stringify({
identifier: 'treasury-wallet-123'
})
});`;
const curlCodeExample = (project: Project): string => `\
// Server-side only: replace with your full secret key. Never expose in browser code.
curl https://api.thirdweb.com/v1/wallets/server \\
curl https://api.thirdweb.com/v1/transactions \\
--request POST \\
--header 'Content-Type: application/json' \\
--header 'x-secret-key: ${project.secretKeys[0]?.masked ?? "<YOUR_SECRET_KEY>"}' \\
--data '{
"identifier": "treasury-wallet-123"
}'`;
"chainId": 421614,
"transactions": [
{
"data": "0x",
"to": "vitalik.eth",
"value": "0"
}
]
}'
`;
// products section ------------------------------------------------------------
function ProductsSection(props: { teamSlug: string; projectSlug: string }) {
@@ -0,0 +1,747 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
EllipsisVerticalIcon,
RefreshCcwIcon,
SendIcon,
ShuffleIcon,
WalletIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { toWei } from "thirdweb";
import { useWalletBalance } from "thirdweb/react";
import { isAddress } from "thirdweb/utils";
import { z } from "zod";
import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets";
import { sendProjectWalletTokens } from "@/actions/project-wallet/send-tokens";
import type { Project } from "@/api/project/projects";
import { FundWalletModal } from "@/components/blocks/fund-wallets-modal";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/Spinner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
import { cn } from "@/lib/utils";
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
type ProjectWalletControlsProps = {
walletAddress: string;
label: string;
project: Pick<
Project,
"id" | "publishableKey" | "teamId" | "services" | "name"
>;
defaultChainId?: number;
};
export function ProjectWalletControls(props: ProjectWalletControlsProps) {
const { walletAddress, label, project, defaultChainId } = props;
const [isSendOpen, setIsSendOpen] = useState(false);
const [isReceiveOpen, setIsReceiveOpen] = useState(false);
const [selectedChainId, setSelectedChainId] = useState(defaultChainId ?? 1);
const [isChangeWalletOpen, setIsChangeWalletOpen] = useState(false);
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
undefined,
);
const client = useMemo(() => getClientThirdwebClient(), []);
const chain = useV5DashboardChain(selectedChainId);
const queryClient = useQueryClient();
const router = useDashboardRouter();
const engineCloudService = useMemo(
() => project.services?.find((service) => service.name === "engineCloud"),
[project.services],
);
const managementAccessToken =
engineCloudService?.managementAccessToken ?? undefined;
const isManagedVault = !!engineCloudService?.encryptedAdminKey;
const serverWalletsQuery = useQuery({
enabled: !!managementAccessToken,
queryFn: async () => {
if (!managementAccessToken) {
return [] as ProjectWalletSummary[];
}
return listProjectServerWallets({
managementAccessToken,
projectId: project.id,
});
},
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
staleTime: 60_000,
});
const serverWallets = serverWalletsQuery.data ?? [];
const currentWalletAddressLower = walletAddress.toLowerCase();
const otherWallets = useMemo(() => {
return serverWallets.filter((wallet) => {
return wallet.address.toLowerCase() !== currentWalletAddressLower;
});
}, [serverWallets, currentWalletAddressLower]);
const canChangeWallet = otherWallets.length > 0;
const handleOpenChangeWallet = () => {
const currentWallet = serverWallets.find(
(wallet) => wallet.address.toLowerCase() === currentWalletAddressLower,
);
const nextWalletId =
otherWallets[0]?.id ?? currentWallet?.id ?? serverWallets[0]?.id;
setSelectedWalletId(nextWalletId);
setIsChangeWalletOpen(true);
};
const handleCloseChangeWallet = () => {
setIsChangeWalletOpen(false);
setSelectedWalletId(undefined);
};
const selectedWallet = useMemo(() => {
if (!selectedWalletId) {
return undefined;
}
return serverWallets.find((wallet) => wallet.id === selectedWalletId);
}, [selectedWalletId, serverWallets]);
const isSelectionDifferent = Boolean(
selectedWallet &&
selectedWallet.address.toLowerCase() !== currentWalletAddressLower,
);
const changeWalletMutation = useMutation({
mutationFn: async (wallet: ProjectWalletSummary) => {
await updateDefaultProjectWallet({
project: project as Project,
projectWalletAddress: wallet.address,
});
},
onError: (error) => {
const message =
error instanceof Error
? error.message
: "Failed to update project wallet";
toast.error(message);
},
onSuccess: async (_, wallet) => {
const descriptionLabel = wallet.label ?? wallet.address;
toast.success("Project wallet updated", {
description: `Now pointing to ${descriptionLabel}`,
});
await queryClient.invalidateQueries({
queryKey: [
"project",
project.id,
"server-wallets",
managementAccessToken,
],
});
handleCloseChangeWallet();
router.refresh();
},
});
const balanceQuery = useWalletBalance({
address: walletAddress,
chain,
client,
});
const balanceDisplay = balanceQuery.data
? `${balanceQuery.data.displayValue} ${balanceQuery.data.symbol}`
: undefined;
return (
<div className="space-y-5">
<div className="rounded-lg border border-border bg-background p-3">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3 lg:flex-row">
<div className="flex-1">
<p className="text-muted-foreground text-xs">Address</p>
<WalletAddress address={walletAddress} client={client} />
</div>
<div className="flex-1">
<p className="text-muted-foreground text-xs">Label</p>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<p className="font-mono text-sm break-all py-2 sm:py-0">
{label}
</p>
{canChangeWallet && (
<Button
className="h-8 gap-1 sm:ml-4"
onClick={handleOpenChangeWallet}
size="sm"
disabled={serverWalletsQuery.isLoading}
variant="outline"
>
<ShuffleIcon className="size-3.5" />
Change default
</Button>
)}
</div>
</div>
</div>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex-1 flex-col">
<p className="text-xs text-muted-foreground pb-2">Balance</p>
<div className="flex items-center gap-2">
<WalletIcon className="size-4 text-muted-foreground" />
{balanceQuery.isLoading ? (
<Spinner className="size-4" />
) : balanceDisplay ? (
<span className="text-md tracking-tight">
{balanceDisplay}
</span>
) : (
<span className="text-muted-foreground text-sm">N/A</span>
)}
<Button
aria-label="Refresh balance"
className="h-8 w-8 p-0"
disabled={balanceQuery.isFetching}
onClick={() => balanceQuery.refetch()}
size="icon"
variant="outline"
>
<RefreshCcwIcon
className={cn(
"size-4",
balanceQuery.isFetching && "animate-spin",
)}
/>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Open wallet actions"
className="h-9 w-9"
size="icon"
variant="outline"
>
<EllipsisVerticalIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 rounded-xl">
<DropdownMenuItem
className="flex items-center gap-2"
onSelect={() => setIsSendOpen(true)}
>
<SendIcon className="size-4 text-muted-foreground" />
Send funds
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onSelect={() => setIsReceiveOpen(true)}
>
<WalletIcon className="size-4 text-muted-foreground" />
Receive funds
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex-col flex-1">
<p className="text-xs text-muted-foreground pb-2">Network</p>
<SingleNetworkSelector
chainId={selectedChainId}
className="max-w-xs rounded-lg"
client={client}
disableDeprecated
disableChainId
onChange={setSelectedChainId}
placeholder="Select network"
/>
</div>
</div>
</div>
</div>
<SendProjectWalletModal
chainId={selectedChainId}
client={client}
isManagedVault={isManagedVault}
label={label}
onClose={() => setIsSendOpen(false)}
onSuccess={() => balanceQuery.refetch()}
open={isSendOpen}
publishableKey={project.publishableKey}
teamId={project.teamId}
walletAddress={walletAddress}
/>
<FundWalletModal
checkoutWidgetTitle={`Fund ${label}`}
client={client}
defaultChainId={selectedChainId}
description="Use your card or crypto to deposit into this server wallet"
onOpenChange={setIsReceiveOpen}
open={isReceiveOpen}
recipientAddress={walletAddress}
title="Fund project wallet"
/>
<Dialog
onOpenChange={(nextOpen) => {
if (!nextOpen) {
handleCloseChangeWallet();
}
}}
open={isChangeWalletOpen}
>
<DialogContent className="gap-0 p-0">
<DialogHeader className="p-4 lg:p-6">
<DialogTitle>Change default wallet</DialogTitle>
<DialogDescription>
Choose another server wallet to use as the default for this
project.
</DialogDescription>
</DialogHeader>
<div className="px-4 pb-8 lg:px-6">
{serverWalletsQuery.isLoading ? (
<div className="flex justify-center py-6">
<Spinner className="size-5" />
</div>
) : serverWallets.length <= 1 ? (
<p className="text-sm text-muted-foreground">
You need at least two server wallets to pick a different
default.
</p>
) : (
<div className="flex flex-col gap-3">
<Select
onValueChange={(value) => setSelectedWalletId(value)}
value={selectedWalletId ?? ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server wallet">
{selectedWallet ? (
<div className="flex items-center gap-2">
<WalletAddress
address={selectedWallet.address}
client={client}
/>
<div className="flex items-center gap-2">
<span className="text-sm text-foreground">
{selectedWallet.label ?? "Unnamed server wallet"}
</span>
{selectedWallet.address.toLowerCase() ===
currentWalletAddressLower && (
<Badge variant="success" className="text-xs">
current
</Badge>
)}
</div>
</div>
) : (
"Select server wallet"
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{serverWallets.map((wallet) => {
const isCurrent =
wallet.address.toLowerCase() ===
currentWalletAddressLower;
return (
<SelectItem key={wallet.id} value={wallet.id}>
<div className="flex items-center gap-2">
<WalletAddress
address={wallet.address}
client={client}
/>
<div className="flex items-center gap-2">
<span className="text-sm text-foreground">
{wallet.label ?? "Unnamed server wallet"}
</span>
{isCurrent && (
<Badge variant="success" className="text-xs">
current
</Badge>
)}
</div>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
)}
</div>
<DialogFooter className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
<Button
onClick={handleCloseChangeWallet}
type="button"
variant="outline"
disabled={changeWalletMutation.isPending}
>
Cancel
</Button>
<Button
disabled={
!selectedWallet ||
!isSelectionDifferent ||
changeWalletMutation.isPending
}
onClick={() => {
if (selectedWallet) {
changeWalletMutation.mutate(selectedWallet);
}
}}
type="button"
>
{changeWalletMutation.isPending && (
<Spinner className="mr-2 size-4" />
)}
Save default
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
const createSendFormSchema = (secretKeyLabel: string) =>
z.object({
chainId: z.number({
required_error: "Select a network",
}),
toAddress: z
.string()
.trim()
.min(1, "Destination address is required")
.refine((value) => Boolean(isAddress(value)), {
message: "Enter a valid wallet address",
}),
amount: z.string().trim().min(1, "Amount is required"),
secretKey: z.string().trim().min(1, `${secretKeyLabel} is required`),
vaultAccessToken: z.string().trim(),
});
const SECRET_KEY_LABEL = "Project secret key";
type SendFormValues = z.infer<ReturnType<typeof createSendFormSchema>>;
function SendProjectWalletModal(props: {
open: boolean;
onClose: () => void;
onSuccess: () => void;
walletAddress: string;
publishableKey: string;
teamId: string;
chainId: number;
label: string;
client: ReturnType<typeof getClientThirdwebClient>;
isManagedVault: boolean;
}) {
const {
open,
onClose,
onSuccess,
walletAddress,
publishableKey,
teamId,
chainId,
label,
client,
isManagedVault,
} = props;
const secretKeyLabel = SECRET_KEY_LABEL;
const secretKeyPlaceholder = "Enter your project secret key";
const secretKeyHelper =
"Your project secret key was generated when you created your project. If you lost it, regenerate one from Project settings.";
const vaultAccessTokenHelper =
"Vault access tokens are optional credentials with server wallet permissions. Manage them in Vault settings.";
const formSchema = useMemo(() => createSendFormSchema(SECRET_KEY_LABEL), []);
const form = useForm<SendFormValues>({
defaultValues: {
amount: "0",
chainId,
secretKey: "",
vaultAccessToken: "",
toAddress: "",
},
mode: "onChange",
resolver: zodResolver(formSchema),
});
const selectedChain = useV5DashboardChain(form.watch("chainId"));
// eslint-disable-next-line no-restricted-syntax -- form submission chainId must track selector state
useEffect(() => {
form.setValue("chainId", chainId);
}, [chainId, form]);
// eslint-disable-next-line no-restricted-syntax -- reset cached inputs when modal closes to avoid leaking state
useEffect(() => {
if (!open) {
const currentValues = form.getValues();
form.reset({
amount: "0",
chainId,
secretKey: currentValues.secretKey ?? "",
vaultAccessToken: currentValues.vaultAccessToken ?? "",
toAddress: "",
});
}
}, [open, chainId, form]);
const sendMutation = useMutation({
mutationFn: async (values: SendFormValues) => {
const quantityWei = toWei(values.amount).toString();
const secretKeyValue = values.secretKey.trim();
const vaultAccessTokenValue = values.vaultAccessToken.trim();
const result = await sendProjectWalletTokens({
chainId: values.chainId,
publishableKey,
quantityWei,
recipientAddress: values.toAddress,
teamId,
walletAddress,
secretKey: secretKeyValue,
...(vaultAccessTokenValue
? { vaultAccessToken: vaultAccessTokenValue }
: {}),
});
if (!result.ok) {
const errorMessage =
typeof result.error === "string"
? result.error
: "Failed to send funds";
throw new Error(errorMessage);
}
return result.transactionIds;
},
onError: (error) => {
toast.error(error.message);
},
onSuccess: (transactionIds) => {
toast.success("Transfer submitted", {
description:
transactionIds && transactionIds.length > 0
? `Transaction ID: ${transactionIds[0]}`
: undefined,
});
onSuccess();
onClose();
},
});
return (
<Dialog
onOpenChange={(nextOpen) => {
if (!nextOpen) {
onClose();
}
}}
open={open}
>
<DialogContent className="gap-0 p-0">
<DialogHeader className="p-4 lg:p-6">
<DialogTitle>Send from {label}</DialogTitle>
<DialogDescription>
Execute a one-off transfer using your server wallet.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) => {
void sendMutation.mutateAsync(values);
})}
>
<div className="space-y-4 px-4 pb-8 lg:px-6">
<FormField
control={form.control}
name="chainId"
render={({ field }) => (
<FormItem>
<FormLabel>Network</FormLabel>
<FormControl>
<SingleNetworkSelector
chainId={field.value}
className="bg-card"
client={client}
disableDeprecated
onChange={(nextChainId) => {
field.onChange(nextChainId);
}}
placeholder="Select network"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="toAddress"
render={({ field }) => (
<FormItem>
<FormLabel>Send to</FormLabel>
<FormControl>
<Input
autoComplete="off"
placeholder="0x..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<Input
inputMode="decimal"
min="0"
step="any"
{...field}
/>
</FormControl>
<FormDescription>
Sending native token
{selectedChain?.nativeCurrency?.symbol
? ` (${selectedChain.nativeCurrency.symbol})`
: ""}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretKey"
render={({ field }) => (
<FormItem>
<FormLabel>{secretKeyLabel}</FormLabel>
<FormControl>
<Input
autoComplete="off"
autoCorrect="off"
placeholder={secretKeyPlaceholder}
spellCheck={false}
type="password"
{...field}
/>
</FormControl>
<FormDescription>{secretKeyHelper}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{!isManagedVault && (
<FormField
control={form.control}
name="vaultAccessToken"
render={({ field }) => (
<FormItem>
<FormLabel>
Vault access token
<span className="text-muted-foreground">
{" "}
(optional)
</span>
</FormLabel>
<FormControl>
<Input
autoComplete="off"
autoCorrect="off"
placeholder="Enter a vault access token (optional)"
spellCheck={false}
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{vaultAccessTokenHelper}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
<div className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
<Button onClick={onClose} type="button" variant="outline">
Cancel
</Button>
<Button
className="gap-2"
disabled={!form.formState.isValid || sendMutation.isPending}
type="submit"
>
{sendMutation.isPending && <Spinner className="size-4" />}
Submit transfer
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,215 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CircleAlertIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets";
import type { Project } from "@/api/project/projects";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
import CreateServerWallet from "../../transactions/server-wallets/components/create-server-wallet.client";
export function ProjectWalletSetup(props: {
project: Project;
teamSlug: string;
managementAccessToken: string | undefined;
}) {
const { project, teamSlug, managementAccessToken } = props;
const router = useDashboardRouter();
const queryClient = useQueryClient();
const client = useMemo(() => getClientThirdwebClient(), []);
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
undefined,
);
const serverWalletsQuery = useQuery({
enabled: Boolean(managementAccessToken),
queryFn: async () => {
if (!managementAccessToken) {
return [] as ProjectWalletSummary[];
}
return listProjectServerWallets({
managementAccessToken,
projectId: project.id,
});
},
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
staleTime: 60_000,
});
const serverWallets = serverWalletsQuery.data ?? [];
const effectiveSelectedWalletId = useMemo(() => {
if (
selectedWalletId &&
serverWallets.some((wallet) => wallet.id === selectedWalletId)
) {
return selectedWalletId;
}
return serverWallets[0]?.id;
}, [selectedWalletId, serverWallets]);
const selectedWallet = useMemo(() => {
if (!effectiveSelectedWalletId) {
return undefined;
}
return serverWallets.find(
(wallet) => wallet.id === effectiveSelectedWalletId,
);
}, [effectiveSelectedWalletId, serverWallets]);
const setDefaultWalletMutation = useMutation({
mutationFn: async (wallet: ProjectWalletSummary) => {
await updateDefaultProjectWallet({
project,
projectWalletAddress: wallet.address,
});
},
onError: (error) => {
const message =
error instanceof Error
? error.message
: "Failed to update project wallet";
toast.error(message);
},
onSuccess: async (_, wallet) => {
const descriptionLabel = wallet.label ?? wallet.address;
toast.success("Project wallet updated", {
description: `Now pointing to ${descriptionLabel}`,
});
await queryClient.invalidateQueries({
queryKey: [
"project",
project.id,
"server-wallets",
managementAccessToken,
],
});
router.refresh();
},
});
const canSelectExisting =
Boolean(managementAccessToken) && serverWallets.length > 0;
return (
<Alert variant="info">
<CircleAlertIcon className="size-5" />
<AlertTitle>No project wallet set</AlertTitle>
<AlertDescription>
Set a project wallet to use for dashboard and API integrations.
</AlertDescription>
<div className="mt-4 space-y-5">
{serverWalletsQuery.isLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Spinner className="size-4" />
<span>Loading existing server wallets…</span>
</div>
) : canSelectExisting ? (
<div className="space-y-4">
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">
Choose an existing server wallet
</p>
<p className="text-sm text-muted-foreground">
These wallets were already created for this project. Pick one to
set as the default.
</p>
<Select
onValueChange={(value) => setSelectedWalletId(value)}
value={effectiveSelectedWalletId ?? ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server wallet">
{selectedWallet ? (
<div className="flex items-center gap-2">
<WalletAddress
address={selectedWallet.address}
client={client}
/>
<span className="text-muted-foreground text-sm">
{selectedWallet.label ?? "Unnamed server wallet"}
</span>
</div>
) : (
"Select server wallet"
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{serverWallets.map((wallet) => (
<SelectItem key={wallet.id} value={wallet.id}>
<div className="flex items-center gap-2">
<WalletAddress
address={wallet.address}
client={client}
/>
<span className="text-muted-foreground text-sm">
{wallet.label ?? "Unnamed server wallet"}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button
disabled={!selectedWallet || setDefaultWalletMutation.isPending}
onClick={() => {
if (selectedWallet) {
setDefaultWalletMutation.mutate(selectedWallet);
}
}}
type="button"
>
{setDefaultWalletMutation.isPending && (
<Spinner className="mr-2 size-4" />
)}
Set as project wallet
</Button>
<CreateServerWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
setAsProjectWallet
/>
</div>
</div>
) : (
<div className="space-y-3 text-sm text-muted-foreground">
<p>
Create a server wallet to start using transactions and other
project features.
</p>
<CreateServerWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
setAsProjectWallet
/>
</div>
)}
</div>
</Alert>
);
}
@@ -29,6 +29,7 @@ import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-fi
import { ProjectAvatar } from "@/components/blocks/avatar/project-avatar";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getProjectWallet } from "@/lib/server/project-wallet";
import { getFiltersFromSearchParams } from "@/lib/time";
import type {
InAppWalletStats,
@@ -38,7 +39,10 @@ import type {
import { loginRedirect } from "@/utils/redirects";
import { PieChartCard } from "../../../components/Analytics/PieChartCard";
import { EngineCloudChartCardAsync } from "./components/EngineCloudChartCard";
import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX";
import {
ProjectFTUX,
ProjectWalletSection,
} from "./components/ProjectFTUX/ProjectFTUX";
import { RpcMethodBarChartCardAsync } from "./components/RpcMethodBarChartCard";
import { TransactionsChartCardAsync } from "./components/Transactions";
import { ProjectHighlightsCard } from "./overview/highlights-card";
@@ -64,6 +68,9 @@ type PageProps = {
searchParams: Promise<PageSearchParams>;
};
// Revalidate this page data every 30 seconds
export const revalidate = 30;
export default async function ProjectOverviewPage(props: PageProps) {
const [params, searchParams] = await Promise.all([
props.params,
@@ -104,6 +111,11 @@ export default async function ProjectOverviewPage(props: PageProps) {
teamId: project.teamId,
});
const projectWallet = await getProjectWallet(project);
const managementAccessToken =
project.services?.find((service) => service.name === "engineCloud")
?.managementAccessToken ?? undefined;
return (
<ResponsiveSearchParamsProvider value={searchParams}>
<ProjectPage
@@ -125,6 +137,12 @@ export default async function ProjectOverviewPage(props: PageProps) {
>
{isActive ? (
<div className="flex flex-col gap-4 md:gap-6">
<ProjectWalletSection
project={project}
teamSlug={params.team_slug}
wallet={projectWallet}
managementAccessToken={managementAccessToken}
/>
<ResponsiveTimeFilters defaultRange={defaultRange} />
<ProjectAnalytics
authToken={authToken}
@@ -137,7 +155,12 @@ export default async function ProjectOverviewPage(props: PageProps) {
/>
</div>
) : (
<ProjectFTUX project={project} teamSlug={params.team_slug} />
<ProjectFTUX
project={project}
teamSlug={params.team_slug}
wallet={projectWallet}
managementAccessToken={managementAccessToken}
/>
)}
</ProjectPage>
</ResponsiveSearchParamsProvider>
@@ -15,6 +15,7 @@ export function QuickStartSection(props: {
projectSlug: string;
clientId: string;
teamId: string;
projectWalletAddress?: string;
}) {
return (
<section>
@@ -43,6 +44,7 @@ export function QuickStartSection(props: {
action={
<CreatePaymentLinkButton
clientId={props.clientId}
projectWalletAddress={props.projectWalletAddress}
teamId={props.teamId}
>
<Button
@@ -45,7 +45,11 @@ const formSchema = z.object({
});
export function CreatePaymentLinkButton(
props: PropsWithChildren<{ clientId: string; teamId: string }>,
props: PropsWithChildren<{
clientId: string;
teamId: string;
projectWalletAddress?: string;
}>,
) {
const [open, setOpen] = useState(false);
@@ -55,6 +59,7 @@ export function CreatePaymentLinkButton(
<DialogContent className="p-0 !max-w-lg">
<CreatePaymentLinkDialogContent
clientId={props.clientId}
projectWalletAddress={props.projectWalletAddress}
setOpen={setOpen}
teamId={props.teamId}
/>
@@ -67,6 +72,7 @@ function CreatePaymentLinkDialogContent(props: {
clientId: string;
teamId: string;
setOpen: (open: boolean) => void;
projectWalletAddress?: string;
}) {
const client = getClientThirdwebClient();
@@ -182,14 +188,41 @@ function CreatePaymentLinkDialogContent(props: {
render={({ field }) => (
<FormItem>
<FormLabel>Recipient Address</FormLabel>
<Input
className="w-full bg-card"
{...field}
onChange={field.onChange}
value={field.value}
placeholder="Address or ENS"
required
/>
<div className="flex flex-col gap-2 sm:flex-row">
<Input
className="w-full bg-card sm:flex-1"
{...field}
onChange={field.onChange}
value={field.value}
placeholder="Address or ENS"
required
/>
{props.projectWalletAddress && (
<Button
className="sm:w-auto"
onClick={() => {
if (!props.projectWalletAddress) {
return;
}
form.setValue(
"recipient",
props.projectWalletAddress,
{
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
},
);
}}
size="sm"
type="button"
variant="outline"
>
Use Project Wallet
</Button>
)}
</div>
<FormMessage />
</FormItem>
)}
@@ -40,7 +40,11 @@ import { ErrorState } from "../../components/ErrorState";
import { formatTokenAmount } from "../../components/format";
import { CreatePaymentLinkButton } from "./CreatePaymentLinkButton.client";
export function PaymentLinksTable(props: { clientId: string; teamId: string }) {
export function PaymentLinksTable(props: {
clientId: string;
teamId: string;
projectWalletAddress?: string;
}) {
return (
<section>
<div className="mb-4">
@@ -49,12 +53,20 @@ export function PaymentLinksTable(props: { clientId: string; teamId: string }) {
The payments you have created in this project
</p>
</div>
<PaymentLinksTableInner clientId={props.clientId} teamId={props.teamId} />
<PaymentLinksTableInner
clientId={props.clientId}
projectWalletAddress={props.projectWalletAddress}
teamId={props.teamId}
/>
</section>
);
}
function PaymentLinksTableInner(props: { clientId: string; teamId: string }) {
function PaymentLinksTableInner(props: {
clientId: string;
teamId: string;
projectWalletAddress?: string;
}) {
const paymentLinksQuery = useQuery({
queryFn: async () => {
return getPaymentLinks({
@@ -120,6 +132,7 @@ function PaymentLinksTableInner(props: { clientId: string; teamId: string }) {
<CreatePaymentLinkButton
key="create-payment-link"
clientId={props.clientId}
projectWalletAddress={props.projectWalletAddress}
teamId={props.teamId}
>
<Button className="gap-2 rounded-full" variant="default" size="sm">
@@ -6,6 +6,7 @@ import { getProject } from "@/api/project/projects";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { PayIcon } from "@/icons/PayIcon";
import { getProjectWallet } from "@/lib/server/project-wallet";
import { loginRedirect } from "@/utils/redirects";
import { AdvancedSection } from "./components/AdvancedSection.client";
import { QuickStartSection } from "./components/QuickstartSection.client";
@@ -29,6 +30,8 @@ export default async function Page(props: {
redirect(`/team/${params.team_slug}`);
}
const projectWallet = await getProjectWallet(project);
const client = getClientThirdwebClient({
jwt: authToken,
teamId: project.teamId,
@@ -51,6 +54,7 @@ export default async function Page(props: {
component: (
<CreatePaymentLinkButton
clientId={project.publishableKey}
projectWalletAddress={projectWallet?.address}
teamId={project.teamId}
>
<Button className="gap-1.5 rounded-full" size="sm">
@@ -141,12 +145,14 @@ export default async function Page(props: {
<div className="flex flex-col gap-12">
<PaymentLinksTable
clientId={project.publishableKey}
projectWalletAddress={projectWallet?.address}
teamId={project.teamId}
/>
<QuickStartSection
projectSlug={params.project_slug}
teamSlug={params.team_slug}
clientId={project.publishableKey}
projectWalletAddress={projectWallet?.address}
teamId={project.teamId}
/>
@@ -7,6 +7,7 @@ import { toast } from "sonner";
import type { Project } from "@/api/project/projects";
import { type Fee, updateFee } from "@/api/universal-bridge/developer";
import { SettingsCard } from "@/components/blocks/SettingsCard";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
@@ -25,6 +26,7 @@ interface PayConfigProps {
teamId: string;
teamSlug: string;
fees: Fee;
projectWalletAddress?: string;
}
export const PayConfig: React.FC<PayConfigProps> = (props) => {
@@ -103,7 +105,37 @@ export const PayConfig: React.FC<PayConfigProps> = (props) => {
<FormItem>
<FormLabel>Recipient address</FormLabel>
<FormControl>
<Input {...field} placeholder="0x..." />
<div className="flex flex-col gap-2 sm:flex-row">
<Input
{...field}
className="sm:flex-1"
placeholder="0x..."
/>
{props.projectWalletAddress && (
<Button
onClick={() => {
if (!props.projectWalletAddress) {
return;
}
form.setValue(
"payoutAddress",
props.projectWalletAddress,
{
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
},
);
}}
size="sm"
type="button"
variant="outline"
>
Use Project Wallet
</Button>
)}
</div>
</FormControl>
</FormItem>
)}
@@ -3,6 +3,7 @@ import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { getTeamBySlug } from "@/api/team/get-team";
import { getFees } from "@/api/universal-bridge/developer";
import { getProjectWallet } from "@/lib/server/project-wallet";
import { loginRedirect } from "@/utils/redirects";
import { ProjectSettingsBreadcrumb } from "../_components/project-settings-breadcrumb";
import { PayConfig } from "./PayConfig";
@@ -31,6 +32,8 @@ export default async function Page(props: {
redirect(`/team/${team_slug}`);
}
const projectWallet = await getProjectWallet(project);
let fees = await getFees({
clientId: project.publishableKey,
teamId: team.id,
@@ -64,6 +67,7 @@ export default async function Page(props: {
fees={fees}
project={project}
teamId={team.id}
projectWalletAddress={projectWallet?.address}
teamSlug={team_slug}
/>
</div>
@@ -143,6 +143,7 @@ export function CreateNFTPage(props: {
contractName: contractType,
deploymentType: "asset",
publisher: "deployer.thirdweb.eth",
is_testnet: chain.testnet,
});
contractAddressRef.current = contractAddress;
@@ -316,6 +316,8 @@ export function LaunchNFT(props: {
contractType: ercType === "erc721" ? "DropERC721" : "DropERC1155",
error: errorMessage,
step: currentStep.id,
is_testnet: chainMeta.testnet,
chainId: Number(formValues.collectionInfo.chain),
});
throw error;
@@ -325,6 +327,8 @@ export function LaunchNFT(props: {
reportAssetCreationSuccessful({
assetType: "nft",
contractType: ercType === "erc721" ? "DropERC721" : "DropERC1155",
chainId: Number(formValues.collectionInfo.chain),
is_testnet: chainMeta?.testnet ?? false,
});
props.onLaunchSuccess();
@@ -178,12 +178,14 @@ export function CreateTokenAssetPage(props: {
teamId: props.teamId,
});
const chainMetadata = getChain(Number(params.values.chain));
reportContractDeployed({
address: contractAddress,
chainId: Number(params.values.chain),
contractName: "DropERC20",
contractName: "ERC20Asset",
deploymentType: "asset",
publisher: account.address,
is_testnet: chainMetadata.testnet,
});
contractAddressRef.current = contractAddress;
@@ -299,12 +301,15 @@ export function CreateTokenAssetPage(props: {
teamId: props.teamId,
});
const chainMetadata = getChain(Number(values.chain));
reportContractDeployed({
address: contractAddress,
chainId: Number(values.chain),
contractName: "DropERC20",
deploymentType: "asset",
publisher: "deployer.thirdweb.eth",
is_testnet: chainMetadata.testnet,
});
return {
@@ -230,6 +230,8 @@ export function LaunchTokenStatus(props: {
: "ERC20Asset",
error: errorMessage,
step: currentStep.id,
is_testnet: chainMetadata?.testnet,
chainId: Number(formValues.chain),
});
updateStatus(i, {
@@ -247,6 +249,8 @@ export function LaunchTokenStatus(props: {
formValues.saleMode === "drop-erc20:token-drop"
? "DropERC20"
: "ERC20Asset",
chainId: Number(formValues.chain),
is_testnet: chainMetadata?.testnet,
});
if (contractAddressRef.current) {
@@ -3,14 +3,17 @@
import { encrypt } from "@thirdweb-dev/service-utils";
import {
createAccessToken,
createEoa,
createServiceAccount,
createVaultClient,
rotateServiceAccount,
type VaultClient,
} from "@thirdweb-dev/vault-sdk";
import { engineCloudProxy } from "@/actions/proxies";
import type { Project } from "@/api/project/projects";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
import { updateProjectClient } from "@/hooks/useApi";
import { getProjectWalletLabel } from "@/lib/project-wallet";
const SERVER_WALLET_ACCESS_TOKEN_PURPOSE =
"Access Token for All Server Wallets";
@@ -126,6 +129,92 @@ export async function createVaultAccountAndAccessToken(props: {
}
}
export async function createProjectServerWallet(props: {
project: Project;
managementAccessToken: string;
label?: string;
setAsProjectWallet?: boolean;
}) {
const vaultClient = await initVaultClient();
const walletLabel = props.label?.trim()
? props.label.trim()
: getProjectWalletLabel(props.project.name);
const eoa = await createEoa({
client: vaultClient,
request: {
auth: {
accessToken: props.managementAccessToken,
},
options: {
metadata: {
label: walletLabel,
projectId: props.project.id,
teamId: props.project.teamId,
type: "server-wallet",
},
},
},
});
if (!eoa.success) {
throw new Error(eoa.error?.message || "Failed to create server wallet");
}
engineCloudProxy({
body: JSON.stringify({
signerAddress: eoa.data.address,
}),
headers: {
"Content-Type": "application/json",
"x-client-id": props.project.publishableKey,
"x-team-id": props.project.teamId,
},
method: "POST",
pathname: "/cache/smart-account",
}).catch((err) => {
console.warn("failed to cache server wallet", err);
});
if (props.setAsProjectWallet) {
await updateDefaultProjectWallet({
project: props.project,
projectWalletAddress: eoa.data.address,
});
}
return eoa.data;
}
export async function updateDefaultProjectWallet(props: {
project: Project;
projectWalletAddress: string;
}) {
const services = props.project.services.filter(
(service) => service.name !== "engineCloud",
);
const engineCloudService = props.project.services.find(
(service) => service.name === "engineCloud",
);
if (engineCloudService) {
const engineCloudServiceWithProjectWallet = {
...engineCloudService,
projectWalletAddress: props.projectWalletAddress,
};
await updateProjectClient(
{
projectId: props.project.id,
teamId: props.project.teamId,
},
{
services: [...services, engineCloudServiceWithProjectWallet],
},
);
}
}
async function createAndEncryptVaultAccessTokens(props: {
project: Project;
vaultClient: VaultClient;
@@ -157,6 +246,13 @@ async function createAndEncryptVaultAccessTokens(props: {
const managementToken = managementTokenResult.data;
const walletToken = walletTokenResult.data;
// create a default project server wallet
const defaultProjectServerWallet = await createProjectServerWallet({
project,
managementAccessToken: managementToken.accessToken,
label: getProjectWalletLabel(project.name),
});
if (projectSecretKey) {
// verify that the project secret key is valid
const projectSecretKeyHash = await hashSecretKey(projectSecretKey);
@@ -182,7 +278,9 @@ async function createAndEncryptVaultAccessTokens(props: {
},
{
services: [
...props.project.services,
...props.project.services.filter(
(service) => service.name !== "engineCloud",
),
{
name: "engineCloud",
actions: [],
@@ -191,6 +289,7 @@ async function createAndEncryptVaultAccessTokens(props: {
encryptedAdminKey,
encryptedWalletAccessToken,
rotationCode: rotationCode,
projectWalletAddress: defaultProjectServerWallet.address,
},
],
},
@@ -204,7 +303,9 @@ async function createAndEncryptVaultAccessTokens(props: {
},
{
services: [
...props.project.services,
...props.project.services.filter(
(service) => service.name !== "engineCloud",
),
{
name: "engineCloud",
actions: [],
@@ -213,6 +314,7 @@ async function createAndEncryptVaultAccessTokens(props: {
encryptedAdminKey: null,
encryptedWalletAccessToken: null,
rotationCode: rotationCode,
projectWalletAddress: defaultProjectServerWallet.address,
},
],
},
@@ -1,10 +1,8 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { createEoa } from "@thirdweb-dev/vault-sdk";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { engineCloudProxy } from "@/actions/proxies";
import type { Project } from "@/api/project/projects";
import { Button } from "@/components/ui/button";
import {
@@ -17,12 +15,13 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/Spinner";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { initVaultClient } from "../../lib/vault.client";
import { createProjectServerWallet } from "../../lib/vault.client";
export default function CreateServerWallet(props: {
project: Project;
teamSlug: string;
managementAccessToken: string | undefined;
setAsProjectWallet?: boolean;
}) {
const router = useDashboardRouter();
const [label, setLabel] = useState("");
@@ -36,49 +35,17 @@ export default function CreateServerWallet(props: {
managementAccessToken: string;
label: string;
}) => {
const vaultClient = await initVaultClient();
const eoa = await createEoa({
client: vaultClient,
request: {
auth: {
accessToken: managementAccessToken,
},
options: {
metadata: {
label,
projectId: props.project.id,
teamId: props.project.teamId,
type: "server-wallet",
},
},
},
});
if (!eoa.success) {
throw new Error("Failed to create eoa");
}
// no need to await this, it's not blocking
engineCloudProxy({
body: JSON.stringify({
signerAddress: eoa.data.address,
}),
headers: {
"Content-Type": "application/json",
"x-client-id": props.project.publishableKey,
"x-team-id": props.project.teamId,
},
method: "POST",
pathname: "/cache/smart-account",
}).catch((err) => {
console.warn("failed to cache server wallet", err);
const wallet = await createProjectServerWallet({
label,
managementAccessToken,
project: props.project,
setAsProjectWallet: props.setAsProjectWallet,
});
router.refresh();
setModalOpen(false);
return eoa;
setLabel("");
return wallet;
},
onError: (error) => {
toast.error(error.message);
@@ -1,8 +1,9 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { format, formatDistanceToNowStrict } from "date-fns";
import {
CheckIcon,
MoreVerticalIcon,
RefreshCcwIcon,
SendIcon,
@@ -10,6 +11,7 @@ import {
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import { useWalletBalance } from "thirdweb/react";
import {
@@ -20,6 +22,7 @@ import type { Project } from "@/api/project/projects";
import { FundWalletModal } from "@/components/blocks/fund-wallets-modal";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -50,7 +53,9 @@ import {
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import { updateDefaultProjectWallet } from "../../lib/vault.client";
import CreateServerWallet from "../components/create-server-wallet.client";
import type { Wallet } from "./types";
@@ -287,18 +292,34 @@ function ServerWalletTableRow(props: {
queryKey: ["smart-account-address", wallet.address, chainId],
});
// Get the project wallet address
const engineCloudService = project.services.find(
(service) => service.name === "engineCloud",
);
const defaultWalletAddress = engineCloudService?.projectWalletAddress;
const isDefaultWallet =
defaultWalletAddress &&
wallet.address.toLowerCase() === defaultWalletAddress.toLowerCase();
return (
<TableRow key={wallet.id}>
{/* Label */}
<TableCell>
<span
className={cn(
"text-sm text-foreground",
!wallet.metadata.label && "text-muted-foreground",
<div className="flex items-center gap-2">
<span
className={cn(
"text-sm text-foreground",
!wallet.metadata.label && "text-muted-foreground",
)}
>
{wallet.metadata.label || "N/A"}
</span>
{isDefaultWallet && (
<Badge variant="success" className="text-xs">
default
</Badge>
)}
>
{wallet.metadata.label || "N/A"}
</span>
</div>
</TableCell>
{/* Address */}
@@ -391,6 +412,25 @@ function WalletActionsDropdown(props: {
chainId: number;
}) {
const [showFundModal, setShowFundModal] = useState(false);
const router = useDashboardRouter();
const setAsDefaultMutation = useMutation({
mutationFn: async () => {
await updateDefaultProjectWallet({
project: props.project,
projectWalletAddress: props.wallet.address,
});
},
onSuccess: () => {
toast.success("Wallet set as project wallet");
router.refresh();
},
onError: (error) => {
toast.error(
error instanceof Error ? error.message : "Failed to set default wallet",
);
},
});
return (
<>
@@ -420,6 +460,14 @@ function WalletActionsDropdown(props: {
Fund wallet
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => setAsDefaultMutation.mutate()}
disabled={setAsDefaultMutation.isPending}
className="flex items-center gap-2 h-9 rounded-lg"
>
<CheckIcon className="size-4 text-muted-foreground" />
Set as default
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -21,6 +21,7 @@ export function UniversalBridgeEmbed({
buyAmount={amount}
tokenAddress={token?.address}
pageType="bridge"
isTestnet={chain.testnet}
/>
);
}
+3 -4
View File
@@ -14,7 +14,7 @@ import { bridgeAppThirdwebClient } from "./constants";
const title = "thirdweb Bridge: Buy, Bridge & Swap Crypto on 85+ Chains";
const description =
"Bridge, swap, and on-ramp across 9M+ token routes on 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing and near-instant fast finality";
"Bridge and swap 4500+ tokens across 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality";
export const metadata: Metadata = {
description,
@@ -73,7 +73,7 @@ export default async function BridgePage({
<div className="flex-wrap flex gap-3 items-center justify-center">
<DataPill>85+ Chains Supported</DataPill>
<DataPill>3200+ Tokens Supported</DataPill>
<DataPill>4500+ Tokens Supported</DataPill>
<DataPill>9+ Million Routes Available</DataPill>
</div>
</div>
@@ -105,8 +105,7 @@ export default async function BridgePage({
href="https://portal.thirdweb.com/bridge"
linkType="integrate-bridge"
>
Integrate Bridge in your apps in minutes, and start generating
revenue
Accept Payments in Any Token Right Inside Your App
</PillLink>
<PillLink
+124 -117
View File
@@ -1,5 +1,6 @@
import { ShieldCheckIcon } from "lucide-react";
import type { Metadata } from "next";
import { ThemeProvider } from "next-themes";
import { Bridge, defineChain, toTokens } from "thirdweb";
import { getChainMetadata } from "thirdweb/chains";
import { shortenAddress } from "thirdweb/utils";
@@ -80,131 +81,137 @@ export default async function PayPage({
}
return (
<div className="flex z-10 flex-col lg:flex-row h-full w-full">
<header className="min-w-full lg:min-w-[500px] border-b lg:border-r lg:h-full bg-card flex flex-col gap-4 items-start p-4 lg:p-8">
<div>
<div className="flex flex-row items-center justify-start gap-4">
{projectMetadata.image && (
<img
src={
resolveSchemeWithErrorHandler({
uri: projectMetadata.image,
client: payAppThirdwebClient,
}) || ""
}
alt={projectMetadata.name}
width={25}
height={25}
className="rounded-full overflow-hidden"
/>
<ThemeProvider
forcedTheme={theme === "light" ? "light" : "dark"}
attribute="class"
disableTransitionOnChange
enableSystem={false}
>
<div className="flex z-10 flex-col lg:flex-row h-full w-full">
<header className="min-w-full lg:min-w-[500px] border-b lg:border-r lg:h-full bg-card flex flex-col gap-4 items-start p-4 lg:p-8">
<div>
<div className="flex flex-row items-center justify-start gap-4">
{projectMetadata.image && (
<img
src={
resolveSchemeWithErrorHandler({
uri: projectMetadata.image,
client: payAppThirdwebClient,
}) || ""
}
alt={projectMetadata.name}
width={25}
height={25}
className="rounded-full overflow-hidden"
/>
)}
<h2 className="text-xl font-bold">{projectMetadata.name}</h2>
</div>
{projectMetadata.description && (
<p className="mt-2 text-sm text-muted-foreground">
{projectMetadata.description}
</p>
)}
<h2 className="text-xl font-bold">{projectMetadata.name}</h2>
</div>
{projectMetadata.description && (
<p className="mt-2 text-sm text-muted-foreground">
{projectMetadata.description}
</p>
)}
</div>
<div className="hidden lg:block my-4 w-full">
{paymentLink.amount && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">Details</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
<div className="flex flex-row items-center gap-2">
{token.iconUri && (
<img
src={resolveSchemeWithErrorHandler({
uri: token.iconUri,
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
})}
alt={token.name}
width={25}
height={25}
className="size-5 rounded-full overflow-hidden"
/>
)}
{toTokens(BigInt(paymentLink.amount), token.decimals)}{" "}
{token.symbol}
</div>
{token.prices.USD && (
<span>
$
{(
Number(token.prices.USD) *
Number(
toTokens(BigInt(paymentLink.amount), token.decimals),
)
).toFixed(2)}
</span>
)}
</div>
</div>
)}
{chain && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">Network</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
<div className="flex flex-row items-center gap-2">
{chain.icon?.url && (
<img
src={resolveSchemeWithErrorHandler({
uri: chain.icon.url,
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
})}
alt={chain.name}
width={chain.icon.width}
height={chain.icon.height}
className="size-5 rounded-full overflow-hidden"
/>
)}
{chain.name}
</div>
</div>
</div>
)}
{recipientEnsOrAddress.ensName ||
(recipientEnsOrAddress.address && (
<div className="hidden lg:block my-4 w-full">
{paymentLink.amount && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">Seller</span>
<span className="text-muted-foreground text-xs">Details</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
{recipientEnsOrAddress.ensName ??
shortenAddress(recipientEnsOrAddress.address)}
<div className="flex flex-row items-center gap-2">
{token.iconUri && (
<img
src={resolveSchemeWithErrorHandler({
uri: token.iconUri,
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
})}
alt={token.name}
width={25}
height={25}
className="size-5 rounded-full overflow-hidden"
/>
)}
{toTokens(BigInt(paymentLink.amount), token.decimals)}{" "}
{token.symbol}
</div>
{token.prices.USD && (
<span>
$
{(
Number(token.prices.USD) *
Number(
toTokens(BigInt(paymentLink.amount), token.decimals),
)
).toFixed(2)}
</span>
)}
</div>
</div>
))}
</div>
)}
{chain && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">Network</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
<div className="flex flex-row items-center gap-2">
{chain.icon?.url && (
<img
src={resolveSchemeWithErrorHandler({
uri: chain.icon.url,
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
})}
alt={chain.name}
width={chain.icon.width}
height={chain.icon.height}
className="size-5 rounded-full overflow-hidden"
/>
)}
{chain.name}
</div>
</div>
</div>
)}
{recipientEnsOrAddress.ensName ||
(recipientEnsOrAddress.address && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">Seller</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
{recipientEnsOrAddress.ensName ??
shortenAddress(recipientEnsOrAddress.address)}
</div>
</div>
))}
</div>
<div className="mt-auto hidden lg:block">
<Badge className="flex items-center gap-1.5 bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800">
<ShieldCheckIcon className="size-3" />
Secured by thirdweb
</Badge>
</div>
</header>
<main className="flex justify-center p-12 w-full items-center">
<PayPageWidget
amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
chainId={Number(paymentLink.destinationToken.chainId)}
clientId={undefined} // Payment links don't need to use the same client ID to be executed
image={paymentLink.imageUrl}
name={paymentLink.title}
paymentLinkId={id}
purchaseData={paymentLink.purchaseData}
recipientAddress={paymentLink.receiver}
redirectUri={redirectUri}
theme={theme}
token={token}
/>
</main>
</div>
<div className="mt-auto hidden lg:block">
<Badge className="flex items-center gap-1.5 bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800">
<ShieldCheckIcon className="size-3" />
Secured by thirdweb
</Badge>
</div>
</header>
<main className="flex justify-center p-12 w-full items-center">
<PayPageWidget
amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
chainId={Number(paymentLink.destinationToken.chainId)}
clientId={undefined} // Payment links don't need to use the same client ID to be executed
image={paymentLink.imageUrl}
name={paymentLink.title}
paymentLinkId={id}
purchaseData={paymentLink.purchaseData}
recipientAddress={paymentLink.receiver}
redirectUri={redirectUri}
token={token}
/>
</main>
</div>
</ThemeProvider>
);
}
@@ -1,7 +1,6 @@
"use client";
import { payAppThirdwebClient } from "app/pay/constants";
import { useTheme } from "next-themes";
import { useEffect } from "react";
import { createThirdwebClient, NATIVE_TOKEN_ADDRESS, toTokens } from "thirdweb";
import { AutoConnect, CheckoutWidget } from "thirdweb/react";
import { checksumAddress } from "thirdweb/utils";
@@ -11,6 +10,7 @@ import {
reportPaymentLinkBuySuccessful,
} from "@/analytics/report";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { getSDKTheme } from "@/utils/sdk-component-theme";
export function PayPageWidget({
chainId,
@@ -21,7 +21,6 @@ export function PayPageWidget({
name,
image,
redirectUri,
theme,
purchaseData,
clientId,
}: {
@@ -34,17 +33,10 @@ export function PayPageWidget({
image?: string;
redirectUri?: string;
clientId: string | undefined;
theme?: "light" | "dark";
purchaseData: Record<string, unknown> | undefined;
}) {
const { theme: browserTheme, setTheme } = useTheme();
const { theme } = useTheme();
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (theme) {
setTheme(theme);
}
}, [theme, setTheme]);
const chain = useV5DashboardChain(chainId);
return (
@@ -55,6 +47,7 @@ export function PayPageWidget({
}
/>
<CheckoutWidget
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
connectOptions={{
wallets: [
createWallet("io.metamask"),
@@ -87,7 +80,6 @@ export function PayPageWidget({
paymentLinkId={paymentLinkId}
purchaseData={purchaseData}
seller={checksumAddress(recipientAddress)}
theme={theme ?? (browserTheme === "light" ? "light" : "dark")}
tokenAddress={
token.address === NATIVE_TOKEN_ADDRESS
? undefined
+3 -11
View File
@@ -1,6 +1,5 @@
import "@workspace/ui/global.css";
import { Inter } from "next/font/google";
import { ThemeProvider } from "next-themes";
import { cn } from "@/lib/utils";
import { PayProviders } from "./components/client/Providers.client";
@@ -24,16 +23,9 @@ export default async function PayLayout({
)}
>
<PayProviders>
<ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
enableSystem={false}
>
<div className="relative mx-auto flex h-full w-full items-center justify-center overflow-x-hidden overflow-y-scroll">
{children}
</div>
</ThemeProvider>
<div className="relative mx-auto flex h-full w-full items-center justify-center overflow-x-hidden overflow-y-scroll">
{children}
</div>
</PayProviders>
</body>
</html>
+29 -13
View File
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { ThemeProvider } from "next-themes";
import { createThirdwebClient, defineChain, getContract } from "thirdweb";
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
import { checksumAddress } from "thirdweb/utils";
@@ -33,7 +34,16 @@ export default async function PayPage({
!params.tokenAddress &&
!params.amount
) {
return <PaymentLinkForm />;
return (
<ThemeProvider
forcedTheme={params.theme === "light" ? "light" : "dark"}
attribute="class"
disableTransitionOnChange
enableSystem={false}
>
<PaymentLinkForm />
</ThemeProvider>
);
}
// Validate query parameters
@@ -84,17 +94,23 @@ export default async function PayPage({
};
return (
<PayPageWidget
amount={BigInt(params.amount)}
chainId={Number(params.chainId)}
clientId={client.clientId}
image={params.image}
name={params.name}
purchaseData={undefined}
recipientAddress={params.recipientAddress}
redirectUri={params.redirectUri}
theme={params.theme}
token={token}
/>
<ThemeProvider
forcedTheme={params.theme === "light" ? "light" : "dark"}
attribute="class"
disableTransitionOnChange
enableSystem={false}
>
<PayPageWidget
amount={BigInt(params.amount)}
chainId={Number(params.chainId)}
clientId={client.clientId}
image={params.image}
name={params.name}
purchaseData={undefined}
recipientAddress={params.recipientAddress}
redirectUri={params.redirectUri}
token={token}
/>
</ThemeProvider>
);
}
@@ -76,7 +76,7 @@ export function ChatContainer() {
return (
<Reasoning
key={`${message.id}-reasoning-${i}`}
className="w-full"
className="w-full max-w-md"
isStreaming={status === "streaming"}
>
<ReasoningTrigger />
@@ -7,24 +7,25 @@ import { token } from "../../payments/x402/components/constants";
// Allow streaming responses up to 5 minutes
export const maxDuration = 300;
const client = createThirdwebClient({
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
});
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string;
const ENGINE_VAULT_ACCESS_TOKEN = process.env
.ENGINE_VAULT_ACCESS_TOKEN as string;
// const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
const API_URL = "http://localhost:3030";
const twFacilitator = facilitator({
baseUrl: `${API_URL}/v1/payments/x402`,
client,
serverWalletAddress: BACKEND_WALLET_ADDRESS,
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
});
export async function GET(request: NextRequest) {
const client = createThirdwebClient({
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
});
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string;
const ENGINE_VAULT_ACCESS_TOKEN = process.env
.ENGINE_VAULT_ACCESS_TOKEN as string;
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
const twFacilitator = facilitator({
baseUrl: `${API_URL}/v1/payments/x402`,
client,
serverWalletAddress: BACKEND_WALLET_ADDRESS,
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
});
const paymentData = request.headers.get("X-PAYMENT");
const queryParams = request.nextUrl.searchParams;
@@ -38,6 +39,7 @@ export async function GET(request: NextRequest) {
}
const amount = queryParams.get("amount") || "0.01";
const payTo = queryParams.get("payTo") ?? undefined;
const tokenAddress = queryParams.get("tokenAddress") || token.address;
const decimals = queryParams.get("decimals") || token.decimals.toString();
const waitUntil =
@@ -49,6 +51,7 @@ export async function GET(request: NextRequest) {
method: "GET",
paymentData,
network: defineChain(Number(chainId)),
payTo,
price: {
amount: toUnits(amount, parseInt(decimals)).toString(),
asset: {
@@ -45,6 +45,7 @@ export function X402LeftSection(props: {
const tokenId = useId();
const amountId = useId();
const waitUntilId = useId();
const payToId = useId();
const handleChainChange = (chainId: number) => {
setSelectedChain(chainId);
@@ -81,6 +82,13 @@ export function X402LeftSection(props: {
}));
};
const handlePayToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setOptions((v) => ({
...v,
payTo: e.target.value as `0x${string}`,
}));
};
const handleWaitUntilChange = (
value: "simulated" | "submitted" | "confirmed",
) => {
@@ -140,6 +148,22 @@ export function X402LeftSection(props: {
)}
</div>
{/* Pay To input */}
<div className="flex flex-col gap-2">
<Label htmlFor={payToId}>Pay To Address</Label>
<Input
id={payToId}
type="text"
placeholder="0x..."
value={options.payTo}
onChange={handlePayToChange}
className="bg-card"
/>
<p className="text-sm text-muted-foreground">
The wallet address that will receive the payment
</p>
</div>
{/* Wait Until selection */}
<div className="flex flex-col gap-2">
<Label htmlFor={waitUntilId}>Wait Until</Label>
+4
View File
@@ -738,6 +738,10 @@ const paymentRedirects = {
"/connect/pay/buy-with-fiat": "/payments",
"/connect/pay/enable-test-mode": "/payments",
"/connect/pay/guides/enable-test-mode": "/payments",
"/payments/sell": "/bridge/sell",
"/payments/swap": "/bridge/swap",
"/payments/tokens": "/bridge/tokens",
"/payments/routes": "/bridge/routes",
};
const contractRedirects = {
+113 -25
View File
@@ -17,6 +17,76 @@ https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>
Make sure to keep your secret key safe and never share it with anyone.
By default, all tools are available. You can filter the tools available by passing a comma-separated list of tools as a query parameter.
```http
https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>&tools=fetchWithPayment,getWalletBalance
```
You can find all available tools at the bottom of this page.
## Usage with agents
Use your favorite agent framework to plug in the MCP server as a collection of tools for your agent. Refer to your agent framework's documentation for more information.
#### Example usage with langchain:
<Tabs defaultValue="typescript">
<TabsList>
<TabsTrigger value="typescript">TypeScript</TabsTrigger>
<TabsTrigger value="python">Python</TabsTrigger>
</TabsList>
<TabsContent value="typescript">
```typescript
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
const mcpServers = {
thirdweb: {
url: `https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>`
}
};
// Connect to MCP servers and get tools
const client = new MultiServerMCPClient(mcpServers);
const tools = await client.getTools();
const agent = createReactAgent({
llm: model,
tools: tools,
});
await agent.invoke({messages: [{ role: "user", content: "create a server wallet called 'my-wallet'" }]})
```
</TabsContent>
<TabsContent value="python">
```python
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
client = MultiServerMCPClient(
{
"thirdweb": {
"url": "https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>",
}
}
)
tools = await client.get_tools()
agent = create_react_agent("openai:gpt-4.1", tools)
response = await agent.invoke({"messages": "create a server wallet called 'my-wallet'"})
```
</TabsContent>
</Tabs>
Once installed, you can use the entire thirdweb API with natural language.
## Usage with LLM clients
You can also use the MCP server on your own LLM client, like cursor, claude code and more. Refer to your LLM client's documentation for more information.
@@ -112,31 +182,6 @@ claude mcp add --transport http "thirdweb-api" "https://api.thirdweb.com/mcp?sec
</Tabs>
## Usage with agents
Use your favorite agent framework to plug in the MCP server as a collection of tools for your agent. Refer to your agent framework's documentation for more information.
#### Example usage with langchain:
```python
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
client = MultiServerMCPClient(
{
"thirdweb-api": {
"transport": "streamable_http",
"url": "https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>",
}
}
)
tools = await client.get_tools()
agent = create_react_agent("openai:gpt-4.1", tools)
response = await agent.ainvoke({"messages": "create a server wallet called 'my-wallet'"})
```
Once installed, you can use the entire thirdweb API with natural language.
## Example prompts
#### Managing server wallets
@@ -164,3 +209,46 @@ List my contracts
```
approve 100 USDC from treasury wallet to executor wallet
```
## Available tools
Currently, the following tools are available:
- `initiateAuthentication`
- `completeAuthentication`
- `linkAuthentication`
- `unlinkAuthentication`
- `getMyWallet`
- `listUserWallets`
- `createUserWallet`
- `listServerWallets`
- `createServerWallet`
- `getWalletBalance`
- `getWalletTransactions`
- `getWalletTokens`
- `getWalletNFTs`
- `signMessage`
- `signTypedData`
- `sendTokens`
- `listContracts`
- `deployContract`
- `readContract`
- `writeContract`
- `getContractTransactions`
- `getContractEvents`
- `getContractMetadata`
- `getContractSignatures`
- `getTransactionById`
- `listTransactions`
- `sendTransactions`
- `createPayment`
- `paymentsPurchase`
- `getPaymentHistory`
- `fetchWithPayment`
- `createToken`
- `listTokens`
- `getTokenOwners`
- `getBridgeChains`
- `convertFiatToCrypto`
- `bridgeSwap`
- `chat`
@@ -53,10 +53,6 @@ The BridgeWidget Script makes it easy to embed cross-chain swaps and fiat onramp
BridgeWidget.render(container, {
clientId: "your-thirdweb-client-id",
theme: "dark",
buy: {
chainId: 8453,
amount: "0.1",
},
});
</script>
```
@@ -126,9 +122,9 @@ type Options = {
};
};
};
buy: {
amount: string;
chainId: number;
buy?: {
amount?: string;
chainId?: number;
tokenAddress?: string;
buttonLabel?: string;
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
@@ -310,8 +306,8 @@ The options to configure the "Buy" tab UI.
```ts
type Buy = {
amount: string; // Required
chainId: number; // Required
amount?: string;
chainId?: number;
tokenAddress?: string;
buttonLabel?: string;
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
@@ -327,21 +323,21 @@ type Buy = {
```
<Details summary="amount" tags={["Required"]}>
<Details summary="amount">
Set the default token amount shown in the "Buy" tab.
</Details>
<Details summary="chainId" tags={["Required"]}>
<Details summary="chainId">
Specify which chain should be selected in the "Buy" tab.
Specify the default chain to be selected in the "Buy" tab.
</Details>
<Details summary="tokenAddress">
Specify which token should be selected in the "Buy" tab. If not specified, the native token of the selected chain will be selected.
Specify the default token to be selected in the "Buy" tab. If not specified, the native token of the selected chain will be selected.
</Details>
@@ -1,6 +1,16 @@
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@doc";
# Session Keys Guide
Session keys enable secure transaction execution on behalf of smart accounts without requiring direct access to the main account's private key. This guide will walk you through creating and using session keys with the thirdweb TypeScript SDK.
Session keys enable secure transaction execution on behalf of accounts without requiring direct access to the main account's private key. This guide covers session keys for both ERC-4337 smart accounts and ERC-7702 smart EOAs (Externally Owned Accounts).
<Tabs defaultValue="erc4337">
<TabsList>
<TabsTrigger value="erc4337">ERC-4337 Smart Accounts</TabsTrigger>
<TabsTrigger value="erc7702">ERC-7702 Smart EOAs</TabsTrigger>
</TabsList>
<TabsContent value="erc4337">
## Prerequisites
@@ -323,8 +333,409 @@ try {
}
```
</TabsContent>
<TabsContent value="erc7702">
## Prerequisites
Before you begin, ensure you have:
- A thirdweb client configured
- An in-app wallet with ERC-7702 support
- A session key account address (can be generated or an existing wallet)
## Setup
First, let's set up the necessary imports and configuration:
```typescript
import {
createThirdwebClient,
getContract,
sendTransaction,
prepareTransaction
} from "thirdweb";
import { sepolia } from "thirdweb/chains";
import { inAppWallet, createSessionKey } from "thirdweb/wallets/in-app";
// Configure your client
const client = createThirdwebClient({
clientId: "your-client-id",
secretKey: "your-secret-key" // Only use in server environments
});
// Your session key account address
const sessionKeyAccountAddress = "0x..."; // Replace with your session key address
```
## Step 1: Create ERC-7702 Smart EOA
Create an in-app wallet configured with ERC-7702 execution mode:
```typescript
const wallet = inAppWallet({
executionMode: {
mode: "EIP7702",
sponsorGas: true, // Enable gas sponsorship
},
});
// Connect the wallet (user authentication)
const account = await wallet.connect({
chain: sepolia,
client: client,
strategy: "google", // or "email", "guest", etc.
});
console.log("Smart EOA address:", account.address);
```
### Execution Modes
The `executionMode` option allows you to configure the wallet behavior:
- `mode`: Set to `"EIP7702"` for ERC-7702 smart EOA functionality
- `sponsorGas`: Enable gas sponsorship for gasless transactions
## Step 2: Deploy/Upgrade the Account
Send a transaction to trigger the account deployment or upgrade to ERC-7702:
```typescript
await sendTransaction({
account: account,
transaction: prepareTransaction({
chain: sepolia,
client: client,
to: account.address,
value: 0n, // Self-transfer to trigger deployment
}),
});
console.log("Smart EOA deployed/upgraded successfully");
```
## Step 3: Get Account Contract
Create a contract instance for the deployed smart EOA:
```typescript
const accountContract = getContract({
address: account.address,
chain: sepolia,
client: client,
// ABI will be automatically resolved since the contract is deployed
});
```
## Step 4: Create Session Key with Full Permissions
Add a session key with full execution permissions:
```typescript
const tx = await sendTransaction({
account: account,
transaction: createSessionKey({
account: account,
contract: accountContract,
sessionKeyAddress: sessionKeyAccountAddress,
durationInSeconds: 86400, // 1 day
grantFullPermissions: true, // Allow unrestricted access
}),
});
console.log("Session key created:", tx.transactionHash);
```
### Session Key Options
- `account`: The admin account that will create the session key
- `contract`: The smart EOA contract instance
- `sessionKeyAddress`: The address that will be granted session key permissions
- `durationInSeconds`: How long the session key should be valid (in seconds)
- `grantFullPermissions`: If `true`, grants unrestricted access. If `false`, you must specify policies.
## Step 5 (Advanced): Create Session Key with Granular Permissions
For more fine-grained control, you can specify call and transfer policies:
```typescript
import { Condition, LimitType } from "thirdweb/wallets/in-app";
const tx = await sendTransaction({
account: account,
transaction: createSessionKey({
account: account,
contract: accountContract,
sessionKeyAddress: sessionKeyAccountAddress,
durationInSeconds: 86400, // 1 day
grantFullPermissions: false, // Use granular permissions
callPolicies: [
{
target: "0x123...", // Target contract address
selector: "0xa9059cbb", // Function selector (e.g., ERC20 transfer)
maxValuePerUse: 0n, // Max ETH value per call
valueLimit: {
limitType: LimitType.Lifetime, // Lifetime limit
limit: 1000000n, // Max total value in wei
period: 0n, // Not used for lifetime limits
},
constraints: [
{
condition: Condition.Equal, // Constraint type
index: 0n, // Parameter index
refValue: "0x000000000000000000000000abc...", // Expected value
},
],
},
],
transferPolicies: [
{
target: "0x456...", // Recipient address for transfers
maxValuePerUse: 100000n, // Max wei per transfer
valueLimit: {
limitType: LimitType.Allowance, // Allowance-based limit
limit: 1000000n, // Total allowance in wei
period: 86400n, // Reset period in seconds
},
},
],
}),
});
console.log("Session key with policies created:", tx.transactionHash);
```
### Permission Types
#### Call Policies
Control which smart contract calls the session key can make:
- `target`: The contract address the session key can call
- `selector`: The function selector (4-byte identifier) that can be called
- `maxValuePerUse`: Maximum ETH value that can be sent per transaction
- `valueLimit`: Total value limits over time
- `constraints`: Parameter-level constraints for function calls
#### Transfer Policies
Control native token transfers the session key can make:
- `target`: The recipient address for transfers
- `maxValuePerUse`: Maximum wei per individual transfer
- `valueLimit`: Total transfer limits over time
#### Limit Types
```typescript
enum LimitType {
Unlimited = 0, // No limit
Lifetime = 1, // One-time total limit
Allowance = 2, // Renewable allowance based on period
}
```
#### Constraint Conditions
```typescript
enum Condition {
Unconstrained = 0, // No constraint
Equal = 1, // Must equal reference value
Greater = 2, // Must be greater than reference value
Less = 3, // Must be less than reference value
GreaterOrEqual = 4, // Must be >= reference value
LessOrEqual = 5, // Must be <= reference value
NotEqual = 6, // Must not equal reference value
}
```
## Step 6: Use the Session Key
Once created, the session key can be used with Engine or other server-side execution:
```typescript
import { Engine } from "thirdweb/engine";
const serverWallet = Engine.serverWallet({
address: sessionKeyAccountAddress,
chain: sepolia,
client: client,
vaultAccessToken: process.env.VAULT_TOKEN as string,
});
// Execute transactions with the session key
const tx = await sendTransaction({
account: serverWallet,
transaction: prepareTransaction({
chain: sepolia,
client: client,
to: "0x...", // Target address
value: 0n,
}),
});
console.log("Transaction executed:", tx.transactionHash);
```
## Complete Example
Here's a complete example for setting up and using a session key with ERC-7702:
```typescript
import {
createThirdwebClient,
getContract,
sendTransaction,
prepareTransaction
} from "thirdweb";
import { sepolia } from "thirdweb/chains";
import { inAppWallet, createSessionKey } from "thirdweb/wallets/in-app";
import { Engine } from "thirdweb/engine";
async function setupSessionKeyWith7702() {
// Configuration
const client = createThirdwebClient({
clientId: "your-client-id",
secretKey: "your-secret-key"
});
const sessionKeyAccountAddress = "0x..."; // Your session key address
try {
// Step 1: Create 7702 Smart EOA
const wallet = inAppWallet({
executionMode: {
mode: "EIP7702",
sponsorGas: true,
},
});
const account = await wallet.connect({
chain: sepolia,
client: client,
strategy: "google",
});
console.log("Smart EOA created:", account.address);
// Step 2: Deploy/upgrade the account
await sendTransaction({
account: account,
transaction: prepareTransaction({
chain: sepolia,
client: client,
to: account.address,
value: 0n,
}),
});
// Step 3: Get account contract
const accountContract = getContract({
address: account.address,
chain: sepolia,
client: client,
});
// Step 4: Create session key
const sessionTx = await sendTransaction({
account: account,
transaction: createSessionKey({
account: account,
contract: accountContract,
sessionKeyAddress: sessionKeyAccountAddress,
durationInSeconds: 86400, // 1 day
grantFullPermissions: true,
}),
});
console.log("Session key created:", sessionTx.transactionHash);
// Step 5: Use the session key with Engine
const serverWallet = Engine.serverWallet({
address: sessionKeyAccountAddress,
chain: sepolia,
client: client,
vaultAccessToken: process.env.VAULT_TOKEN as string,
});
// Execute a transaction with the session key
const tx = await sendTransaction({
account: serverWallet,
transaction: prepareTransaction({
chain: sepolia,
client: client,
to: "0x...", // Target address
value: 0n,
}),
});
console.log("Transaction successful:", tx.transactionHash);
return tx;
} catch (error) {
console.error("Error:", error);
throw error;
}
}
// Execute the function
setupSessionKeyWith7702()
.then((tx) => console.log("Done!", tx.transactionHash))
.catch((error) => console.error("Failed:", error));
```
## Security Considerations
- **Session Key Storage**: Store session keys securely, preferably in a vault system
- **Permission Scope**: Use granular permissions (`grantFullPermissions: false`) for production use cases
- **Time Limits**: Set appropriate `durationInSeconds` values based on your use case
- **Policy Design**: Carefully design call and transfer policies to minimize risk
- **Key Rotation**: Regularly rotate session keys and revoke unused ones
- **Monitoring**: Monitor session key usage for suspicious activity
## Troubleshooting
### Common Issues
1. **Account not deployed**: Ensure you've sent a transaction to trigger the account deployment/upgrade
2. **Invalid signature**: Make sure the admin account is properly authenticated and can sign typed data
3. **Permission denied**: Verify the session key has the necessary policies for the operation
4. **Expired session key**: Check that the current time is within the session key's validity period
### Error Handling
Always wrap your session key operations in try-catch blocks:
```typescript
try {
const tx = await sendTransaction({
account: account,
transaction: createSessionKey({
account: account,
contract: accountContract,
sessionKeyAddress: sessionKeyAccountAddress,
durationInSeconds: 86400,
grantFullPermissions: true,
}),
});
} catch (error) {
if (error.message.includes("signature")) {
console.error("Failed to sign session key creation");
} else if (error.message.includes("duration")) {
console.error("Invalid duration specified");
} else {
console.error("Session key creation failed:", error);
}
}
```
</TabsContent>
</Tabs>
## Next Steps
- Learn more about [Smart Wallets](/engine/v3/configure-wallets/server-wallets)
- Learn more about [In-App Wallets](/wallets/in-app-wallet/overview)
- Explore [Engine API Reference](https://engine.thirdweb.com/reference)
- Check out the [TypeScript SDK](/references/typescript/v5/serverWallet) documentation
- Check out the [TypeScript SDK](/references/typescript/v5) documentation
- Read about [ERC-7702](https://eips.ethereum.org/EIPS/eip-7702) specification
+4
View File
@@ -80,6 +80,10 @@ export const sidebar: SideBar = {
href: `${paymentsSlug}/x402/server`,
name: "Server Side",
},
{
href: `${paymentsSlug}/x402/agents`,
name: "Agents",
},
{
href: `${paymentsSlug}/x402/facilitator`,
name: "Facilitator",
@@ -0,0 +1,96 @@
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { OpenApiEndpoint, createMetadata } from "@doc";
export const metadata = createMetadata({
image: {
title: "x402 Agents",
icon: "payments",
},
title: "x402 Agents",
description: "Easily create AI agents that can pay for any x402-compatible API calls.",
});
# x402 Agents
Easily create AI agents that can pay for any x402-compatible API calls.
## Using the MCP Server
The easiest way to equip your agents with the ability to pay for API calls is to use the [remote MCP server](/ai/mcp) and provide the tools to your agent.
The MCP server comes with all the tools by default, but you can filter the tools available by passing a comma-separated list of tools as a query parameter.
In this example, we create a ReAct agent using LangChain and filter the MCP tools to `fetchWithPayment` and `getWalletBalance` as the only 2 tools we give our agent. You can view the full list of available tools [here](/ai/mcp#available-tools).
<Tabs defaultValue="typescript">
<TabsList>
<TabsTrigger value="typescript">TypeScript</TabsTrigger>
<TabsTrigger value="python">Python</TabsTrigger>
</TabsList>
<TabsContent value="typescript">
```typescript
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
model: "gpt-4.1",
});
const mcpServers = {
thirdweb: {
url: `https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>&tools=fetchWithPayment,getWalletBalance`
}
};
// Connect to MCP servers and get tools
const client = new MultiServerMCPClient(mcpServers);
const tools = await client.getTools();
const agent = createReactAgent({
llm: model,
tools: tools,
prompt: "Use the fetchWithPayment tool to fetch any endpoint. Always pay in {{tokenAddress}}. Your wallet address is {{walletAddress}}."
});
```
</TabsContent>
<TabsContent value="python">
```python
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
client = MultiServerMCPClient(
{
"thirdweb": {
"url": "https://api.thirdweb.com/mcp?secretKey=<your-project-secret-key>&tools=fetchWithPayment,getWalletBalance",
}
}
)
tools = await client.get_tools()
agent = create_react_agent("openai:gpt-4.1", tools, prompt="Use the fetchWithPayment tool to fetch any endpoint. Always pay in {{tokenAddress}}. Your wallet address is {{walletAddress}}.")
```
</TabsContent>
</Tabs>
## Using the API directly
You can also create your own MCP tool or wrap all external calls with the `/v1/x402/fetchWithPayment` endpoint to automatically handle payment flows when APIs return a `402 Payment Required` response.
Pass the target `url`, `method` and optional `from` wallet address to the endpoint that will complete the payment. The address should be one of your [user wallets](/wallets/users) or [server wallets](/wallets/server).
```bash
curl -X POST https://api.thirdweb.com/v1/payments/x402/fetch?url=https://api.example.com/premium&method=GET&from=0x1234... \
-H "Content-Type: application/json" \
-H "x-secret-key: <your-project-secret-key>" \
-d '{ ... }' # optional request body passed through to the url called.
```
<OpenApiEndpoint path="/v1/payments/x402/fetch" method="POST" />
@@ -1,9 +1,18 @@
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint } from "@doc";
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint, createMetadata } from "@doc";
import { TypeScriptIcon, EngineIcon } from "@/icons";
export const metadata = createMetadata({
image: {
title: "x402 Client",
icon: "payments",
},
title: "x402 Client",
description: "Make requests to any x402-compatible backend and automatically handle payment flows when APIs return a `402 Payment Required` response.",
});
# Client Side
Make requests to any x402-compatible backend by automatically handling payment flows when APIs return a `402 Payment Required` response.
Make requests to any x402-compatible backend and automatically handle payment flows when APIs return a `402 Payment Required` response.
The client library wraps the native `fetch` API and handles:
1. Initial request to the API
@@ -1,6 +1,15 @@
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint, Callout } from "@doc";
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint, Callout, createMetadata } from "@doc";
import { TypeScriptIcon, EngineIcon } from "@/icons";
export const metadata = createMetadata({
image: {
title: "x402 Facilitator",
icon: "payments",
},
title: "x402 Facilitator",
description: "Verify and submit x402 payments using your own server wallet.",
});
# Facilitator
The facilitator is a service that handles verifying and submitting x402 payments. It uses your own [server wallet](/wallets/server) and leverages EIP-7702 to submit transactions gaslessly.
+10 -1
View File
@@ -1,6 +1,15 @@
import { ArticleIconCard } from "@doc";
import { ArticleIconCard, createMetadata } from "@doc";
import { ReactIcon, TypeScriptIcon, EngineIcon } from "@/icons";
export const metadata = createMetadata({
image: {
title: "x402 Payments",
icon: "payments",
},
title: "x402 Payments",
description: "Instant payments for your APIs, websites, and autonomous agents.",
});
# x402 Payments
x402 is an open-source protocol that turns the dormant HTTP 402 Payment Required status code into a fully-featured, on-chain payment layer for APIs, websites, and autonomous agents.
@@ -1,7 +1,16 @@
import { Tabs, TabsList, TabsTrigger, TabsContent, DocImage } from "@doc";
import { Tabs, TabsList, TabsTrigger, TabsContent, DocImage, createMetadata } from "@doc";
import { Steps, Step } from "@doc";
import PaymentFlow from "./x402-protocol-flow.png";
export const metadata = createMetadata({
image: {
title: "x402 Server",
icon: "payments",
},
title: "x402 Server",
description: "Accept x402 payments in your APIs from any x402-compatible client.",
});
# Server Side
Accept x402 payments in your APIs using any x402-compatible client. Your server can verify and settle payments using thirdweb's facilitator service or any custom facilitator.
@@ -0,0 +1,663 @@
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@doc";
# Session Keys Guide
Session keys enable secure transaction execution on behalf of accounts without requiring direct access to the main account's private key. This guide covers session keys for both ERC-4337 smart accounts and ERC-7702 smart EOAs (Externally Owned Accounts).
<Tabs defaultValue="erc7702">
<TabsList>
<TabsTrigger value="erc7702">EIP-7702 Upgraded EOAs</TabsTrigger>
<TabsTrigger value="erc4337">ERC-4337 Smart Accounts</TabsTrigger>
</TabsList>
<TabsContent value="erc4337">
## Prerequisites
Before you begin, ensure you have:
- A thirdweb client configured
- Access to a session key account address
## Setup
First, let's set up the necessary imports and configuration:
```typescript
import {
generateAccount,
smartWallet,
sendTransaction,
getContract,
createThirdwebClient
} from "thirdweb";
import { sepolia } from "thirdweb/chains";
import { getAllActiveSigners } from "thirdweb/extensions/erc4337";
import { Engine } from "thirdweb/engine";
// Configure your client
const client = createThirdwebClient({
clientId: "your-client-id",
secretKey: "your-secret-key" // Only use in server environments
});
// Your session key account address
const sessionKeyAccountAddress = "0x..."; // Replace with your session key address (server wallet)
// Target address for transactions
const targetAddress = "0x..."; // Replace with your target address
```
## Step 1: Configure User Smart Wallet with Session Key
The first step is to add our session key address as a signer to the user's smart account. This is typically done on the client side since it needs explicit user approval. This can be done by configuring the smart wallet with the session key address and permissions.
In a React application, this can be done by using the `ConnectButton` or `ConnectEmbed` component. This will automatically configure the smart wallet with the session key address and permissions.
```typescript
<ConnectButton
accountAbstraction={{
chain: sepolia,
sessionKey: {
address: sessionKeyAccountAddress,
permissions: {
// "*" allows all targets, or specify specific contract addresses
approvedTargets: "*",
},
},
sponsorGas: true,
}}
/>
```
This can also be done in pure TypeScript by using the `smartWallet` function and connecting it to a personal account.
For this guide, we'll generate a random personal account that will be used to create the smart wallet:
```typescript
// this would be the user's personal account
const personalAccount = await generateAccount({
client: client,
});
// wrapped in a smart wallet with session key permissions
const smart = smartWallet({
chain: sepolia,
sessionKey: {
address: sessionKeyAccountAddress,
permissions: {
// "*" allows all targets, or specify specific contract addresses
approvedTargets: "*",
},
},
sponsorGas: true, // Enable gas sponsorship
});
console.log("Personal account created:", personalAccount.address);
```
### Session Key Permissions
The `permissions` object allows you to control what the session key can do:
- `approvedTargets`: Specify which contract addresses the session key can interact with
- Use `"*"` for all targets
- Use an array of addresses for specific contracts: `["0x123...", "0x456..."]`
## Step 2: Connect Smart Account
Connect the smart wallet using the personal account:
```typescript
const smartAccount = await smart.connect({
client: client,
personalAccount: personalAccount,
});
console.log("Smart account address:", smartAccount.address);
```
Note that in a React application, this would be done automatically by the `ConnectButton` or `ConnectEmbed` component.
## Step 3 (Optional): Verify Session Key Registration
Check that the session key is properly registered as an active signer:
```typescript
const signers = await getAllActiveSigners({
contract: getContract({
address: smartAccount.address,
chain: sepolia,
client: client,
}),
});
// Verify the session key is in the list of active signers
const isSessionKeyActive = signers
.map((s) => s.signer)
.includes(sessionKeyAccountAddress);
console.log("Session key is active:", isSessionKeyActive);
console.log("All active signers:", signers.map((s) => s.signer));
```
## Step 4: Create Engine Server Wallet
Set up an Engine server wallet using the session key for transaction execution:
```typescript
const serverWallet = Engine.serverWallet({
address: sessionKeyAccountAddress,
chain: sepolia,
client: client,
executionOptions: {
entrypointVersion: "0.6", // ERC-4337 entrypoint version
signerAddress: sessionKeyAccountAddress,
smartAccountAddress: smartAccount.address,
type: "ERC4337",
},
});
```
### Execution Options
- `entrypointVersion`: The ERC-4337 entrypoint version to use
- `signerAddress`: The session key address that will sign transactions
- `smartAccountAddress`: The smart account address that will execute transactions
- `type`: The account abstraction type (ERC4337)
## Step 5: Execute Transactions
Now you can execute transactions using the session key, here we are sending 0 ETH to the target address for testing purposes:
```typescript
const tx = await sendTransaction({
account: serverWallet,
transaction: {
chain: sepolia,
client: client,
to: targetAddress,
value: 0n, // Amount in wei (0 for no ETH transfer)
// data: "0x...", // Optional: contract call data
},
});
console.log("Transaction sent:", tx.transactionHash);
```
## Complete Example
Here's a complete example putting it all together:
```typescript
import {
generateAccount,
smartWallet,
sendTransaction,
getContract,
createThirdwebClient
} from "thirdweb";
import { sepolia } from "thirdweb/chains";
import { getAllActiveSigners } from "thirdweb/extensions/erc4337";
import { Engine } from "thirdweb/engine";
async function executeTransactionWithSessionKey() {
// Configuration
const client = createThirdwebClient({
clientId: "your-client-id",
secretKey: "your-secret-key"
});
const sessionKeyAccountAddress = "0x..."; // Your session key address (server wallet)
const targetAddress = "0x..."; // Target address for the final transaction
try {
// Step 1: Create personal account
const personalAccount = await generateAccount({ client });
// Step 2: Configure smart wallet
const smart = smartWallet({
chain: sepolia,
sessionKey: {
address: sessionKeyAccountAddress,
permissions: {
approvedTargets: "*",
},
},
sponsorGas: true,
});
// Step 3: Connect smart account
const smartAccount = await smart.connect({
client,
personalAccount,
});
// Step 4: Verify session key
const signers = await getAllActiveSigners({
contract: getContract({
address: smartAccount.address,
chain: sepolia,
client,
}),
});
const isSessionKeyActive = signers
.map((s) => s.signer)
.includes(sessionKeyAccountAddress);
if (!isSessionKeyActive) {
throw new Error("Session key is not active");
}
// Step 5: Create server wallet
const serverWallet = Engine.serverWallet({
address: sessionKeyAccountAddress,
chain: sepolia,
client,
executionOptions: {
entrypointVersion: "0.6",
signerAddress: sessionKeyAccountAddress,
smartAccountAddress: smartAccount.address,
type: "ERC4337",
},
});
// Step 6: Execute transaction
const tx = await sendTransaction({
account: serverWallet,
transaction: {
chain: sepolia,
client,
to: targetAddress,
value: 0n,
},
});
console.log("Transaction successful:", tx.transactionHash);
return tx;
} catch (error) {
console.error("Error executing transaction:", error);
throw error;
}
}
// Execute the function
executeTransactionWithSessionKey()
.then((tx) => console.log("Done!", tx.transactionHash))
.catch((error) => console.error("Failed:", error));
```
## Security Considerations
- **Session Key Storage**: Store session keys securely, preferably in a vault system
- **Permission Scope**: Limit session key permissions to only necessary targets
- **Key Rotation**: Regularly rotate session keys for enhanced security
- **Monitoring**: Monitor session key usage for suspicious activity
## Troubleshooting
### Common Issues
1. **Session key not active**: Ensure the session key is properly registered with the smart account
2. **Permission denied**: Check that the target address is included in `approvedTargets`
3. **Gas estimation failed**: Verify that gas sponsorship is properly configured
</TabsContent>
<TabsContent value="erc7702">
## Prerequisites
Before you begin, ensure you have:
- A thirdweb client configured
- An in-app wallet with ERC-7702 support
- A session key account address (can be generated or an existing wallet)
## Setup
First, let's set up the necessary imports and configuration:
```typescript
import {
createThirdwebClient,
getContract,
sendTransaction,
prepareTransaction
} from "thirdweb";
import { sepolia } from "thirdweb/chains";
import { inAppWallet, createSessionKey } from "thirdweb/wallets/in-app";
// Configure your client
const client = createThirdwebClient({
clientId: "your-client-id",
secretKey: "your-secret-key" // Only use in server environments
});
// Your session key account address
const sessionKeyAccountAddress = "0x..."; // Replace with your session key address (server wallet)
```
## Step 1: Create ERC-7702 Smart EOA
Create an in-app wallet configured with ERC-7702 execution mode:
```typescript
const wallet = inAppWallet({
executionMode: {
mode: "EIP7702",
sponsorGas: true, // Enable gas sponsorship
},
});
// Connect the wallet (user authentication)
const account = await wallet.connect({
chain: sepolia,
client: client,
strategy: "google", // or "email", "guest", etc.
});
console.log("Upgraded EOA address:", account.address);
```
### Execution Modes
The `executionMode` option allows you to configure the wallet behavior:
- `mode`: Set to `"EIP7702"` for EIP-7702 upgraded EOA functionality
- `sponsorGas`: Enable gas sponsorship for gasless transactions
## Step 2: Get Account Contract
Create a contract instance for the deployed smart EOA:
```typescript
const accountContract = getContract({
address: account.address,
chain: sepolia,
client: client,
});
```
## Step 3: Create Session Key with Full Permissions
Add a session key with full execution permissions:
```typescript
const tx = await sendTransaction({
account: account,
transaction: createSessionKey({
account: account,
contract: accountContract,
sessionKeyAddress: sessionKeyAccountAddress,
durationInSeconds: 86400, // 1 day
grantFullPermissions: true, // Allow unrestricted access
}),
});
console.log("Session key created:", tx.transactionHash);
```
### Session Key Options
- `account`: The admin account that will create the session key
- `contract`: The smart EOA contract instance
- `sessionKeyAddress`: The address that will be granted session key permissions
- `durationInSeconds`: How long the session key should be valid (in seconds)
- `grantFullPermissions`: If `true`, grants unrestricted access. If `false`, you must specify policies.
## Step 4 (Advanced): Create Session Key with Granular Permissions
For more fine-grained control, you can specify call and transfer policies:
```typescript
import { Condition, LimitType } from "thirdweb/wallets/in-app";
const tx = await sendTransaction({
account: account,
transaction: createSessionKey({
account: account,
contract: accountContract,
sessionKeyAddress: sessionKeyAccountAddress,
durationInSeconds: 86400, // 1 day
grantFullPermissions: false, // Use granular permissions
callPolicies: [
{
target: "0x123...", // Target contract address
selector: "0xa9059cbb", // Function selector (e.g., ERC20 transfer)
maxValuePerUse: 0n, // Max ETH value per call
valueLimit: {
limitType: LimitType.Lifetime, // Lifetime limit
limit: 1000000n, // Max total value in wei
period: 0n, // Not used for lifetime limits
},
constraints: [
{
condition: Condition.Equal, // Constraint type
index: 0n, // Parameter index
refValue: "0x000000000000000000000000abc...", // Expected value
},
],
},
],
transferPolicies: [
{
target: "0x456...", // Recipient address for transfers
maxValuePerUse: 100000n, // Max wei per transfer
valueLimit: {
limitType: LimitType.Allowance, // Allowance-based limit
limit: 1000000n, // Total allowance in wei
period: 86400n, // Reset period in seconds
},
},
],
}),
});
console.log("Session key with policies created:", tx.transactionHash);
```
### Permission Types
#### Call Policies
Control which smart contract calls the session key can make:
- `target`: The contract address the session key can call
- `selector`: The function selector (4-byte identifier) that can be called
- `maxValuePerUse`: Maximum ETH value that can be sent per transaction
- `valueLimit`: Total value limits over time
- `constraints`: Parameter-level constraints for function calls
#### Transfer Policies
Control native token transfers the session key can make:
- `target`: The recipient address for transfers
- `maxValuePerUse`: Maximum wei per individual transfer
- `valueLimit`: Total transfer limits over time
#### Limit Types
```typescript
enum LimitType {
Unlimited = 0, // No limit
Lifetime = 1, // One-time total limit
Allowance = 2, // Renewable allowance based on period
}
```
#### Constraint Conditions
```typescript
enum Condition {
Unconstrained = 0, // No constraint
Equal = 1, // Must equal reference value
Greater = 2, // Must be greater than reference value
Less = 3, // Must be less than reference value
GreaterOrEqual = 4, // Must be >= reference value
LessOrEqual = 5, // Must be <= reference value
NotEqual = 6, // Must not equal reference value
}
```
## Step 5: Use the Session Key
Once created, the session key can be used with Engine or other server-side execution:
```typescript
import { Engine } from "thirdweb/engine";
const serverWallet = Engine.serverWallet({
address: sessionKeyAccountAddress,
chain: sepolia,
client: client,
executionOptions: {
type: "EIP7702",
sessionKeyAddress: sessionKeyAccountAddress,
accountAddress: account.address,
},
});
// Execute transactions with the session key
const tx = await sendTransaction({
account: serverWallet,
transaction: prepareTransaction({
chain: sepolia,
client: client,
to: "0x...", // Target address
value: 0n,
}),
});
console.log("Transaction executed:", tx.transactionHash);
```
## Complete Example
Here's a complete example for setting up and using a session key with ERC-7702:
```typescript
import {
createThirdwebClient,
getContract,
sendTransaction,
prepareTransaction
} from "thirdweb";
import { sepolia } from "thirdweb/chains";
import { inAppWallet, createSessionKey } from "thirdweb/wallets/in-app";
import { Engine } from "thirdweb/engine";
async function setupSessionKeyWith7702() {
// Configuration
const client = createThirdwebClient({
clientId: "your-client-id",
secretKey: "your-secret-key"
});
const sessionKeyAccountAddress = "0x..."; // Your session key address (server wallet)
try {
// Step 1: Create 7702 Smart EOA
const wallet = inAppWallet({
executionMode: {
mode: "EIP7702",
sponsorGas: true,
},
});
const account = await wallet.connect({
chain: sepolia,
client: client,
strategy: "google",
});
console.log("Upgraded EOA created:", account.address);
// Step 2: Get account contract
const accountContract = getContract({
address: account.address,
chain: sepolia,
client: client,
});
// Step 3: Create session key
const sessionTx = await sendTransaction({
account: account,
transaction: createSessionKey({
account: account,
contract: accountContract,
sessionKeyAddress: sessionKeyAccountAddress,
durationInSeconds: 86400, // 1 day
grantFullPermissions: true,
}),
});
console.log("Session key created:", sessionTx.transactionHash);
// Step 4: Use the session key with Engine
const serverWallet = Engine.serverWallet({
address: sessionKeyAccountAddress,
chain: sepolia,
client: client,
executionOptions: {
type: "EIP7702",
sessionKeyAddress: sessionKeyAccountAddress,
accountAddress: account.address,
},
});
// Execute a transaction with the session key
const tx = await sendTransaction({
account: serverWallet,
transaction: prepareTransaction({
chain: sepolia,
client: client,
to: "0x...", // Target address
value: 0n,
}),
});
console.log("Transaction successful:", tx.transactionHash);
return tx;
} catch (error) {
console.error("Error:", error);
throw error;
}
}
// Execute the function
setupSessionKeyWith7702()
.then((tx) => console.log("Done!", tx.transactionHash))
.catch((error) => console.error("Failed:", error));
```
## Security Considerations
- **Session Key Storage**: Store session keys securely, preferably in a vault system
- **Permission Scope**: Use granular permissions (`grantFullPermissions: false`) for production use cases
- **Time Limits**: Set appropriate `durationInSeconds` values based on your use case
- **Policy Design**: Carefully design call and transfer policies to minimize risk
- **Key Rotation**: Regularly rotate session keys and revoke unused ones
- **Monitoring**: Monitor session key usage for suspicious activity
## Troubleshooting
### Common Issues
1. **Account not deployed**: Ensure you've sent a transaction to trigger the account deployment/upgrade
2. **Invalid signature**: Make sure the admin account is properly authenticated and can sign typed data
3. **Permission denied**: Verify the session key has the necessary policies for the operation
4. **Expired session key**: Check that the current time is within the session key's validity period
</TabsContent>
</Tabs>
## Next Steps
- Learn more about [In-App Wallets](/wallets/users)
- Explore the [API Reference](/reference)
- Check out the [TypeScript SDK](/references/typescript/v5) documentation
+4
View File
@@ -65,6 +65,10 @@ export const sidebar: SideBar = {
href: `${walletSlug}/export-private-key`,
name: "Export Private Keys",
},
{
href: `${walletSlug}/session-keys`,
name: "Session Keys",
},
{
href: `${walletSlug}/adapters`,
name: "Usage with other libraries",
+40
View File
@@ -1,5 +1,45 @@
# wagmi-inapp
## 0.0.8
### Patch Changes
- Updated dependencies []:
- thirdweb@5.108.13
- @thirdweb-dev/wagmi-adapter@0.2.163
## 0.0.7
### Patch Changes
- Updated dependencies [[`0082e99`](https://github.com/thirdweb-dev/js/commit/0082e996cae47a326110e74457c72b3f3936743d), [`0082e99`](https://github.com/thirdweb-dev/js/commit/0082e996cae47a326110e74457c72b3f3936743d)]:
- thirdweb@5.108.12
- @thirdweb-dev/wagmi-adapter@0.2.162
## 0.0.6
### Patch Changes
- Updated dependencies [[`05550d0`](https://github.com/thirdweb-dev/js/commit/05550d045f291b8e53afd0d7e73c438bee202fb7)]:
- thirdweb@5.108.11
- @thirdweb-dev/wagmi-adapter@0.2.161
## 0.0.5
### Patch Changes
- Updated dependencies [[`f630912`](https://github.com/thirdweb-dev/js/commit/f630912b9b0e1d8ebe22b0b52c650f5e40e12baf), [`62cfbb7`](https://github.com/thirdweb-dev/js/commit/62cfbb7441e66156c55c76d73f5b481f5ea576bc)]:
- thirdweb@5.108.10
- @thirdweb-dev/wagmi-adapter@0.2.160
## 0.0.4
### Patch Changes
- Updated dependencies [[`968913d`](https://github.com/thirdweb-dev/js/commit/968913d55b761c1e23062fdf6008904fa8c17050)]:
- thirdweb@5.108.9
- @thirdweb-dev/wagmi-adapter@0.2.159
## 0.0.3
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wagmi-inapp",
"private": true,
"version": "0.0.3",
"version": "0.0.8",
"type": "module",
"scripts": {
"dev": "vite",
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/ai-sdk-provider
## 0.1.4
### Patch Changes
- [#8211](https://github.com/thirdweb-dev/js/pull/8211) [`71a1e58`](https://github.com/thirdweb-dev/js/commit/71a1e58eafa6d51f0a3eb9c7a3fcc685465c72b5) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Default in-memory session store across thirdweb AI instances, option to pass your own
## 0.1.3
### Patch Changes
+1 -1
View File
@@ -56,5 +56,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.1.3"
"version": "0.1.4"
}
@@ -1,4 +1,8 @@
export { createThirdwebAI, type ThirdwebProvider } from "../provider.js";
export {
createThirdwebAI,
type SessionStore,
type ThirdwebProvider,
} from "../provider.js";
export type {
MonitorTransactionInput,
MonitorTransactionOutput,
+14 -7
View File
@@ -544,7 +544,7 @@ export class ThirdwebProvider implements ProviderV2 {
constructor(config: ThirdwebConfig = {}) {
this.config = config;
this.session = new SessionStore();
this.session = config.sessionStore || memorySessionStore;
}
chat(id?: string, settings: ThirdwebSettings = {}) {
@@ -581,22 +581,29 @@ export class ThirdwebProvider implements ProviderV2 {
}
}
class SessionStore {
export interface SessionStore {
getSessionId(chatId: string): string | null;
setSessionId(chatId: string, sessionId: string): void;
clearSessionId(chatId: string): void;
}
class InMemorySessionStore implements SessionStore {
private sessionId: Map<string, string> = new Map();
getSessionId(chatId: string) {
getSessionId(chatId: string): string | null {
return this.sessionId.get(chatId) || null;
}
setSessionId(chatId: string, sessionId: string) {
setSessionId(chatId: string, sessionId: string): void {
this.sessionId.set(chatId, sessionId);
}
clearSessionId(chatId: string) {
clearSessionId(chatId: string): void {
this.sessionId.delete(chatId);
}
}
// singleton session store used as default if no session store is provided
const memorySessionStore = new InMemorySessionStore();
// Factory function for easier usage
export function createThirdwebAI(config: ThirdwebConfig = {}) {
return new ThirdwebProvider(config);
+6
View File
@@ -1,4 +1,5 @@
import type { UIDataTypes, UIMessage } from "ai";
import type { SessionStore } from "./provider.js";
import type {
MonitorTransactionInput,
MonitorTransactionOutput,
@@ -26,6 +27,11 @@ export interface ThirdwebConfig {
* Optional: The user wallet auth token (JWT) for executing transactions
*/
walletAuthToken?: string;
/**
* Optional: The session store to use for storing and retrieving thirdweb AI session IDs from ai SDK chat ids.
* If not provided, a default in-memory session store will be used.
*/
sessionStore?: SessionStore;
}
export interface ThirdwebSettings {
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/engine
## 3.3.0
### Minor Changes
- [#8238](https://github.com/thirdweb-dev/js/pull/8238) [`9545b43`](https://github.com/thirdweb-dev/js/commit/9545b4367598408dbb88c63d8a8fe21305f870b5) Thanks [@d4mr](https://github.com/d4mr)! - Add Solana endpoints
## 3.2.2
### Patch Changes
- [#8218](https://github.com/thirdweb-dev/js/pull/8218) [`f630912`](https://github.com/thirdweb-dev/js/commit/f630912b9b0e1d8ebe22b0b52c650f5e40e12baf) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update to latest API spec
## 3.2.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/engine",
"version": "3.2.1",
"version": "3.3.0",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
+251
View File
@@ -11,11 +11,18 @@ import type {
CancelTransactionResponses,
CreateAccountData,
CreateAccountResponses,
CreateSolanaAccountData,
CreateSolanaAccountResponses,
EncodeContractData,
EncodeContractResponses,
GetActivityLogsData,
GetActivityLogsErrors,
GetActivityLogsResponses,
GetSolanaActivityLogsData,
GetSolanaActivityLogsErrors,
GetSolanaActivityLogsResponses,
GetSolanaTransactionsData,
GetSolanaTransactionsResponses,
GetTransactionAnalyticsData,
GetTransactionAnalyticsResponses,
GetTransactionAnalyticsSummaryData,
@@ -25,17 +32,30 @@ import type {
GetTransactionsResponses,
ListAccountsData,
ListAccountsResponses,
ListSolanaAccountsData,
ListSolanaAccountsResponses,
ReadContractData,
ReadContractResponses,
SearchActivityLogsData,
SearchActivityLogsErrors,
SearchActivityLogsResponses,
SearchSolanaActivityLogsData,
SearchSolanaActivityLogsErrors,
SearchSolanaActivityLogsResponses,
SearchSolanaTransactionsData,
SearchSolanaTransactionsResponses,
SearchTransactionsData,
SearchTransactionsResponses,
SendSolanaTransactionData,
SendSolanaTransactionResponses,
SendTransactionData,
SendTransactionResponses,
SignMessageData,
SignMessageResponses,
SignSolanaMessageData,
SignSolanaMessageResponses,
SignSolanaTransactionData,
SignSolanaTransactionResponses,
SignTypedDataData,
SignTypedDataResponses,
WriteContractData,
@@ -227,6 +247,33 @@ export const encodeContract = <ThrowOnError extends boolean = false>(
});
};
/**
* Send Solana Transaction
* Execute a Solana transaction with custom instructions
*/
export const sendSolanaTransaction = <ThrowOnError extends boolean = false>(
options: Options<SendSolanaTransactionData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).post<
SendSolanaTransactionResponses,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/transaction",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Cancel Transaction
* Attempt to cancel a queued transaction. Transactions that have been sent and are waiting for mine cannot be cancelled.
@@ -300,6 +347,110 @@ export const createAccount = <ThrowOnError extends boolean = false>(
});
};
/**
* List Solana Accounts
* List all Solana wallets provisioned for the project.
*/
export const listSolanaAccounts = <ThrowOnError extends boolean = false>(
options?: Options<ListSolanaAccountsData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
ListSolanaAccountsResponses,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/accounts",
...options,
});
};
/**
* Get or Create Solana Account
* Create a new solana wallet in the vault or return the existing wallet for a given label.
*/
export const createSolanaAccount = <ThrowOnError extends boolean = false>(
options?: Options<CreateSolanaAccountData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
CreateSolanaAccountResponses,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/accounts",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
};
/**
* Sign Solana Message
* Sign an arbitrary message with a Solana wallet.
*/
export const signSolanaMessage = <ThrowOnError extends boolean = false>(
options?: Options<SignSolanaMessageData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
SignSolanaMessageResponses,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/sign-message",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
};
/**
* Sign Solana Transaction
* Sign a serialized Solana transaction.
*/
export const signSolanaTransaction = <ThrowOnError extends boolean = false>(
options?: Options<SignSolanaTransactionData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
SignSolanaTransactionResponses,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/sign-transaction",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
};
/**
* Get Transactions
* Search transactions with various filters and pagination
@@ -455,3 +606,103 @@ export const searchActivityLogs = <ThrowOnError extends boolean = false>(
},
});
};
/**
* Get Solana Transactions
* Search Solana transactions with various filters and pagination
*/
export const getSolanaTransactions = <ThrowOnError extends boolean = false>(
options?: Options<GetSolanaTransactionsData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
GetSolanaTransactionsResponses,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/transactions",
...options,
});
};
/**
* Search Solana Transactions
* Advanced search for Solana transactions with complex nested filters
*/
export const searchSolanaTransactions = <ThrowOnError extends boolean = false>(
options?: Options<SearchSolanaTransactionsData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
SearchSolanaTransactionsResponses,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/transactions/search",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
};
/**
* Get Solana Activity Logs
* Get paginated activity logs for a specific Solana transaction with tenancy enforcement
*/
export const getSolanaActivityLogs = <ThrowOnError extends boolean = false>(
options: Options<GetSolanaActivityLogsData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetSolanaActivityLogsResponses,
GetSolanaActivityLogsErrors,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/transactions/activity-logs",
...options,
});
};
/**
* Search Solana Activity Logs
* Search Solana activity logs across transactions with advanced filtering and tenancy enforcement
*/
export const searchSolanaActivityLogs = <ThrowOnError extends boolean = false>(
options?: Options<SearchSolanaActivityLogsData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
SearchSolanaActivityLogsResponses,
SearchSolanaActivityLogsErrors,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/transactions/activity-logs/search",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
};
File diff suppressed because it is too large Load Diff
+35
View File
@@ -1,5 +1,40 @@
# @thirdweb-dev/nebula
## 0.2.66
### Patch Changes
- Updated dependencies []:
- thirdweb@5.108.13
## 0.2.65
### Patch Changes
- Updated dependencies [[`0082e99`](https://github.com/thirdweb-dev/js/commit/0082e996cae47a326110e74457c72b3f3936743d), [`0082e99`](https://github.com/thirdweb-dev/js/commit/0082e996cae47a326110e74457c72b3f3936743d)]:
- thirdweb@5.108.12
## 0.2.64
### Patch Changes
- Updated dependencies [[`05550d0`](https://github.com/thirdweb-dev/js/commit/05550d045f291b8e53afd0d7e73c438bee202fb7)]:
- thirdweb@5.108.11
## 0.2.63
### Patch Changes
- Updated dependencies [[`f630912`](https://github.com/thirdweb-dev/js/commit/f630912b9b0e1d8ebe22b0b52c650f5e40e12baf), [`62cfbb7`](https://github.com/thirdweb-dev/js/commit/62cfbb7441e66156c55c76d73f5b481f5ea576bc)]:
- thirdweb@5.108.10
## 0.2.62
### Patch Changes
- Updated dependencies [[`968913d`](https://github.com/thirdweb-dev/js/commit/968913d55b761c1e23062fdf6008904fa8c17050)]:
- thirdweb@5.108.9
## 0.2.61
### Patch Changes
+1 -1
View File
@@ -57,5 +57,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.61"
"version": "0.2.66"
}
+1
View File
@@ -241,6 +241,7 @@ export type ProjectService =
rotationCode?: string | null;
encryptedAdminKey?: string | null;
encryptedWalletAccessToken?: string | null;
projectWalletAddress?: string | null;
}
| ProjectBundlerService
| ProjectEmbeddedWalletsService;
+38
View File
@@ -1,5 +1,43 @@
# thirdweb
## 5.108.13
### Patch Changes
- Updated dependencies [[`9545b43`](https://github.com/thirdweb-dev/js/commit/9545b4367598408dbb88c63d8a8fe21305f870b5)]:
- @thirdweb-dev/engine@3.3.0
## 5.108.12
### Patch Changes
- [#8226](https://github.com/thirdweb-dev/js/pull/8226) [`0082e99`](https://github.com/thirdweb-dev/js/commit/0082e996cae47a326110e74457c72b3f3936743d) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add optional recipientAddress to x402 settlePayment
- [#8226](https://github.com/thirdweb-dev/js/pull/8226) [`0082e99`](https://github.com/thirdweb-dev/js/commit/0082e996cae47a326110e74457c72b3f3936743d) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose 7702 utility functions
## 5.108.11
### Patch Changes
- [#8228](https://github.com/thirdweb-dev/js/pull/8228) [`05550d0`](https://github.com/thirdweb-dev/js/commit/05550d045f291b8e53afd0d7e73c438bee202fb7) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - Export ERC7702 MinimalAccount helpers and events
## 5.108.10
### Patch Changes
- [#8218](https://github.com/thirdweb-dev/js/pull/8218) [`f630912`](https://github.com/thirdweb-dev/js/commit/f630912b9b0e1d8ebe22b0b52c650f5e40e12baf) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Export missing x402 types
- [#8215](https://github.com/thirdweb-dev/js/pull/8215) [`62cfbb7`](https://github.com/thirdweb-dev/js/commit/62cfbb7441e66156c55c76d73f5b481f5ea576bc) Thanks [@MananTank](https://github.com/MananTank)! - Show error in BuyWidget and SwapWidget UI if fetching token details fails
- Updated dependencies [[`f630912`](https://github.com/thirdweb-dev/js/commit/f630912b9b0e1d8ebe22b0b52c650f5e40e12baf)]:
- @thirdweb-dev/engine@3.2.2
## 5.108.9
### Patch Changes
- [#8192](https://github.com/thirdweb-dev/js/pull/8192) [`968913d`](https://github.com/thirdweb-dev/js/commit/968913d55b761c1e23062fdf6008904fa8c17050) Thanks [@MananTank](https://github.com/MananTank)! - Fix TransactionWidget not updating when `currency` prop is changed after initial render
## 5.108.8
### Patch Changes
+1 -1
View File
@@ -430,5 +430,5 @@
}
},
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "5.108.8"
"version": "5.108.13"
}
@@ -189,8 +189,8 @@ export function serverWallet(options: ServerWalletOptions): ServerWallet {
};
case "EIP7702":
return {
...executionOptions,
chainId,
from: address,
type: "EIP7702",
};
}
@@ -1,5 +1,53 @@
// --- KEEEP IN SYNC with exports/wallets/in-app.ts ---
export { executedEvent } from "../../extensions/erc7702/__generated__/MinimalAccount/events/Executed.js";
export { sessionCreatedEvent } from "../../extensions/erc7702/__generated__/MinimalAccount/events/SessionCreated.js";
export { valueReceivedEvent } from "../../extensions/erc7702/__generated__/MinimalAccount/events/ValueReceived.js";
// ERC7702 generated helpers
export {
eip712Domain,
isEip712DomainSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/eip712Domain.js";
export {
type GetCallPoliciesForSignerParams,
getCallPoliciesForSigner,
isGetCallPoliciesForSignerSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/getCallPoliciesForSigner.js";
export {
type GetSessionExpirationForSignerParams,
getSessionExpirationForSigner,
isGetSessionExpirationForSignerSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/getSessionExpirationForSigner.js";
export {
type GetSessionStateForSignerParams,
getSessionStateForSigner,
isGetSessionStateForSignerSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/getSessionStateForSigner.js";
export {
type GetTransferPoliciesForSignerParams,
getTransferPoliciesForSigner,
isGetTransferPoliciesForSignerSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/getTransferPoliciesForSigner.js";
export {
type IsWildcardSignerParams,
isIsWildcardSignerSupported,
isWildcardSigner,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/isWildcardSigner.js";
export {
type CreateSessionWithSigParams,
createSessionWithSig,
isCreateSessionWithSigSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/write/createSessionWithSig.js";
export {
type ExecuteParams,
execute,
isExecuteSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/write/execute.js";
export {
type ExecuteWithSigParams,
executeWithSig,
isExecuteWithSigSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/write/executeWithSig.js";
//ACCOUNT
export {
type CreateSessionKeyOptions,
@@ -1,11 +1,60 @@
// --- KEEEP IN SYNC with exports/wallets/in-app.native.ts ---
export { executedEvent } from "../../extensions/erc7702/__generated__/MinimalAccount/events/Executed.js";
export { sessionCreatedEvent } from "../../extensions/erc7702/__generated__/MinimalAccount/events/SessionCreated.js";
export { valueReceivedEvent } from "../../extensions/erc7702/__generated__/MinimalAccount/events/ValueReceived.js";
// ERC7702 generated helpers
export {
eip712Domain,
isEip712DomainSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/eip712Domain.js";
export {
type GetCallPoliciesForSignerParams,
getCallPoliciesForSigner,
isGetCallPoliciesForSignerSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/getCallPoliciesForSigner.js";
export {
type GetSessionExpirationForSignerParams,
getSessionExpirationForSigner,
isGetSessionExpirationForSignerSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/getSessionExpirationForSigner.js";
export {
type GetSessionStateForSignerParams,
getSessionStateForSigner,
isGetSessionStateForSignerSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/getSessionStateForSigner.js";
export {
type GetTransferPoliciesForSignerParams,
getTransferPoliciesForSigner,
isGetTransferPoliciesForSignerSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/getTransferPoliciesForSigner.js";
export {
type IsWildcardSignerParams,
isIsWildcardSignerSupported,
isWildcardSigner,
} from "../../extensions/erc7702/__generated__/MinimalAccount/read/isWildcardSigner.js";
export {
type CreateSessionWithSigParams,
createSessionWithSig,
isCreateSessionWithSigSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/write/createSessionWithSig.js";
export {
type ExecuteParams,
execute,
isExecuteSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/write/execute.js";
export {
type ExecuteWithSigParams,
executeWithSig,
isExecuteWithSigSupported,
} from "../../extensions/erc7702/__generated__/MinimalAccount/write/executeWithSig.js";
//ACCOUNT
export {
type CreateSessionKeyOptions,
createSessionKey,
isCreateSessionKeySupported,
} from "../../extensions/erc7702/account/createSessionKey.js";
export type {
Condition,
LimitType,
+1
View File
@@ -1,3 +1,4 @@
export type { Money, PaymentMiddlewareConfig } from "x402/types";
export { decodePayment, encodePayment } from "../x402/encode.js";
export {
facilitator,
@@ -9,7 +9,6 @@ import { getCompilerMetadata } from "../../../contract/actions/get-compiler-meta
import { getContract } from "../../../contract/contract.js";
import { decimals } from "../../../extensions/erc20/read/decimals.js";
import { getToken } from "../../../pay/convert/get-token.js";
import type { SupportedFiatCurrency } from "../../../pay/convert/type.js";
import { encode } from "../../../transaction/actions/encode.js";
import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js";
import { getTransactionGasCost } from "../../../transaction/utils.js";
@@ -18,10 +17,7 @@ import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-va
import { toTokens } from "../../../utils/units.js";
import type { Wallet } from "../../../wallets/interfaces/wallet.js";
import { hasSponsoredTransactionsEnabled } from "../../../wallets/smart/is-smart-wallet.js";
import {
formatCurrencyAmount,
formatTokenAmount,
} from "../../web/ui/ConnectWallet/screens/formatTokenBalance.js";
import { formatTokenAmount } from "../../web/ui/ConnectWallet/screens/formatTokenBalance.js";
import { useChainMetadata } from "./others/useChainQuery.js";
interface TransactionDetails {
@@ -31,7 +27,6 @@ interface TransactionDetails {
selector: string;
description?: string;
};
usdValueDisplay: string | null;
txCostDisplay: string;
gasCostDisplay: string | null;
tokenInfo: TokenWithPrices | null;
@@ -45,7 +40,6 @@ interface UseTransactionDetailsOptions {
transaction: PreparedTransaction;
client: ThirdwebClient;
wallet: Wallet | undefined;
currency: SupportedFiatCurrency | undefined;
}
/**
@@ -54,7 +48,6 @@ interface UseTransactionDetailsOptions {
*/
export function useTransactionDetails({
transaction,
currency,
client,
wallet,
}: UseTransactionDetailsOptions) {
@@ -158,9 +151,6 @@ export function useTransactionDetails({
: (value || 0n) + (gasCostWei || 0n);
const totalCost = toTokens(totalCostWei, decimal);
const price = tokenInfo?.prices[currency || "USD"] || 0;
const usdValue = price ? Number(totalCost) * price : null;
return {
contractMetadata,
costWei,
@@ -173,9 +163,6 @@ export function useTransactionDetails({
totalCost,
totalCostWei,
txCostDisplay: `${formatTokenAmount(costWei, decimal)} ${tokenSymbol}`,
usdValueDisplay: usdValue
? formatCurrencyAmount(currency || "USD", usdValue)
: null,
};
},
queryKey: [
@@ -251,11 +251,14 @@ export function FundWallet(props: FundWalletProps) {
? tokenQuery.data.token
: undefined,
isFetching: tokenQuery.isFetching,
isError:
tokenQuery.isError ||
tokenQuery.data?.type === "unsupported_token",
}
: undefined
}
balance={{
data: tokenBalanceQuery.data?.value,
data: tokenBalanceQuery.data,
isFetching: tokenBalanceQuery.isFetching,
}}
client={props.client}
@@ -279,6 +282,22 @@ export function FundWallet(props: FundWalletProps) {
<Spacer y="md" />
{(tokenQuery.isError ||
tokenQuery.data?.type === "unsupported_token") && (
<div
style={{
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.full,
padding: spacing.xs,
marginBottom: spacing.md,
}}
>
<Text size="sm" color="danger" center>
Failed to fetch token details
</Text>
</div>
)}
{/* Continue Button */}
{activeWalletInfo ? (
<Button
@@ -373,6 +392,7 @@ function TokenSection(props: {
| {
data: TokenWithPrices | undefined;
isFetching: boolean;
isError: boolean;
}
| undefined;
currency: SupportedFiatCurrency;
@@ -381,7 +401,14 @@ function TokenSection(props: {
title: string;
isConnected: boolean;
balance: {
data: bigint | undefined;
data:
| {
value: bigint;
decimals: number;
symbol: string;
name: string;
}
| undefined;
isFetching: boolean;
};
onWalletClick: () => void;
@@ -565,17 +592,16 @@ function TokenSection(props: {
<Text size="xs" color="secondaryText">
Current Balance
</Text>
{props.balance.data === undefined ||
props.selectedToken.data === undefined ? (
{props.balance.data === undefined ? (
<Skeleton height={fontSize.xs} width="100px" />
) : (
<Text size="xs" color="primaryText">
{formatTokenAmount(
props.balance.data,
props.selectedToken.data.decimals,
props.balance.data.value,
props.balance.data.decimals,
5,
)}{" "}
{props.selectedToken.data.symbol}
{props.balance.data.symbol}
</Text>
)}
</div>
@@ -24,6 +24,7 @@ import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.j
import { useActiveWallet } from "../../../core/hooks/wallets/useActiveWallet.js";
import { ConnectButton } from "../ConnectWallet/ConnectButton.js";
import { PoweredByThirdweb } from "../ConnectWallet/PoweredByTW.js";
import { formatCurrencyAmount } from "../ConnectWallet/screens/formatTokenBalance.js";
import { Container, Line } from "../components/basic.js";
import { Button } from "../components/buttons.js";
import { ChainName } from "../components/ChainName.js";
@@ -100,7 +101,6 @@ export function TransactionPayment({
client,
transaction: transaction,
wallet,
currency: currency,
});
// We can't use useWalletBalance here because erc20Value is a possibly async value
@@ -134,6 +134,19 @@ export function TransactionPayment({
const buttonLabel = _buttonLabel || `Execute ${functionName}`;
const tokenFiatPricePerToken =
transactionDataQuery.data?.tokenInfo?.prices[currency] || undefined;
const totalFiatCost =
tokenFiatPricePerToken && transactionDataQuery.data
? tokenFiatPricePerToken * Number(transactionDataQuery.data.totalCost)
: undefined;
const costToDisplay =
totalFiatCost !== undefined
? formatCurrencyAmount(currency, totalFiatCost)
: transactionDataQuery.data?.txCostDisplay;
if (isLoading) {
return (
<WithHeader
@@ -207,8 +220,7 @@ export function TransactionPayment({
>
{/* USD Value */}
<Text color="primaryText" size="xl" weight={600}>
{transactionDataQuery.data?.usdValueDisplay ||
transactionDataQuery.data?.txCostDisplay}
{costToDisplay}
</Text>
{/* Function Name */}
@@ -770,6 +770,9 @@ function TransactionWidgetContent(
if (screen.id === "execute-tx") {
return (
<ExecutingTxScreen
onBack={() => {
setScreen({ id: "init-ui" });
}}
closeModal={() => {
setScreen({ id: "init-ui" });
}}
@@ -21,6 +21,7 @@ export function SelectedTokenButton(props: {
| {
data: TokenWithPrices | undefined;
isFetching: boolean;
isError: boolean;
}
| undefined;
client: ThirdwebClient;
@@ -47,7 +48,7 @@ export function SelectedTokenButton(props: {
{/* icons */}
<Container relative color="secondaryText">
{/* token icon */}
{props.selectedToken ? (
{props.selectedToken && !props.selectedToken.isError ? (
<Img
key={props.selectedToken?.data?.iconUri}
src={
@@ -112,7 +113,7 @@ export function SelectedTokenButton(props: {
</Container>
{/* token symbol and chain name */}
{props.selectedToken ? (
{props.selectedToken && !props.selectedToken.isError ? (
<Container flex="column" style={{ gap: "3px" }}>
{props.selectedToken?.isFetching ? (
<Skeleton width="60px" height={fontSize.md} />
@@ -199,7 +199,6 @@ export function PaymentOverview(props: {
<TransactionOverViewCompact
client={props.client}
paymentMethod={props.paymentMethod}
currency={props.currency}
transaction={props.modeInfo.transaction}
metadata={props.metadata}
/>
@@ -211,7 +210,6 @@ export function PaymentOverview(props: {
const TransactionOverViewCompact = (props: {
transaction: PreparedTransaction;
currency: SupportedFiatCurrency;
paymentMethod: PaymentMethod;
client: ThirdwebClient;
metadata: {
@@ -224,7 +222,6 @@ const TransactionOverViewCompact = (props: {
client: props.client,
transaction: props.transaction,
wallet: props.paymentMethod.payerWallet,
currency: props.currency,
});
if (!txInfo.data) {
@@ -96,6 +96,7 @@ function useTokenPrice(options: {
);
},
refetchOnMount: false,
retry: false,
refetchOnWindowFocus: false,
});
}
@@ -320,6 +321,7 @@ export function SwapUI(props: SwapUIProps) {
? {
data: sellTokenQuery.data,
isFetching: sellTokenQuery.isFetching,
isError: sellTokenQuery.isError,
}
: undefined
}
@@ -372,6 +374,7 @@ export function SwapUI(props: SwapUIProps) {
? {
data: buyTokenQuery.data,
isFetching: buyTokenQuery.isFetching,
isError: buyTokenQuery.isError,
}
: undefined
}
@@ -389,7 +392,9 @@ export function SwapUI(props: SwapUIProps) {
/>
{/* error message */}
{preparedResultQuery.error ? (
{preparedResultQuery.error ||
buyTokenQuery.isError ||
sellTokenQuery.isError ? (
<Text
size="sm"
color="danger"
@@ -398,7 +403,13 @@ export function SwapUI(props: SwapUIProps) {
paddingBlock: spacing.md,
}}
>
Failed to get a quote
{preparedResultQuery.error
? "Failed to get a quote"
: buyTokenQuery.isError
? "Failed to fetch buy token details"
: sellTokenQuery.isError
? "Failed to fetch sell token details"
: "Failed to get a quote"}
</Text>
) : (
<Spacer y="md" />
@@ -626,6 +637,7 @@ function TokenSection(props: {
| {
data: TokenWithPrices | undefined;
isFetching: boolean;
isError: boolean;
}
| undefined;
currency: SupportedFiatCurrency;
@@ -20,7 +20,7 @@ export function ExecutingTxScreen(props: {
tx: PreparedTransaction;
closeModal: () => void;
onTxSent: (data: WaitForReceiptOptions) => void;
onBack?: () => void;
onBack: (() => void) | undefined;
windowAdapter: WindowAdapter;
}) {
const sendTxCore = useSendTransaction({
@@ -133,6 +133,9 @@ function DepositAndExecuteTx(props: ModalProps) {
if (screen === "execute-tx") {
return (
<ExecutingTxScreen
onBack={() => {
setScreen("deposit");
}}
closeModal={props.onClose}
onTxSent={props.onTxSent}
tx={props.tx}
+3 -1
View File
@@ -44,6 +44,7 @@ export async function decodePaymentRequest(
price,
network,
facilitator,
payTo,
resourceUrl,
routeConfig = {},
method,
@@ -104,7 +105,7 @@ export async function decodePaymentRequest(
resource: resourceUrl,
description: description ?? "",
mimeType: mimeType ?? "application/json",
payTo: getAddress(facilitator.address),
payTo: getAddress(facilitator.address), // always pay to the facilitator address first
maxTimeoutSeconds: maxTimeoutSeconds ?? 300,
asset: getAddress(asset.address),
outputSchema: {
@@ -117,6 +118,7 @@ export async function decodePaymentRequest(
output: outputSchema,
},
extra: {
recipientAddress: payTo, // input payTo is the final recipient address
...((asset as ERC20TokenAmount["asset"]).eip712 ?? {}),
},
});
+1 -1
View File
@@ -34,7 +34,7 @@ export type PaymentArgs = {
facilitator: ThirdwebX402Facilitator;
/** Optional configuration for the payment middleware route */
routeConfig?: PaymentMiddlewareConfig;
/** @deprecated Use facilitator.address instead */
/** Optional recipient address to receive the payment if different from your facilitator address */
payTo?: string;
};
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/react-native-adapter
## 0.1.0
### Minor Changes
- [#8168](https://github.com/thirdweb-dev/js/pull/8168) [`35c7d21`](https://github.com/thirdweb-dev/js/commit/35c7d21e368f9ea5636c8663ba31b7d473b5b343) Thanks [@d4mr](https://github.com/d4mr)! - Added Vault operations support for Solana.
## 0.0.3
### Patch Changes
+1 -13
View File
@@ -1,16 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"extends": "//",
"overrides": [
{
"assist": {
"actions": {
"source": {
"useSortedKeys": "off"
}
}
},
"includes": ["package.json"]
}
]
"extends": "//"
}

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