Compare commits

..
Author SHA1 Message Date
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
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
938c1e2f60 Version Packages (#8185)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-05 01:36:29 +13:00
Joaquim VergesandGitHub af9a73d930 [SDK] Fix waitUntil facilitator parameter not being respected (#8184) 2025-10-04 05:06:10 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
34c5d24043 Version Packages (#8182)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-04 19:33:33 +13:00
MananTank e612f16a85 [MNY-192] SDK: BuyWidget Improvements (#8181)
<!--

## 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 several enhancements to the payment features, including optional props for token and chain selection, improvements to UI components, and the addition of a receiver address input for transactions.

### Detailed summary
- Added `receiverAddress` to various components.
- Made `amount` and `chainId` optional in `BuyWidget`.
- Improved UI for `BuyWidget`, allowing token and chain selection.
- Updated `FundWallet` to handle token selection and input.
- Enhanced `DecimalInput` for better user experience.
- Refactored components for better code organization and readability.

> ✨ 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**
  * Receiver address input for Buy/Pay flows.
  * Editable fiat and token amounts, current balance visible, and wallet disconnect option.
  * Copy icon supports configurable size.

* **Improvements**
  * Chain and amount can be optional; token and chain remain changeable.
  * Enhanced token selection, preset amount buttons, compact active-wallet header, improved theming and responsive layouts.
  * Added story variants (light/dark).

* **Documentation**
  * Examples updated to reflect buy prefill changes.

* **Style**
  * Simplified address text styling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-03 23:52:49 +00:00
Jonas DanielsandGitHub 4a30b9c89a Add Google Click ID tracking to Stripe checkout (#8180) 2025-10-03 15:05:20 -07:00
MananTank ef9cf3409b SDK: Delete Bridge Orchestrator, Refactor Bridge widget components (#8169)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring and updating various components related to payment and transaction handling in the `thirdweb` library. It introduces new props, modifies existing interfaces, and cleans up the codebase by removing unused files.

### Detailed summary
- Deleted several unused files related to payment and transaction handling.
- Updated `transaction` and `payment` components to use new props and interfaces.
- Replaced `type` definitions with `interface` in some components for consistency.
- Improved error handling and state management in transaction modals.
- Enhanced `SuccessScreen` and `UnsupportedTokenScreen` with new props for better UI.
- Modified various story files to reflect component updates and new props.
- Adjusted payment selection and fund wallet components to align with new data structures.

> The following files were skipped due to too many changes: `packages/thirdweb/src/stories/Bridge/CheckoutWidget.stories.tsx`, `packages/thirdweb/src/stories/Bridge/PaymentSelection.stories.tsx`, `packages/thirdweb/src/stories/Bridge/fixtures.ts`, `packages/thirdweb/src/react/web/ui/Bridge/payment-details/PaymentOverview.tsx`, `packages/thirdweb/src/stories/Bridge/PaymentDetails.stories.tsx`, `packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx`, `packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx`, `packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.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**
  * Optional fiat currency display across payment & transaction modals (defaults to USD).
  * Token support detection with a dedicated unsupported-token screen.

* **Refactor**
  * Bridge flows reorganized into modular multi-step UIs (selection, quote, preview, execute, success).
  * Components now use explicit metadata (title/description/image) and explicit currency/mode props; branding handling centralized.

* **Documentation**
  * Storybook reorganized: decorator-based theming, updated/added widget stories, and cleaned-up story exports.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-03 15:36:06 +00:00
Joaquim VergesandGitHub fd20a6cabb [SDK] Deprecate payTo parameter and simplify facilitator implementation (#8177) 2025-10-03 02:32:59 -07:00
Jonas DanielsandGitHub 9095041ea7 Fix invoice status logic and refactor billing components (#8175) 2025-10-02 20:44:22 -07:00
Joaquim VergesandGitHub 2dbb9e2de4 [Dashboard] Move server wallet sections to wallets tab (#8174) 2025-10-02 18:00:43 -07:00
Joaquim VergesandGitHub 00b8adf3a2 [Docs] Simplify x402 documentation (#8173) 2025-10-02 15:02:17 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ae3dfa36c3 Version Packages (#8170)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-02 16:08:01 +13:00
Joaquim VergesandGitHub eb603f8908 [Multi] Update dependencies, improve wagmi adapter (#8167) 2025-10-02 15:42:13 +13:00
Joaquim VergesandGitHub e11b0da20e [Dashboard] Update transaction status endpoint path (#8166) 2025-09-30 16:10:00 -07:00
Joaquim VergesandGitHub 4c51fe6a8f [Dashboard] Add transaction filtering by Queue ID to engine v2 (#8163) 2025-09-30 15:53:15 -07:00
MananTank 64034c0ddb Dashboard: Fix Token Rewards UI not accounting for token decimals (#8164)
<!--

## 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 token rewards by adding `decimals` properties for tokens in various components and improving the retrieval of metadata related to tokens.

### Detailed summary
- Added `decimals` property to `token0` and `token1` in `claim-rewards-page.stories.tsx`.
- Introduced `chainMetadata` in `page.tsx`.
- Updated `ClaimRewardsPageUI` to utilize `decimals` for token amounts.
- Refactored `getUnclaimedFees` to fetch `decimals` and `symbol` for tokens.
- Improved token contract retrieval logic in `getUnclaimedFees`.

> ✨ 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
  - Token amounts now display using each token’s specific decimals.
  - Chain metadata is passed to unclaimed-fees retrieval to source native token symbol and decimals.

- Bug Fixes
  - Removed hardcoded 18-decimal assumption so amounts render correctly for tokens with different precisions.

- Tests
  - Story/test stubs updated to include decimals for token objects for more realistic scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-30 22:37:28 +00:00
Jonas DanielsandGitHub 90c1e576ff Add /payments/x402 to framer-rewrites (#8165) 2025-09-30 15:27:56 -07:00
Joaquim VergesandGitHub 27d5a81c49 [Dashboard] Add transaction filtering by Queue ID and wallet address (#8162) 2025-09-30 15:15:10 -07:00
MananTank dbadc23e04 Dashboard: Disable pay modal in ERC1155 set claim conditions if cost estimation is already done in dashboard (#8161)
<!--

## 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 handling transaction confirmations in the `create-nft-page.tsx` file, specifically addressing scenarios with multiple batches.

### Detailed summary
- Added a conditional check for `totalBatches > 1`.
- If true, it uses `sendAndConfirmTxNoPayModal.mutateAsync(tx)` for transaction confirmation.
- If false, it defaults to `sendAndConfirmTx.mutateAsync(tx)` for single batch transactions.

> ✨ 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**
  * Streamlined multi-batch transaction confirmation when configuring NFT claim conditions. When multiple batches are required and a wallet is connected, a no-payment confirmation modal is used to reduce prompts and improve reliability. Single-batch flows continue to use the standard confirmation path.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-30 19:01:52 +00:00
Joaquim VergesandGitHub 21d5d0b6af [Nebula] Add permanent redirect to thirdweb.com/ai (#8155) 2025-09-30 11:42:56 -07:00
MananTank b2239c91c9 [MNY-214] Dashboard: ERC1155 set claim conditions step UX improvements (#8146)
<!--

## 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 functionality for handling insufficient funds when creating NFTs, enhancing the user experience by allowing users to buy funds directly if their balance is low.

### Detailed summary
- Added `onNotEnoughFunds` callback to handle insufficient funds.
- Implemented logic to calculate total transaction costs.
- Integrated `BuyWidget` to enable users to purchase funds directly.
- Enhanced error handling and user feedback for fund-related issues.

> ✨ 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
  - Gas-cost–aware NFT minting with total cost estimation and balance pre-checks.
  - Insufficient funds handling with a user-facing prompt showing required vs. available balance.
  - Integrated Buy Funds widget with theme and chain awareness, plus retry and back controls.
  - Optional transaction path that skips the pay modal in eligible scenarios.
  - Enhanced batch processing for ERC1155/721 flows with a callback to surface funding issues.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-30 14:51:10 +00:00
166 changed files with 8717 additions and 5896 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: Auto Author Assign
on:
pull_request:
types: [opened, reopened, ready_for_review, draft]
types: [opened, reopened, ready_for_review]
permissions:
pull-requests: write
+1
View File
@@ -10,6 +10,7 @@ module.exports = [
"/wallets",
"/account-abstraction",
"/payments",
"/payments/x402",
"/auth",
"/in-app-wallets",
"/transactions",
@@ -1,6 +1,6 @@
import "server-only";
import { headers } from "next/headers";
import { cookies, headers } from "next/headers";
import Stripe from "stripe";
import type { Team } from "@/api/team/get-team";
import {
@@ -88,6 +88,9 @@ export async function fetchClientSecret(team: Team) {
throw new Error("No customer ID found");
}
// try to get the gclid cookie
const gclid = (await cookies()).get("gclid")?.value;
// Create Checkout Sessions from body params.
const session = await stripe.checkout.sessions.create({
ui_mode: "embedded",
@@ -117,6 +120,8 @@ export async function fetchClientSecret(team: Team) {
missing_payment_method: "cancel",
},
},
// if gclid exists, set it as a metadata field so we can attribute the conversion later
metadata: gclid ? { gclid } : undefined,
},
});
+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);
}
/**
@@ -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,
});
}
}}
@@ -46,7 +46,7 @@ export function AdvancedSearchInput(props: {
value={searchType}
onValueChange={(value) => setSearchType(value as SearchType)}
>
<SelectTrigger className="w-[140px] bg-card">
<SelectTrigger className="w-[140px] bg-background rounded-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -61,7 +61,7 @@ export function AdvancedSearchInput(props: {
<div className="flex flex-1">
<div className="relative flex-1">
<Input
className="bg-card pl-9 border-r-0 rounded-r-none"
className="bg-background pl-9 border-r-0 rounded-r-none rounded-l-full"
placeholder={`Search by ${searchType}...`}
value={query}
onChange={(e) => setQuery(e.target.value)}
@@ -86,7 +86,7 @@ export function AdvancedSearchInput(props: {
onClick={handleSearch}
variant="outline"
disabled={!query.trim() || props.isLoading}
className="rounded-l-none gap-2 bg-card disabled:opacity-100"
className="rounded-l-none rounded-r-full gap-2 bg-background disabled:opacity-100"
>
{props.isLoading && <Spinner className="size-4" />}
Search
@@ -2,7 +2,7 @@
import { createColumnHelper } from "@tanstack/react-table";
import { format } from "date-fns";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { ArrowLeftIcon, ArrowRightIcon, UserIcon } from "lucide-react";
import Papa from "papaparse";
import { useCallback, useMemo, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
@@ -264,11 +264,24 @@ export function InAppWalletUsersPageContent(
return (
<div>
<div className="flex flex-col gap-4">
{/* Top section */}
<div className="flex flex-col gap-4">
<div className="flex flex-col md:flex-row lg:items-center justify-end gap-3">
<div className="w-full max-w-lg">
<div className="overflow-hidden rounded-xl border border-border bg-card">
<div className="flex flex-col lg:flex-row lg:justify-between p-4 lg:px-6 py-5 lg:items-center gap-5">
<div>
<div className="flex mb-3">
<div className="p-2 rounded-full bg-background border border-border">
<UserIcon className="size-5 text-muted-foreground" />
</div>
</div>
<h2 className="font-semibold text-2xl tracking-tight">
User Wallets
</h2>
<p className="text-muted-foreground text-sm">
View and manage your project's users
</p>
</div>
<div className="flex flex-col items-start lg:items-end gap-2.5 border-t lg:border-t-0 pt-5 lg:pt-0 border-dashed">
<div className="w-full lg:w-auto lg:min-w-[320px]">
<AdvancedSearchInput
onSearch={handleSearch}
onClear={handleClearSearch}
@@ -277,7 +290,7 @@ export function InAppWalletUsersPageContent(
/>
</div>
<Button
className="gap-2 bg-card"
className="gap-2 bg-background rounded-full"
disabled={wallets.length === 0 || isPending}
onClick={downloadCSV}
variant="outline"
@@ -298,11 +311,11 @@ export function InAppWalletUsersPageContent(
data={wallets}
isFetched={walletsQuery.isFetched}
isPending={walletsQuery.isPending}
tableContainerClassName="rounded-b-none"
tableContainerClassName="rounded-none border-x-0 border-b-0"
title="in-app wallets"
/>
<div className="flex justify-center gap-3 rounded-b-lg border border-t-0 bg-card p-6">
<div className="flex justify-center gap-3 border-t bg-card p-6">
<Button
className="gap-2 bg-background"
disabled={activePage === 1 || walletsQuery.isPending}
+16 -2
View File
@@ -379,12 +379,27 @@ export function useEngineTransactions(params: {
page?: number;
status?: EngineStatus;
};
id?: string;
}) {
const { instanceUrl, autoUpdate, authToken } = params;
const { instanceUrl, autoUpdate, authToken, id } = params;
return useQuery({
placeholderData: keepPreviousData,
queryFn: async () => {
if (id) {
const res = await fetch(`${instanceUrl}transaction/status/${id}`, {
headers: getEngineRequestHeaders(authToken),
method: "GET",
});
const json = await res.json();
const transaction = (json.result as Transaction) || {};
return {
transactions: [transaction],
totalCount: 1,
};
}
const url = new URL(`${instanceUrl}transaction/get-all`);
if (params.queryParams) {
for (const key in params.queryParams) {
@@ -402,7 +417,6 @@ export function useEngineTransactions(params: {
});
const json = await res.json();
return (json.result as TransactionResponse) || {};
},
queryKey: engineKeys.transactions(instanceUrl, params),
@@ -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>
@@ -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();
@@ -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, 3200+ tokens and 9+ million routes supported
</p>
</div>
</div>
<TokenPage chains={chains} />
@@ -25,6 +25,34 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
import { ThirdwebMiniLogo } from "../../../../../../../components/ThirdwebMiniLogo";
import { searchParams } from "../search-params";
function getStatusBadge(invoice: Stripe.Invoice) {
switch (invoice.status) {
case "paid":
return <Badge variant="success">Paid</Badge>;
case "open":
// we treat "uncollectible" as unpaid
case "uncollectible": {
// if the invoice has a due date, use that, otherwise use the effective at date (which is the date the invoice was created)
const effectiveDueDate = invoice.due_date || invoice.effective_at;
// if the invoice due date is in the past, we want to display it as past due
if (effectiveDueDate && effectiveDueDate * 1000 < Date.now()) {
return <Badge variant="destructive">Past Due</Badge>;
}
return <Badge variant="outline">Open</Badge>;
}
case "void":
return <Badge variant="secondary">Void</Badge>;
default:
return <Badge variant="outline">Unknown</Badge>;
}
}
function isInvoicePayable(invoice: Stripe.Invoice) {
return invoice.status === "open" || invoice.status === "uncollectible";
}
export function BillingHistory(props: {
teamSlug: string;
invoices: Stripe.Invoice[];
@@ -57,27 +85,6 @@ export function BillingHistory(props: {
});
};
const getStatusBadge = (invoice: Stripe.Invoice) => {
switch (invoice.status) {
case "paid":
return <Badge variant="success">Paid</Badge>;
case "open":
// we treate "uncollectible" as unpaid
case "uncollectible": {
// if the invoice due date is in the past, we want to display it as past due
if (invoice.due_date && invoice.due_date * 1000 < Date.now()) {
return <Badge variant="destructive">Past Due</Badge>;
}
return <Badge variant="outline">Open</Badge>;
}
case "void":
return <Badge variant="secondary">Void</Badge>;
default:
return <Badge variant="outline">Unknown</Badge>;
}
};
if (props.invoices.length === 0) {
if (props.status === "open") {
return (
@@ -126,7 +133,7 @@ export function BillingHistory(props: {
<TableCell>{getStatusBadge(invoice)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
{invoice.status === "open" && (
{isInvoicePayable(invoice) && (
<>
{/* always show the crypto payment button */}
<ToolTipLabel
@@ -552,7 +552,7 @@ export function AccountAbstractionSettingsPage(
you build your own rules.{" "}
<UnderlineLink
className="text-primary-500"
href="https://portal.thirdweb.com/wallets/smart-wallet/sponsorship-rules#setting-up-a-server-verifier"
href="https://portal.thirdweb.com/transactions/policies#server-verifier"
rel="noopener noreferrer"
target="_blank"
>
@@ -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,3 +1,4 @@
import { Input } from "@workspace/ui/components/input";
import { format, formatDistanceToNowStrict } from "date-fns";
import {
ArrowLeftIcon,
@@ -110,6 +111,7 @@ export function TransactionsTable(props: {
const [autoUpdate, setAutoUpdate] = useState(true);
const [page, setPage] = useState(1);
const [status, setStatus] = useState<EngineStatus | undefined>(undefined);
const [filterId, setFilterId] = useState<string | undefined>(undefined);
const autoUpdateId = useId();
const pageSize = 10;
const transactionsQuery = useEngineTransactions({
@@ -121,6 +123,7 @@ export function TransactionsTable(props: {
page: page,
status,
},
id: filterId,
});
const transactions = transactionsQuery.data?.transactions ?? [];
@@ -160,6 +163,16 @@ export function TransactionsTable(props: {
onCheckedChange={(v) => setAutoUpdate(!!v)}
/>
</div>
<Input
className="max-w-[250px]"
onChange={(e) => {
const value = e.target.value.trim();
setFilterId(value || undefined);
setPage(1);
}}
placeholder="Filter by Queue ID"
value={filterId || ""}
/>
<StatusSelector
setStatus={(v) => {
setStatus(v);
@@ -74,6 +74,10 @@ export type CreateNFTCollectionFunctions = {
count: number;
};
gasless: boolean;
onNotEnoughFunds: (data: {
requiredAmount: string;
balance: string;
}) => void;
}) => Promise<void>;
lazyMintNFTs: (params: {
values: CreateNFTCollectionAllValues;
@@ -1,6 +1,14 @@
"use client";
import { useRef } from "react";
import { encode, getContract, type ThirdwebClient } from "thirdweb";
import {
encode,
estimateGasCost,
getContract,
getGasPrice,
type PreparedTransaction,
type ThirdwebClient,
toEther,
} from "thirdweb";
import { deployERC721Contract, deployERC1155Contract } from "thirdweb/deploys";
import { multicall } from "thirdweb/extensions/common";
import {
@@ -14,7 +22,8 @@ import {
} from "thirdweb/extensions/erc1155";
import { grantRole } from "thirdweb/extensions/permissions";
import { useActiveAccount } from "thirdweb/react";
import { maxUint256 } from "thirdweb/utils";
import { maxUint256, resolvePromisedValue } from "thirdweb/utils";
import { getWalletBalance } from "thirdweb/wallets";
import { create7702MinimalAccount } from "thirdweb/wallets/smart";
import { revalidatePathAction } from "@/actions/revalidate";
import { reportContractDeployed } from "@/analytics/report";
@@ -39,6 +48,10 @@ export function CreateNFTPage(props: {
const contractAddressRef = useRef<string | undefined>(undefined);
const getChain = useGetV5DashboardChain();
const sendAndConfirmTx = useSendAndConfirmTx();
const sendAndConfirmTxNoPayModal = useSendAndConfirmTx({
payModal: false,
});
function getAccount(params: { gasless: boolean }) {
if (!activeAccount) {
throw new Error("Wallet is not connected");
@@ -130,6 +143,7 @@ export function CreateNFTPage(props: {
contractName: contractType,
deploymentType: "asset",
publisher: "deployer.thirdweb.eth",
is_testnet: chain.testnet,
});
contractAddressRef.current = contractAddress;
@@ -218,6 +232,10 @@ export function CreateNFTPage(props: {
count: number;
};
gasless: boolean;
onNotEnoughFunds: (data: {
requiredAmount: string;
balance: string;
}) => void;
}) {
const { values, batch } = params;
const contract = getDeployedContract({
@@ -277,7 +295,46 @@ export function CreateNFTPage(props: {
data: encodedTransactions,
});
await sendAndConfirmTx.mutateAsync(tx);
// if there are more than one batches, on the first batch, we check if the user has enough funds to cover the cost of all the batches ->
// calculate the cost of one batch, multiply by the number of batches to get the total cost
// if the user does not have enough funds, call the onNotEnoughFunds callback
const totalBatches = Math.ceil(values.nfts.length / batch.count);
if (batch.startIndex === 0 && totalBatches > 1 && !params.gasless) {
if (!activeAccount) {
throw new Error("Wallet is not connected");
}
const costPerBatch = await getTotalTransactionCost({
tx: tx,
from: activeAccount.address,
});
const totalCost = costPerBatch * BigInt(totalBatches);
const walletBalance = await getWalletBalance({
address: activeAccount.address,
chain: contract.chain,
client: contract.client,
});
if (walletBalance.value < totalCost) {
params.onNotEnoughFunds({
balance: toEther(walletBalance.value),
requiredAmount: toEther(totalCost),
});
throw new Error(
`Not enough funds: Required ${toEther(totalCost)}, Balance ${toEther(walletBalance.value)}`,
);
}
await sendAndConfirmTxNoPayModal.mutateAsync(tx);
} else if (totalBatches > 1) {
await sendAndConfirmTxNoPayModal.mutateAsync(tx);
} else {
await sendAndConfirmTx.mutateAsync(tx);
}
}
async function handleSetAdmins(params: {
@@ -390,3 +447,41 @@ function transformSocialUrls(
{} as Record<string, string>,
);
}
async function getTransactionGasCost(tx: PreparedTransaction, from?: string) {
try {
const gasCost = await estimateGasCost({
from,
transaction: tx,
});
const bufferCost = gasCost.wei / 10n;
// Note: get tx.value AFTER estimateGasCost
// add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
return gasCost.wei + bufferCost;
} catch {
if (from) {
// try again without passing from
return await getTransactionGasCost(tx);
}
// fallback if both fail, use the tx value + 1M * gas price
const gasPrice = await getGasPrice({
chain: tx.chain,
client: tx.client,
});
return 1_000_000n * gasPrice;
}
}
async function getTotalTransactionCost(params: {
tx: PreparedTransaction;
from?: string;
}) {
const [txValue, txGasCost] = await Promise.all([
resolvePromisedValue(params.tx.value),
getTransactionGasCost(params.tx, params.from),
]);
return (txValue || 0n) + txGasCost;
}
@@ -1,14 +1,19 @@
"use client";
import {
ArrowLeftIcon,
ArrowRightIcon,
ArrowUpFromLineIcon,
ImageOffIcon,
RefreshCcwIcon,
ShoppingBagIcon,
} from "lucide-react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { useMemo, useRef, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import {
BuyWidget,
TokenProvider,
TokenSymbol,
useActiveAccount,
@@ -37,6 +42,7 @@ import {
import { Skeleton } from "@/components/ui/skeleton";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { parseError } from "@/utils/errorParser";
import { getSDKTheme } from "@/utils/sdk-component-theme";
import { ChainOverview } from "../../_common/chain-overview";
import { StepCard } from "../../_common/step-card";
import { StorageErrorPlanUpsell } from "../../_common/storage-error-upsell";
@@ -71,6 +77,10 @@ export function LaunchNFT(props: {
const walletRequiresApproval = activeWallet?.id !== "inApp";
const account = useActiveAccount();
const contractAddressRef = useRef<string | null>(null);
const { theme } = useTheme();
const chainMeta = useV5DashboardChain(
Number(formValues.collectionInfo.chain),
);
function updateStatus(
index: number,
@@ -161,6 +171,18 @@ export function LaunchNFT(props: {
const [isGasless, setIsGasless] = useState(canEnableGasless);
const showGaslessSection = false; // activeWallet?.id === "inApp";
const [notEnoughFunds, setNotEnoughFunds] = useState<
| false
| {
values: {
requiredAmount: string;
balance: string;
};
isBuySuccess: boolean;
showBuyWidget: boolean;
}
>(false);
const contractLink = contractAddressRef.current
? `/team/${props.teamSlug}/${props.projectSlug}/contract/${formValues.collectionInfo.chain}/${contractAddressRef.current}`
: null;
@@ -204,6 +226,16 @@ export function LaunchNFT(props: {
},
gasless: isGasless,
values: formValues,
onNotEnoughFunds: (data) => {
setNotEnoughFunds({
values: {
balance: data.balance,
requiredAmount: data.requiredAmount,
},
isBuySuccess: false,
showBuyWidget: false,
});
},
});
batchesProcessedRef.current += 1;
@@ -216,6 +248,16 @@ export function LaunchNFT(props: {
},
gasless: isGasless,
values: formValues,
onNotEnoughFunds: (data) => {
setNotEnoughFunds({
values: {
balance: data.balance,
requiredAmount: data.requiredAmount,
},
isBuySuccess: false,
showBuyWidget: false,
});
},
});
}
}
@@ -274,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;
@@ -283,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();
@@ -357,41 +403,143 @@ export function LaunchNFT(props: {
className="gap-0 overflow-hidden p-0 md:max-w-[480px]"
dialogCloseClassName="hidden"
>
<div className="flex flex-col gap-6 p-6">
<DialogHeader className="space-y-0.5">
<DialogTitle className="font-semibold text-xl tracking-tight">
Status
</DialogTitle>
{walletRequiresApproval && (
<DialogDescription>
Each step will prompt a signature request in your wallet
</DialogDescription>
)}
</DialogHeader>
{/** biome-ignore lint/complexity/useOptionalChain: can't change it to optional chain */}
{notEnoughFunds && notEnoughFunds.showBuyWidget ? (
<div className="bg-card">
<div className="border-b py-2 px-2">
<Button
variant="ghost"
size="sm"
onClick={() =>
setNotEnoughFunds({
...notEnoughFunds,
showBuyWidget: false,
})
}
className="gap-2 px-2 text-muted-foreground hover:text-foreground"
>
<ArrowLeftIcon className="size-4" />
Back
</Button>
</div>
<BuyWidget
client={props.client}
chain={chain}
amount={String(
Number(notEnoughFunds.values.requiredAmount) -
Number(notEnoughFunds.values.balance),
)}
buttonLabel={`Buy ${chainMeta?.nativeCurrency?.symbol || "ETH"}`}
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
className="!border-none !w-full !rounded-none"
onSuccess={() => {
setNotEnoughFunds({
...notEnoughFunds,
isBuySuccess: true,
showBuyWidget: false,
});
}}
/>
</div>
) : (
<div className="flex flex-col gap-6 p-6 mb-2">
<DialogHeader className="space-y-0.5">
<DialogTitle className="font-semibold text-xl tracking-tight">
Status
</DialogTitle>
{walletRequiresApproval && (
<DialogDescription>
Each step will prompt a signature request in your wallet
</DialogDescription>
)}
</DialogHeader>
<MultiStepStatus
onRetry={handleRetry}
renderError={(step, errorMessage) => {
if (
props.teamPlan === "free" &&
errorMessage.toLowerCase().includes("storage limit")
) {
return (
<StorageErrorPlanUpsell
onRetry={() => handleRetry(step)}
teamSlug={props.teamSlug}
trackingCampaign="create-nft"
/>
);
}
<MultiStepStatus
onRetry={handleRetry}
renderError={(step, errorMessage) => {
if (
props.teamPlan === "free" &&
errorMessage.toLowerCase().includes("storage limit")
) {
return (
<StorageErrorPlanUpsell
onRetry={() => handleRetry(step)}
teamSlug={props.teamSlug}
trackingCampaign="create-nft"
/>
);
}
return null;
}}
steps={steps}
/>
</div>
if (notEnoughFunds) {
return (
<div>
{notEnoughFunds.isBuySuccess ? (
<p className="text-success-text text-sm mt-1.5 mb-2">
Funds bought successfully
</p>
) : (
<div className="mt-2 mb-2 space-y-1">
<p className="text-destructive-text text-sm">
Not enough funds
</p>
<p className="text-muted-foreground text-sm">
Required Funds:{" "}
{notEnoughFunds.values.requiredAmount}{" "}
{chainMeta?.nativeCurrency?.symbol || "ETH"}
</p>
<p className="text-muted-foreground text-sm">
Balance: {notEnoughFunds.values.balance}{" "}
{chainMeta?.nativeCurrency?.symbol || "ETH"}
</p>
</div>
)}
<div className="mt-2 flex justify-end gap-4 border-border border-t bg-card p-6">
<div className="flex gap-2">
<Button
onClick={() => {
handleRetry(step);
setNotEnoughFunds(false);
}}
size="sm"
variant={
notEnoughFunds.isBuySuccess
? "default"
: "outline"
}
className="gap-2"
>
<RefreshCcwIcon className="size-3.5" />
Retry
</Button>
{!notEnoughFunds.isBuySuccess && (
<Button
onClick={() => {
setNotEnoughFunds({
...notEnoughFunds,
showBuyWidget: true,
});
}}
size="sm"
className="gap-2"
>
<ShoppingBagIcon className="size-3.5" />
Buy Funds
</Button>
)}
</div>
</div>
);
}
return null;
}}
steps={steps}
/>
</div>
)}
<div className="flex justify-end gap-4 border-border border-t bg-card p-6">
{!isComplete ? (
<Button
onClick={() => {
@@ -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) {
@@ -13,6 +13,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
@@ -49,6 +50,8 @@ export function TransactionsTableUI(props: {
getData: (params: {
page: number;
status: TransactionStatus | undefined;
id: string | undefined;
from: string | undefined;
}) => Promise<TransactionsResponse>;
project: Project;
teamSlug: string;
@@ -60,13 +63,15 @@ export function TransactionsTableUI(props: {
const [status, setStatus] = useState<TransactionStatus | undefined>(
undefined,
);
const [id, setId] = useState<string | undefined>(undefined);
const [from, setFrom] = useState<string | undefined>(undefined);
const [page, setPage] = useState(1);
const pageSize = 10;
const transactionsQuery = useQuery({
placeholderData: keepPreviousData,
queryFn: () => props.getData({ page, status }),
queryKey: ["transactions", props.project.id, page, status],
queryFn: () => props.getData({ page, status, id, from }),
queryKey: ["transactions", props.project.id, page, status, id, from],
refetchInterval: autoUpdate ? 4_000 : false,
});
@@ -84,18 +89,18 @@ export function TransactionsTableUI(props: {
return (
<div className="overflow-hidden rounded-lg border border-border bg-card">
<div className="flex flex-col gap-4 rounded-lg rounded-b-none px-6 py-6 lg:flex-row lg:justify-between">
<div>
<h2 className="font-semibold text-2xl tracking-tight">
Transaction History
</h2>
<p className="text-muted-foreground text-sm">
Transactions sent from server wallets
</p>
</div>
<div className="flex flex-col gap-4 rounded-lg rounded-b-none px-6 py-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 className="font-semibold text-2xl tracking-tight">
Transaction History
</h2>
<p className="text-muted-foreground text-sm">
Transactions sent from server wallets
</p>
</div>
<div className="flex items-center justify-end gap-5 border-border border-t pt-4 lg:border-none lg:pt-0">
<div className="flex shrink-0 items-center gap-2">
<div className="flex shrink-0 items-center gap-2 border-border border-t pt-4 lg:border-none lg:pt-0">
<Label htmlFor={autoUpdateId}>Auto Update</Label>
<Switch
checked={autoUpdate}
@@ -103,14 +108,39 @@ export function TransactionsTableUI(props: {
onCheckedChange={(v) => setAutoUpdate(!!v)}
/>
</div>
<StatusSelector
setStatus={(v) => {
setStatus(v);
// reset page
</div>
<div className="flex flex-wrap items-center justify-end gap-3">
<Input
className="max-w-[250px]"
onChange={(e) => {
const value = e.target.value.trim();
setId(value || undefined);
setPage(1);
}}
status={status}
placeholder="Filter by Queue ID"
value={id || ""}
/>
<Input
className="max-w-[250px]"
onChange={(e) => {
const value = e.target.value.trim();
setFrom(value || undefined);
setPage(1);
}}
placeholder="Filter by wallet address"
value={from || ""}
/>
<div>
<StatusSelector
setStatus={(v) => {
setStatus(v);
// reset page
setPage(1);
}}
status={status}
/>
</div>
</div>
</div>
@@ -16,11 +16,13 @@ export function TransactionsTable(props: {
return (
<TransactionsTableUI
client={props.client}
getData={async ({ page, status }) => {
getData={async ({ page, status, id, from }) => {
return await getTransactions({
page,
project: props.project,
status,
id,
from,
});
}}
project={props.project}
@@ -34,10 +36,14 @@ async function getTransactions({
project,
page,
status,
id,
from,
}: {
project: Project;
page: number;
status: TransactionStatus | undefined;
id: string | undefined;
from: string | undefined;
}) {
const transactions = await engineCloudProxy<{ result: TransactionsResponse }>(
{
@@ -52,6 +58,8 @@ async function getTransactions({
limit: "20",
page: page.toString(),
status: status ?? undefined,
id: id ?? undefined,
from: from ?? undefined,
},
},
);
@@ -96,15 +96,22 @@ export function ServerWalletsTableUI({
</div>
<div className="flex flex-col items-start lg:items-end gap-5 border-t lg:border-t-0 pt-5 lg:pt-0 border-dashed">
<SingleNetworkSelector
chainId={selectedChainId}
onChange={setSelectedChainId}
client={client}
disableChainId
className="w-fit min-w-[180px] rounded-full bg-background hover:bg-accent/50"
placeholder="Select network"
popoverContentClassName="!w-[320px] rounded-xl overflow-hidden"
/>
<div className="flex flex-row gap-2.5">
<CreateServerWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
/>
<SingleNetworkSelector
chainId={selectedChainId}
onChange={setSelectedChainId}
client={client}
disableChainId
className="w-fit min-w-[180px] rounded-full bg-background hover:bg-accent/50"
placeholder="Select network"
popoverContentClassName="!w-[320px] rounded-xl overflow-hidden"
/>
</div>
<div className="flex items-center gap-2.5">
<Label
@@ -187,25 +194,10 @@ export function ServerWalletsTableUI({
<XIcon className="size-5 text-muted-foreground" />
</div>
<p className="text-muted-foreground">No server wallets found</p>
<CreateServerWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
/>
</div>
)}
</TableContainer>
{wallets.length > 0 && (
<div className="flex justify-end items-center p-4 py-5 lg:px-6 border-t">
<CreateServerWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
/>
</div>
)}
{totalPages > 1 && (
<div className="flex flex-col items-center border-t p-6">
<div className="mb-4 text-muted-foreground text-sm">
@@ -1,3 +1,4 @@
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
import { redirect } from "next/navigation";
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
import { getAuthToken } from "@/api/auth-token";
@@ -6,10 +7,13 @@ import type { DurationId } from "@/components/analytics/date-range-selector";
import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-filters";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { InAppWalletUsersPageContent } from "@/components/in-app-wallet-users-content/in-app-wallet-users-content";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
import { getFiltersFromSearchParams } from "@/lib/time";
import { loginRedirect } from "@/utils/redirects";
import type { Wallet } from "../transactions/server-wallets/wallet-table/types";
import { ServerWalletsTable } from "../transactions/server-wallets/wallet-table/wallet-table";
import { InAppWalletAnalytics } from "./analytics/chart";
import { InAppWalletsSummary } from "./analytics/chart/Summary";
@@ -20,6 +24,7 @@ export default async function Page(props: {
to?: string;
type?: string;
interval?: string;
page?: string;
}>;
}) {
const [searchParams, params] = await Promise.all([
@@ -40,12 +45,47 @@ export default async function Page(props: {
to: searchParams.to,
});
const project = await getProject(params.team_slug, params.project_slug);
const [vaultClient, project] = await Promise.all([
createVaultClient({
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
}).catch(() => undefined),
getProject(params.team_slug, params.project_slug),
]);
if (!project) {
redirect(`/team/${params.team_slug}`);
}
const projectEngineCloudService = project.services.find(
(service) => service.name === "engineCloud",
);
const managementAccessToken =
projectEngineCloudService?.managementAccessToken;
// Fetch server wallets with pagination (5 per page)
const pageSize = 5;
const currentPage = Number.parseInt(searchParams.page ?? "1");
const eoas =
vaultClient && managementAccessToken
? await listEoas({
client: vaultClient,
request: {
auth: {
accessToken: managementAccessToken,
},
options: {
page: currentPage - 1,
// @ts-expect-error - TODO: fix this
page_size: pageSize,
},
},
})
: { data: { items: [], totalRecords: 0 }, error: null, success: true };
const serverWallets = eoas.data?.items as Wallet[] | undefined;
const client = getClientThirdwebClient({
jwt: authToken,
teamId: project.teamId,
@@ -103,12 +143,34 @@ export default async function Page(props: {
authToken={authToken}
/>
<InAppWalletUsersPageContent
authToken={authToken}
client={client}
projectClientId={project.publishableKey}
teamId={project.teamId}
/>
{/* Server Wallets Section */}
<div className="flex flex-col gap-4">
{eoas.error ? null : (
<ServerWalletsTable
client={client}
currentPage={currentPage}
managementAccessToken={managementAccessToken ?? undefined}
project={project}
teamSlug={params.team_slug}
totalPages={Math.ceil(eoas.data.totalRecords / pageSize)}
totalRecords={eoas.data.totalRecords}
wallets={serverWallets ?? []}
/>
)}
</div>
{/* User Wallets Section */}
<div className="flex flex-col gap-4">
<h2 className="font-semibold text-2xl tracking-tight">
User wallets
</h2>
<InAppWalletUsersPageContent
authToken={authToken}
client={client}
projectClientId={project.publishableKey}
teamId={project.teamId}
/>
</div>
</div>
</ProjectPage>
</ResponsiveSearchParamsProvider>
@@ -30,11 +30,13 @@ function unclaimedFeesStub(token0Amount: bigint, token1Amount: bigint) {
address: "0x1234567890123456789012345678901234567890",
amount: token0Amount,
symbol: "FOO",
decimals: 18,
},
token1: {
address: "0x0987654321098765432109876543210987654321",
amount: token1Amount,
symbol: "BAR",
decimals: 18,
},
};
}
@@ -29,11 +29,13 @@ export function ClaimRewardsPage(props: {
address: string;
amount: bigint;
symbol: string;
decimals: number;
};
token1: {
address: string;
amount: bigint;
symbol: string;
decimals: number;
};
};
chainSlug: string;
@@ -98,11 +100,13 @@ export function ClaimRewardsPageUI(props: {
address: string;
amount: bigint;
symbol: string;
decimals: number;
};
token1: {
address: string;
amount: bigint;
symbol: string;
decimals: number;
};
};
recipient: string;
@@ -275,6 +279,7 @@ function TokenReward(props: {
address: string;
amount: bigint;
symbol: string;
decimals: number;
};
client: ThirdwebClient;
chain: Chain;
@@ -303,7 +308,8 @@ function TokenReward(props: {
</div>
<div className="space-y-0.5">
<p className="font-bold text-sm">
{toTokens(props.token.amount, 18)} {props.token.symbol}
{toTokens(props.token.amount, props.token.decimals)}{" "}
{props.token.symbol}
</p>
<Link
target="_blank"
@@ -31,6 +31,7 @@ export default async function Page(props: {
const chain = info.clientContract.chain;
const assetContractServer = info.serverContract;
const serverClient = assetContractServer.client;
const chainMetadata = info.chainMetadata;
const { address: entrypointContractAddress } =
await getDeployedEntrypointERC20({
@@ -56,6 +57,7 @@ export default async function Page(props: {
// Note: must use server contract/client here
const unclaimedFees = await getUnclaimedFees({
chainMetadata,
positionManager: getContract({
address: reward.positionManager,
chain,
@@ -1,11 +1,14 @@
import type { ThirdwebContract } from "thirdweb";
import { getContract, readContract } from "thirdweb";
import { getAddress, getContract, readContract, ZERO_ADDRESS } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { symbol } from "thirdweb/extensions/common";
import { decimals } from "thirdweb/extensions/erc20";
const maxUint128 = 2n ** 128n - 1n;
export async function getUnclaimedFees(params: {
positionManager: ThirdwebContract;
chainMetadata: ChainMetadata;
reward: {
tokenId: bigint;
recipient: string;
@@ -56,33 +59,60 @@ export async function getUnclaimedFees(params: {
const token0Address = positionsResult[2];
const token1Address = positionsResult[3];
const [token0Symbol, token1Symbol] = await Promise.all([
symbol({
contract: getContract({
address: token0Address,
chain,
client,
}),
}),
symbol({
contract: getContract({
address: token1Address,
chain,
client,
}),
}),
]);
const token0Contract = getContract({
address: token0Address,
chain,
client,
});
const token1Contract = getContract({
address: token1Address,
chain,
client,
});
const isToken0Native = getAddress(token0Address) === getAddress(ZERO_ADDRESS);
const isToken1Native = getAddress(token1Address) === getAddress(ZERO_ADDRESS);
const nativeSymbol = params.chainMetadata.nativeCurrency.symbol;
const nativeDecimals = params.chainMetadata.nativeCurrency.decimals;
const [token0Symbol, token1Symbol, token0Decimals, token1Decimals] =
await Promise.all([
isToken0Native
? nativeSymbol
: symbol({
contract: token0Contract,
}),
isToken1Native
? nativeSymbol
: symbol({
contract: token1Contract,
}),
isToken0Native
? nativeDecimals
: decimals({
contract: token0Contract,
}),
isToken1Native
? nativeDecimals
: decimals({
contract: token1Contract,
}),
]);
return {
token0: {
address: token0Address,
amount: collectResult[0],
symbol: token0Symbol,
decimals: token0Decimals,
},
token1: {
address: token1Address,
amount: collectResult[1],
symbol: token1Symbol,
decimals: token1Decimals,
},
};
}
@@ -21,6 +21,7 @@ export function UniversalBridgeEmbed({
buyAmount={amount}
tokenAddress={token?.address}
pageType="bridge"
isTestnet={chain.testnet}
/>
);
}
+2 -3
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 3,200+ tokens across 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality";
export const metadata: Metadata = {
description,
@@ -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>
);
}
+37 -8
View File
@@ -7,6 +7,20 @@ import { defineDashboardChain } from "@/lib/defineDashboardChain";
import { isLoginRequired } from "@/utils/auth";
import { LAST_VISITED_TEAM_PAGE_PATH } from "./app/(app)/team/components/last-visited-page/consts";
type CookiesToSet = Record<
string,
| [string]
| [
string,
{
httpOnly: boolean;
sameSite?: "lax" | "strict" | "none";
secure: boolean;
maxAge: number;
},
]
>;
// ignore assets, api - only intercept page routes
export const config = {
matcher: [
@@ -24,12 +38,12 @@ export const config = {
};
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const { pathname, searchParams } = request.nextUrl;
// nebula subdomain handling
const paths = pathname.slice(1).split("/");
let cookiesToSet: Record<string, string> | undefined;
let cookiesToSet: CookiesToSet = {};
const activeAccount = request.cookies.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const authCookie = activeAccount
@@ -56,12 +70,27 @@ export async function middleware(request: NextRequest) {
cookiesToSet = {};
}
cookiesToSet[key] = value;
cookiesToSet[key] = [value];
}
}
}
}
// handle gclid (if it exists in search params)
const gclid = searchParams.get("gclid");
if (gclid) {
cookiesToSet.gclid = [
gclid,
{
// TODO: define conversion window, for now 7d should do fine
maxAge: 7 * 24 * 60 * 60,
httpOnly: false,
sameSite: "lax",
secure: true,
},
];
}
// logged in paths
if (isLoginRequired(pathname)) {
// check if the user is logged in (has a valid auth cookie)
@@ -146,7 +175,7 @@ export async function middleware(request: NextRequest) {
if (cookiesToSet) {
const defaultResponse = NextResponse.next();
for (const entry of Object.entries(cookiesToSet)) {
defaultResponse.cookies.set(entry[0], entry[1]);
defaultResponse.cookies.set(entry[0], entry[1][0], entry[1][1]);
}
return defaultResponse;
@@ -162,7 +191,7 @@ function isPossibleAddressOrENSName(address: string) {
function rewrite(
request: NextRequest,
relativePath: string,
cookiesToSet: Record<string, string> | undefined,
cookiesToSet: CookiesToSet,
) {
const url = request.nextUrl.clone();
url.pathname = relativePath;
@@ -170,7 +199,7 @@ function rewrite(
if (cookiesToSet) {
for (const entry of Object.entries(cookiesToSet)) {
res.cookies.set(entry[0], entry[1]);
res.cookies.set(entry[0], entry[1][0], entry[1][1]);
}
}
@@ -184,7 +213,7 @@ function redirect(
| {
searchParams?: string;
permanent?: boolean;
cookiesToSet?: Record<string, string> | undefined;
cookiesToSet?: CookiesToSet;
}
| undefined,
) {
@@ -196,7 +225,7 @@ function redirect(
if (options?.cookiesToSet) {
for (const entry of Object.entries(options.cookiesToSet)) {
res.cookies.set(entry[0], entry[1]);
res.cookies.set(entry[0], entry[1][0], entry[1][1]);
}
}
+9
View File
@@ -64,6 +64,15 @@ const baseNextConfig: NextConfig = {
},
productionBrowserSourceMaps: false,
reactStrictMode: true,
async redirects() {
return [
{
source: "/:path*",
destination: "https://thirdweb.com/ai",
permanent: true,
},
];
},
async rewrites() {
return [
{
@@ -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 />
@@ -1,10 +1,90 @@
import { NextResponse } from "next/server";
import { type NextRequest, NextResponse } from "next/server";
import { createThirdwebClient, defineChain } from "thirdweb";
import { toUnits } from "thirdweb/utils";
import { facilitator, settlePayment } from "thirdweb/x402";
import { token } from "../../payments/x402/components/constants";
// Allow streaming responses up to 5 minutes
export const maxDuration = 300;
export async function GET(_req: Request) {
return NextResponse.json({
success: true,
message: "Payment successful. You have accessed the protected route.",
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;
const chainId = queryParams.get("chainId");
if (!chainId) {
return NextResponse.json(
{ error: "Missing required parameters" },
{ status: 400 },
);
}
const amount = queryParams.get("amount") || "0.01";
const tokenAddress = queryParams.get("tokenAddress") || token.address;
const decimals = queryParams.get("decimals") || token.decimals.toString();
const waitUntil =
(queryParams.get("waitUntil") as "simulated" | "submitted" | "confirmed") ||
"simulated";
const result = await settlePayment({
resourceUrl: "https://playground-web.thirdweb.com/api/paywall",
method: "GET",
paymentData,
network: defineChain(Number(chainId)),
price: {
amount: toUnits(amount, parseInt(decimals)).toString(),
asset: {
address: tokenAddress as `0x${string}`,
decimals: decimals ? parseInt(decimals) : token.decimals,
},
},
routeConfig: {
description: "Access to paid content",
},
waitUntil,
facilitator: twFacilitator,
});
if (result.status === 200) {
// payment successful, execute the request
return NextResponse.json(
{
success: true,
message: "Payment successful. You have accessed the protected route.",
payment: {
amount,
tokenAddress,
},
receipt: result.paymentReceipt,
},
{
status: 200,
headers: result.responseHeaders,
},
);
}
// otherwise, request payment
return NextResponse.json(result.responseBody, {
status: result.status,
headers: result.responseHeaders,
});
}
@@ -18,6 +18,7 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
sellerAddress: "0x0000000000000000000000000000000000000000",
title: "",
transactionData: "",
receiverAddress: undefined,
currency: "USD",
showThirdwebBranding: true,
},
@@ -118,8 +118,12 @@ tokenId: 2n,
tokenAddress: options.payOptions.buyTokenAddress
? quotes(options.payOptions.buyTokenAddress)
: undefined,
seller: options.payOptions.sellerAddress
? quotes(options.payOptions.sellerAddress)
seller:
widget === "checkout"
? quotes(options.payOptions.sellerAddress)
: undefined,
receiverAddress: options.payOptions.receiverAddress
? quotes(options.payOptions.receiverAddress)
: undefined,
buttonLabel: options.payOptions.buttonLabel
? quotes(options.payOptions.buttonLabel)
@@ -140,6 +140,7 @@ export function LeftSection(props: {
<div className="flex flex-col gap-2">
<Label>Token</Label>
<TokenSelector
disableAddress
chainId={selectedChain}
client={THIRDWEB_CLIENT}
enabled={true}
@@ -154,10 +155,10 @@ export function LeftSection(props: {
)}
{/* Mode-specific form fields */}
<div className="my-2">
<div className="">
{/* Buy Mode - Amount and Payment Methods */}
{props.widget === "buy" && (
<div className="space-y-4">
<div className="space-y-6">
<div className="flex flex-col gap-2">
<Label htmlFor={buyTokenAmountId}>Amount</Label>
<Input
@@ -177,6 +178,30 @@ export function LeftSection(props: {
/>
</div>
{props.widget === "buy" && (
<div className="flex flex-col gap-2">
<Label htmlFor="receiver-address">Receiver Address</Label>
<p className="text-muted-foreground text-sm">
Receive the tokens in a different wallet address
</p>
<Input
className="bg-card"
id="receiver-address"
onChange={(e) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
receiverAddress: e.target.value as Address,
},
}));
}}
placeholder="0x..."
value={payOptions.receiverAddress || ""}
/>
</div>
)}
{/* Payment Methods */}
<div className="flex flex-col gap-3">
<Label>Payment Methods</Label>
@@ -206,6 +231,7 @@ export function LeftSection(props: {
/>
<Label htmlFor={cryptoPaymentId}>Crypto</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
checked={payOptions.paymentMethods.includes("card")}
@@ -257,7 +283,6 @@ export function LeftSection(props: {
value={payOptions.sellerAddress || ""}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor={paymentAmountId}>Price</Label>
<Input
@@ -67,6 +67,12 @@ export function RightSection(props: {
tokenAddress={props.options.payOptions.buyTokenAddress}
currency={props.options.payOptions.currency}
showThirdwebBranding={props.options.payOptions.showThirdwebBranding}
receiverAddress={props.options.payOptions.receiverAddress}
key={JSON.stringify({
amount: props.options.payOptions.buyTokenAmount,
chain: props.options.payOptions.buyTokenChain,
tokenAddress: props.options.payOptions.buyTokenAddress,
})}
{...(props.options.payOptions.buttonLabel && {
buttonLabel: props.options.payOptions.buttonLabel,
})}
@@ -89,7 +95,6 @@ export function RightSection(props: {
}
name={props.options.payOptions.title || "Your Product Name"}
paymentMethods={props.options.payOptions.paymentMethods}
presetOptions={[1, 2, 3]}
seller={props.options.payOptions.sellerAddress}
theme={themeObj}
tokenAddress={props.options.payOptions.buyTokenAddress}
@@ -48,6 +48,7 @@ export type BridgeComponentsPlaygroundOptions = {
// direct_payment mode options
sellerAddress: Address;
receiverAddress: Address | undefined;
// transaction mode options
transactionData?: string; // Simplified for demo; could be more complex in real implementation
@@ -17,6 +17,7 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
paymentMethods: ["crypto", "card"],
sellerAddress: "0x0000000000000000000000000000000000000000",
title: "",
receiverAddress: undefined,
transactionData: "",
currency: "USD",
showThirdwebBranding: true,
@@ -20,6 +20,7 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
transactionData: "",
currency: "USD",
showThirdwebBranding: true,
receiverAddress: undefined,
},
theme: {
darkColorOverrides: {},
@@ -44,7 +44,6 @@ export function X402LeftSection(props: {
const chainId = useId();
const tokenId = useId();
const amountId = useId();
const payToId = useId();
const waitUntilId = useId();
const handleChainChange = (chainId: number) => {
@@ -82,13 +81,6 @@ 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",
) => {
@@ -148,22 +140,6 @@ 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>
-90
View File
@@ -1,90 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { createThirdwebClient, defineChain } from "thirdweb";
import { toUnits } from "thirdweb/utils";
import { facilitator, settlePayment } from "thirdweb/x402";
import { token } from "./app/payments/x402/components/constants";
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"}`;
function createFacilitator(
waitUntil: "simulated" | "submitted" | "confirmed" = "simulated",
) {
return facilitator({
baseUrl: `${API_URL}/v1/payments/x402`,
client,
serverWalletAddress: BACKEND_WALLET_ADDRESS,
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
waitUtil: waitUntil,
});
}
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const method = request.method.toUpperCase();
const resourceUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}${pathname}`;
const paymentData = request.headers.get("X-PAYMENT");
const queryParams = request.nextUrl.searchParams;
const chainId = queryParams.get("chainId");
const payTo = queryParams.get("payTo");
if (!chainId || !payTo) {
return NextResponse.json(
{ error: "Missing required parameters" },
{ status: 400 },
);
}
const amount = queryParams.get("amount") || "0.01";
const tokenAddress = queryParams.get("tokenAddress") || token.address;
const decimals = queryParams.get("decimals") || token.decimals.toString();
const waitUntil =
(queryParams.get("waitUntil") as "simulated" | "submitted" | "confirmed") ||
"simulated";
const result = await settlePayment({
resourceUrl,
method,
paymentData,
payTo: payTo as `0x${string}`,
network: defineChain(Number(chainId)),
price: {
amount: toUnits(amount, parseInt(decimals)).toString(),
asset: {
address: tokenAddress as `0x${string}`,
decimals: decimals ? parseInt(decimals) : token.decimals,
},
},
routeConfig: {
description: "Access to paid content",
},
facilitator: createFacilitator(waitUntil),
});
if (result.status === 200) {
// payment successful, execute the request
const response = NextResponse.next();
for (const [key, value] of Object.entries(result.responseHeaders)) {
response.headers.set(key, value);
}
return response;
}
// otherwise, request payment
return NextResponse.json(result.responseBody, {
status: result.status,
headers: result.responseHeaders,
});
}
// Configure which paths the middleware should run on
export const config = {
matcher: ["/api/paywall"],
};
+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>
+5 -1
View File
@@ -80,9 +80,13 @@ export const sidebar: SideBar = {
href: `${paymentsSlug}/x402/server`,
name: "Server Side",
},
{
href: `${paymentsSlug}/x402/agents`,
name: "Agents",
},
{
href: `${paymentsSlug}/x402/facilitator`,
name: "Facilitator API",
name: "Facilitator",
},
],
name: "x402",
@@ -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
@@ -68,46 +77,18 @@ The client library wraps the native `fetch` API and handles:
<TabsContent value="http">
## Prepare Payment
Create a signed payment header from the authenticated user to include in your API request:
Fetch any x402-compatible API and automatically handle payment flows when APIs return a `402 Payment Required` response using the authenticated wallet.
### Making the Paid Request
After preparing the payment, include it in your API request:
Only requires passing a 'url' and 'from' query parameters with the authenticated wallet address (server or user wallet) to complete the payment.
```bash
# First, make the initial request to get payment requirements
curl -X GET https://api.example.com/paid-endpoint
# Response will be 402 Payment Required with payment details
# {
# "x402Version": 1,
# "accepts": [...payment requirements...],
# "error": "Payment required"
# }
# Select one of the payment methods, and sign the payment authorization using the API
curl -X POST https://api.thirdweb.com/v1/payments/x402/prepare \
curl -X POST https://api.thirdweb.com/v1/payments/x402/fetch?url=https://api.example.com/premium&from=0x1234... \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user-access-token>" \
-d '{
"from": "0x1234...",
"paymentRequirements": { ... }
}'
# Response will contain the signed payment header
# {
# "paymentPayload": { ... },
# "paymentHeader": "..." // base64 encoded payment header
# }
# Finally, make the request with the payment header
curl -X GET https://api.example.com/paid-endpoint \
-H "X-PAYMENT: <base64-encoded-payment>"
-H "Authorization: Bearer <user-wallet-token>" \
-d '{ ... }' # request body passed through to the url called.
```
## API Reference
<OpenApiEndpoint path="/v1/payments/x402/prepare" method="POST" />
<OpenApiEndpoint path="/v1/payments/x402/fetch" method="POST" />
</TabsContent>
</Tabs>
@@ -1,7 +1,16 @@
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint, Callout } from "@doc";
import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint, Callout, createMetadata } from "@doc";
import { TypeScriptIcon, EngineIcon } from "@/icons";
# Facilitator API
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.
@@ -16,6 +25,13 @@ The thirdweb facilitator is compatible with any x402 backend and middleware libr
You can view all transactions processed by your facilitator in your project dashboard.
## Chain and token support
Our facilitator supports payments on any EVM chain, as long as the payment token supports either:
- ERC-2612 permit (most ERC20 tokens)
- ERC-3009 sign with authorization (USDC on all chains)
<Tabs defaultValue="typescript">
<TabsList>
<TabsTrigger value="typescript" className="flex items-center [&>p]:mb-0">
+14 -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.
@@ -33,6 +42,8 @@ const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
const response = await fetchWithPay('https://api.example.com/paid-endpoint');
```
You can also use the thirdweb API to fetch any x402 compatible endpoint and pay for it with the authenticated wallet. See the [client side docs](/payments/x402/client) for more details.
## Server Side
To make your API calls payable, use the `settlePayment` function in a middleware or endpoint:
@@ -77,6 +88,8 @@ export async function GET(request: Request) {
}
```
You can also create middlewares to handle payment for multiple endpoints, see the [server side docs](/payments/x402/server) for more details. The [facilitator](/payments/x402/facilitator) handles settling the payment onchain using your own server wallet.
## Going Further
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
@@ -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,63 @@
# Gas Sponsorship Policies
Sponsorship policies are a way to control the execution of sponsored transactions on your account.
You can use them to:
- Set global spend limits.
- Restrict sponsorship to a particular chain.
- Restrict sponsorship to a particular contract.
- Allow/Disallow specific wallets
- Setup your own server verifier.
These policies can be set from your project dashboard, under project > account abstraction > settings.
## Server Verifier
The most flexible and powerful way to control the execution of sponsored transactions is to setup your own server verifier.
In the project dashboard, under project > account abstraction > settings, setup your backend URL and any additional headers you need to pass to the verifier.
On every user transaction, your backend will be called with the transaction data and the user's wallet address, your endpoint should return a boolean indicating whether the transaction should be sponsored.
### Backend endpoint specification:
Your backend will be called with the following request:
```http
POST https://your-backend-url/verify-transaction
Content-Type: application/json
Referer: "https://api.thirdweb.com",
# additional custom headers
# request body
{
"clientId": string;
"chainId": number;
"userOp": {
sender: string;
targets: string[];
gasLimit: string;
gasPrice: string;
data?: {
targets: string[];
callDatas: string[];
values: string[];
};
}
}
```
Your backend should process the request, apply your own policy logic based on the request data and return the following JSON response:
```http
HTTP/1.1 200 OK
Content-Type: application/json
{
"isAllowed": boolean;
"reason"?: string;
}
```
The `isAllowed` field indicates whether the transaction should be sponsored or not. `reason` is an optional field that can be used to provide a reason for the decision.
@@ -42,6 +42,10 @@ export const sidebar: SideBar = {
href: `${transactionsSlug}/session-keys`,
name: "Session Keys",
},
{
href: `${transactionsSlug}/policies`,
name: "Sponsorship Policies",
},
],
},
{ separator: true },
+1
View File
@@ -0,0 +1 @@
VITE_THIRDWEB_CLIENT_ID=
+26
View File
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
+1
View File
@@ -0,0 +1 @@
legacy-peer-deps = true
+33
View File
@@ -0,0 +1,33 @@
# wagmi-inapp
## 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
- Updated dependencies [[`af9a73d`](https://github.com/thirdweb-dev/js/commit/af9a73d9302078a506b7d3cb8d55b611f15b6293)]:
- thirdweb@5.108.8
- @thirdweb-dev/wagmi-adapter@0.2.158
## 0.0.2
### Patch Changes
- Updated dependencies [[`e612f16`](https://github.com/thirdweb-dev/js/commit/e612f16a85ffda024d7f74b41abb48b2999b9107)]:
- thirdweb@5.108.7
- @thirdweb-dev/wagmi-adapter@0.2.157
## 0.0.1
### Patch Changes
- Updated dependencies [[`eb603f8`](https://github.com/thirdweb-dev/js/commit/eb603f89086f25202b157eb748f70a252dd7c6e7), [`eb603f8`](https://github.com/thirdweb-dev/js/commit/eb603f89086f25202b157eb748f70a252dd7c6e7)]:
- @thirdweb-dev/wagmi-adapter@0.2.156
- thirdweb@5.108.6
+37
View File
@@ -0,0 +1,37 @@
# Thirdweb Wagmi Adapter
This is a demo of the [thirdweb Wagmi Adapter](https://github.com/thirdweb-dev/js/tree/main/packages/wagmi-adapter).
It demonstrates how to connect [in-app smart wallets](https://portal.thirdweb.com/connect/wallet/sign-in-methods/configure) to a Wagmi app.
Users connect with google, and obtain a ERC-4337 smart account that can be used to interact with the app without paying for gas.
```ts
// src/wagmi.ts
export const config = createConfig({
chains: [chain],
connectors: [
inAppWalletConnector({
client,
smartAccount: {
chain: thirdwebChain(chain),
sponsorGas: true,
},
}),
],
transports: {
[chain.id]: http(),
},
});
```
## Prerequisites
Copy the `.env.example` file to `.env` and set your Thirdweb client ID. You can get your client ID from the [thirdweb dashboard](https://thirdweb.com).
## How to run
```bash
pnpm install
pnpm dev
```
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"extends": "//",
"overrides": [
{
"assist": {
"actions": {
"source": {
"useSortedKeys": "off"
}
}
},
"includes": ["package.json"]
}
]
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Create Wagmi</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
{
"name": "wagmi-inapp",
"private": true,
"version": "0.0.4",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"fix": "biome check ./src --fix",
"format": "biome format ./src --write",
"lint": "biome check ./src"
},
"dependencies": {
"@tanstack/react-query": "5.81.5",
"@thirdweb-dev/wagmi-adapter": "workspace:*",
"react": "19.1.0",
"react-dom": "19.1.0",
"thirdweb": "workspace:*",
"viem": "2.37.9",
"wagmi": "2.17.5"
},
"devDependencies": {
"@biomejs/biome": "2.0.6",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@vitejs/plugin-react": "5.0.4",
"@wagmi/cli": "latest",
"buffer": "^6.0.3",
"typescript": "5.8.3",
"vite": "7.1.7"
}
}
+112
View File
@@ -0,0 +1,112 @@
import type { ConnectionOptions } from "@thirdweb-dev/wagmi-adapter";
import { ConnectButton } from "thirdweb/react";
import {
useAccount,
useConnect,
useDisconnect,
useSendTransaction,
} from "wagmi";
import { chain, client } from "./wagmi.js";
function App() {
const account = useAccount();
const { connectors, connect, status, error } = useConnect();
const { disconnect } = useDisconnect();
const {
sendTransaction,
isPending,
isSuccess,
isError,
error: sendTxError,
data: sendTxData,
} = useSendTransaction();
return (
<>
<div>
<h2>Account</h2>
<div>
status: {account.status}
<br />
addresses: {JSON.stringify(account.addresses)}
<br />
chainId: {account.chainId}
</div>
{account.status === "connected" && (
<button type="button" onClick={() => disconnect()}>
Disconnect
</button>
)}
</div>
<div>
<h2>Connect</h2>
<ConnectButton
client={client}
onConnect={(wallet) => {
// auto connect to wagmi on tw connect
const twConnector = connectors.find(
(c) => c.id === "in-app-wallet",
);
if (twConnector) {
const options = {
wallet,
} satisfies ConnectionOptions;
connect({
connector: twConnector,
chainId: chain.id,
...options,
});
}
}}
/>
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => {
if (connector.id === "in-app-wallet") {
const connectOptions = {
strategy: "google",
} satisfies ConnectionOptions;
connect({
connector,
chainId: chain.id,
...connectOptions,
});
} else {
connect({ connector, chainId: chain.id });
}
}}
type="button"
>
{connector.name}
</button>
))}
<div>{status}</div>
<div>{error?.message}</div>
</div>
{account && (
<div>
<h2>Transact</h2>
<button
onClick={() => sendTransaction({ to: account.address, value: 0n })}
type="button"
>
Send Tx
</button>
<div>{isPending ? "Sending..." : ""}</div>
<div>
{isSuccess
? `Success: ${sendTxData}`
: isError
? sendTxError?.message
: ""}
</div>
</div>
)}
</>
);
}
export default App;
+21
View File
@@ -0,0 +1,21 @@
:root {
background-color: #181818;
color: rgba(255, 255, 255, 0.87);
color-scheme: light dark;
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-synthesis: none;
font-weight: 400;
line-height: 1.5;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
@media (prefers-color-scheme: light) {
:root {
background-color: #f8f8f8;
color: #181818;
}
}
+28
View File
@@ -0,0 +1,28 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Buffer } from "buffer";
import React from "react";
import ReactDOM from "react-dom/client";
import { WagmiProvider } from "wagmi";
import App from "./App.js";
import { config } from "./wagmi.js";
import "./index.css";
import { ThirdwebProvider } from "thirdweb/react";
globalThis.Buffer = Buffer;
const queryClient = new QueryClient();
// biome-ignore lint/style/noNonNullAssertion: always exists
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThirdwebProvider>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</WagmiProvider>
</ThirdwebProvider>
</React.StrictMode>,
);
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+41
View File
@@ -0,0 +1,41 @@
import { inAppWalletConnector } from "@thirdweb-dev/wagmi-adapter";
import { createThirdwebClient, defineChain as thirdwebChain } from "thirdweb";
import { createConfig, http } from "wagmi";
import { baseSepolia } from "wagmi/chains";
const clientId = import.meta.env.VITE_THIRDWEB_CLIENT_ID;
if (!clientId) {
throw new Error(
"VITE_THIRDWEB_CLIENT_ID is not set. Get your client ID from https://thirdweb.com",
);
}
export const client = createThirdwebClient({
clientId,
});
export const chain = baseSepolia;
export const thirdwebChainForWallet = thirdwebChain(baseSepolia.id);
export const config = createConfig({
chains: [chain],
connectors: [
inAppWalletConnector({
client,
smartAccount: {
chain: thirdwebChain(chain.id),
sponsorGas: true,
},
}),
],
transports: {
[chain.id]: http(),
},
});
declare module "wagmi" {
interface Register {
config: typeof config;
}
}
+47
View File
@@ -0,0 +1,47 @@
{
// This tsconfig file contains the shared config for the build (tsconfig.build.json) and type checking (tsconfig.json) config.
"include": [],
"compilerOptions": {
// Incremental builds
// NOTE: Enabling incremental builds speeds up `tsc`. Keep in mind though that it does not reliably bust the cache when the `tsconfig.json` file changes.
"incremental": false,
// Type checking
"strict": true,
"useDefineForClassFields": true, // Not enabled by default in `strict` mode unless we bump `target` to ES2022.
"noFallthroughCasesInSwitch": true, // Not enabled by default in `strict` mode.
"noImplicitReturns": true, // Not enabled by default in `strict` mode.
"useUnknownInCatchVariables": true, // TODO: This would normally be enabled in `strict` mode but would require some adjustments to the codebase.
"noImplicitOverride": true, // Not enabled by default in `strict` mode.
"noUnusedLocals": true, // Not enabled by default in `strict` mode.
"noUnusedParameters": true, // Not enabled by default in `strict` mode.
"exactOptionalPropertyTypes": false, // Not enabled by default in `strict` mode.
"noUncheckedIndexedAccess": true, // Not enabled by default in `strict` mode.
// JavaScript support
"allowJs": false,
"checkJs": false,
// Interop constraints
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"importHelpers": true, // This is only used for build validation. Since we do not have `tslib` installed, this will fail if we accidentally make use of anything that'd require injection of helpers.
// Language and environment
"moduleResolution": "NodeNext",
"module": "NodeNext",
"target": "ES2021", // Setting this to `ES2021` enables native support for `Node v16+`: https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping.
"lib": [
"ES2022", // By using ES2022 we get access to the `.cause` property on `Error` instances.
"DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped.
],
// Skip type checking for node modules
"skipLibCheck": true,
// jsx for "/react" portion
"jsx": "react-jsx"
}
}
+10
View File
@@ -0,0 +1,10 @@
{
// This configuration is used for local development and type checking.
"extends": "./tsconfig.base.json",
"include": ["src"],
"exclude": ["node_modules"],
// "references": [{ "path": "./scripts/tsconfig.json" }],
"compilerOptions": {
"baseUrl": "."
}
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "vite";
// https://vitejs.dev/config/
/** @type {import('vite').UserConfig} */
export default defineConfig({
server: {
port: 3000,
},
});
+1
View File
@@ -65,6 +65,7 @@
"playground": "turbo run dev --filter=./apps/playground-web --filter=./packages/thirdweb --filter=./packages/insight --filter=./packages/engine --filter=./packages/nebula --filter=./packages/ai-sdk-provider",
"playground:build": "turbo run build --filter=./apps/playground-web",
"portal": "turbo run dev --filter=./apps/portal --filter=./packages/thirdweb --filter=./packages/insight --filter=./packages/engine --filter=./packages/nebula",
"wagmi-demo": "turbo run dev --filter=./apps/wagmi-demo --filter=./packages/thirdweb --filter=./packages/wagmi-adapter",
"portal:build": "turbo run build --filter=./apps/portal",
"prefix": "pnpm dlx sherif@latest -i remark-gfm -i eslint --fix",
"preinstall": "npx only-allow pnpm",
+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 {
+28
View File
@@ -1,5 +1,33 @@
# @thirdweb-dev/nebula
## 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
- Updated dependencies [[`af9a73d`](https://github.com/thirdweb-dev/js/commit/af9a73d9302078a506b7d3cb8d55b611f15b6293)]:
- thirdweb@5.108.8
## 0.2.60
### Patch Changes
- Updated dependencies [[`e612f16`](https://github.com/thirdweb-dev/js/commit/e612f16a85ffda024d7f74b41abb48b2999b9107)]:
- thirdweb@5.108.7
## 0.2.59
### Patch Changes
- Updated dependencies [[`eb603f8`](https://github.com/thirdweb-dev/js/commit/eb603f89086f25202b157eb748f70a252dd7c6e7)]:
- thirdweb@5.108.6
## 0.2.58
### 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.58"
"version": "0.2.62"
}
+29
View File
@@ -1,5 +1,34 @@
# thirdweb
## 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
- [#8184](https://github.com/thirdweb-dev/js/pull/8184) [`af9a73d`](https://github.com/thirdweb-dev/js/commit/af9a73d9302078a506b7d3cb8d55b611f15b6293) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix waitUntil facilitator param not being respected
## 5.108.7
### Patch Changes
- [#8181](https://github.com/thirdweb-dev/js/pull/8181) [`e612f16`](https://github.com/thirdweb-dev/js/commit/e612f16a85ffda024d7f74b41abb48b2999b9107) Thanks [@MananTank](https://github.com/MananTank)! - BuyWidget UI improvements and new features:
- `chain`, and `amount` props are now optional
- User can always change the token and chain selection in the widget
- Both fiat and token amounts are editable
- connected wallet can be disconnected from the widget
- current balance displayed in the widget
## 5.108.6
### Patch Changes
- [#8167](https://github.com/thirdweb-dev/js/pull/8167) [`eb603f8`](https://github.com/thirdweb-dev/js/commit/eb603f89086f25202b157eb748f70a252dd7c6e7) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update dependencies
## 5.108.5
### Patch Changes
+4 -4
View File
@@ -39,7 +39,7 @@
"qrcode": "1.5.3",
"toml": "3.0.0",
"uqr": "0.1.2",
"viem": "2.33.2",
"viem": "2.37.9",
"x402": "0.6.1",
"zod": "3.25.75"
},
@@ -67,7 +67,7 @@
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@viem/anvil": "0.0.10",
"@vitejs/plugin-react": "^4.6.0",
"@vitejs/plugin-react": "5.0.4",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"dotenv-mono": "^1.3.14",
@@ -94,7 +94,7 @@
"typedoc": "0.27.9",
"typedoc-better-json": "0.9.4",
"typescript": "5.8.3",
"vite": "7.0.1",
"vite": "7.1.7",
"vitest": "3.2.4"
},
"engines": {
@@ -430,5 +430,5 @@
}
},
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "5.108.5"
"version": "5.108.9"
}
+3 -2
View File
@@ -432,7 +432,8 @@ type WalletToViemOptions = {
function walletToViem(options: WalletToViemOptions): WalletClient {
const { wallet, chain, client } = options;
if (!wallet.getAccount()) {
const account = wallet.getAccount();
if (!account) {
throw new Error("Wallet is not connected.");
}
@@ -456,7 +457,7 @@ function walletToViem(options: WalletToViemOptions): WalletClient {
wallet,
});
return createWalletClient({
account: wallet.getAccount()?.address,
account: getAddress(account.address),
chain: viemChain,
key: "thirdweb-wallet",
transport: custom({
@@ -276,7 +276,7 @@ async function getLogsFromInsight(options: {
function toLog(r: ContractEvent[]) {
const cleanedEventData = r.map((tx) => ({
address: tx.address,
address: tx.address as Hex,
blockHash: tx.block_hash as Hex,
blockNumber: numberToHex(Number(tx.block_number)),
blockTimestamp: tx.block_timestamp,
@@ -72,6 +72,7 @@ export { useActiveAccount } from "../react/core/hooks/wallets/useActiveAccount.j
export { useActiveWallet } from "../react/core/hooks/wallets/useActiveWallet.js";
export { useActiveWalletChain } from "../react/core/hooks/wallets/useActiveWalletChain.js";
export { useActiveWalletConnectionStatus } from "../react/core/hooks/wallets/useActiveWalletConnectionStatus.js";
export { useAddConnectedWallet } from "../react/core/hooks/wallets/useAddConnectedWallet.js";
export { useAdminWallet } from "../react/core/hooks/wallets/useAdminWallet.js";
export { useAuthToken } from "../react/core/hooks/wallets/useAuthToken.js";
// eip5792
+1
View File
@@ -82,6 +82,7 @@ export { useActiveAccount } from "../react/core/hooks/wallets/useActiveAccount.j
export { useActiveWallet } from "../react/core/hooks/wallets/useActiveWallet.js";
export { useActiveWalletChain } from "../react/core/hooks/wallets/useActiveWalletChain.js";
export { useActiveWalletConnectionStatus } from "../react/core/hooks/wallets/useActiveWalletConnectionStatus.js";
export { useAddConnectedWallet } from "../react/core/hooks/wallets/useAddConnectedWallet.js";
export { useAdminWallet } from "../react/core/hooks/wallets/useAdminWallet.js";
export { useAuthToken } from "../react/core/hooks/wallets/useAuthToken.js";
// eip5792
@@ -6,6 +6,7 @@ import * as Bridge from "../../../../bridge/index.js";
import type { Chain } from "../../../../chains/types.js";
import type { BuyWithCryptoStatus } from "../../../../pay/buyWithCrypto/getStatus.js";
import type { BuyWithFiatStatus } from "../../../../pay/buyWithFiat/getStatus.js";
import type { SupportedFiatCurrency } from "../../../../pay/convert/type.js";
import type { PurchaseData } from "../../../../pay/types.js";
import type { FiatProvider } from "../../../../pay/utils/commonTypes.js";
import type { GaslessOptions } from "../../../../transaction/actions/gasless/types.js";
@@ -94,6 +95,11 @@ export type SendTransactionPayModalConfig =
* The user's ISO 3166 alpha-2 country code. This is used to determine onramp provider support.
*/
country?: string;
/**
* The currency to use for showing the fiat values
* @default "USD"
*/
currency?: SupportedFiatCurrency;
}
| false;
@@ -7,7 +7,7 @@ import { getThirdwebBaseUrl } from "../../../utils/domains.js";
import { getClientFetch } from "../../../utils/fetch.js";
import { toTokens, toUnits } from "../../../utils/units.js";
import type { Wallet } from "../../../wallets/interfaces/wallet.js";
import type { PaymentMethod } from "../machines/paymentMachine.js";
import type { PaymentMethod } from "../../web/ui/Bridge/types.js";
import type { SupportedTokens } from "../utils/defaultTokens.js";
import { useActiveWallet } from "./wallets/useActiveWallet.js";
@@ -33,7 +33,6 @@ export function usePaymentMethods(options: {
destinationAmount: string;
client: ThirdwebClient;
payerWallet?: Wallet;
includeDestinationToken?: boolean;
supportedTokens?: SupportedTokens;
}) {
const {
@@ -41,7 +40,6 @@ export function usePaymentMethods(options: {
destinationAmount,
client,
payerWallet,
includeDestinationToken,
supportedTokens,
} = options;
const localWallet = useActiveWallet(); // TODO (bridge): get all connected wallets
@@ -135,7 +133,6 @@ export function usePaymentMethods(options: {
destinationToken.address,
destinationAmount,
payerWallet?.getAccount()?.address,
includeDestinationToken,
supportedTokens,
], // 5 minutes
refetchOnWindowFocus: false,
@@ -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: [

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