Compare commits

..
Author SHA1 Message Date
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
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ca701a48c2 Version Packages (#8143)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-29 18:11:08 -07:00
Joaquim VergesandGitHub 94aae902a1 [SDK] Expose waitUntil parameter in settlePayment() (#8153) 2025-09-29 17:32:59 -07:00
MananTank 0c7149eb44 Portal: remove underlines from links in changelog page (#8148)
<!--

## 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 styles for links within the `changelog-page` in the `styles.css` file, specifically removing certain text decoration properties.

### Detailed summary
- Removed the text decoration properties (`text-decoration`, `text-decoration-color`, `text-decoration-style`, `text-underline-offset`, `transition`) from the `.changelog-page a` selector.

> ✨ 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 link appearance on the changelog page for consistency with global styles. Regular links no longer use custom underlines or color transitions, resulting in a cleaner look and improved readability. Headings remain without underlines, and hover behavior is unchanged. Users should notice a more consistent visual experience across the page without functional changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-29 23:48:49 +00:00
Joaquim VergesandGitHub ce9220ea00 [Docs] Add comprehensive x402 payment documentation (#8149) 2025-09-30 12:27:40 +13:00
FirekeeperandGitHub 99c2af5e25 Add ecosystem-specific chain config support (#8147) 2025-09-30 04:21:49 +07:00
MananTank 7495f46222 [MNY-213] SDK/SwapWidget: token search improvements (#8145)
<!--

## 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 improving the token search functionality in the `SwapWidget` by incorporating a debounce mechanism to optimize search input handling and refining the token filtering logic.

### Detailed summary
- Updated `use-tokens.ts` to use `query` and `tokenAddress` based on search conditions.
- Added `useDebouncedValue` hook in `select-token-ui.tsx` for debouncing the search input.
- Changed token filtering logic to utilize `debouncedSearch` instead of raw `search` value.

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

<!-- end pr-codex -->
2025-09-29 19:25:57 +00:00
MananTank b8afa98de6 [MNY-212] SDK: Set refetch: false on token list queries in SwapWidget (#8144)
<!--

## 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 improving the loading experience in the `SwapWidget` by removing the retry mechanism when fetching the list of tokens, which reduces the time skeletons are displayed in the UI.

### Detailed summary
- Removed the `retry` option from the token fetching query in `use-tokens.ts`.
- The `retry` option is set to `false` for both the tokens query and the token balances query.
- This change aims to decrease loading times and improve UI responsiveness.

> ✨ 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**
  * Improved SwapWidget responsiveness by removing automatic retries for fetching token lists and balances, reducing how long loading placeholders are shown. Errors surface sooner, enabling quicker user action.

* **Chores**
  * Added a changelog entry documenting the update.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-29 19:00:23 +00:00
Joaquim VergesandGitHub 496615feae [SDK] Improve token info discovery for x402 payments (#8142) 2025-09-29 01:56:53 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e2931df858 Version Packages (#8137)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-28 15:03:25 -07:00
Joaquim VergesandGitHub 85bde21dc9 [SDK] Handle 7702 accounts in verifyTypedData (#8141) 2025-09-29 10:32:20 +13:00
FirekeeperandGitHub 2c84ac4089 [Portal] .NET v3 & Unity v6 Documentation (#8140) 2025-09-28 00:44:00 +07:00
MananTank 5ee449fc9c Dashboard: Bridge page UI adjustments, add tracking (#8139)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the UI and functionality of the dashboard and bridge components. It includes style updates, component restructuring, and the addition of an event reporting function for link clicks.

### Detailed summary
- Added `bg-background` class to the `canvas` in `DarkVeil.tsx`.
- Wrapped `FaqSection` in a `div` for padding in `page.tsx`.
- Introduced `reportBridgePageLinkClick` function in `report.ts` for event tracking.
- Simplified structure of `faq-section.tsx`.
- Created `PillLink` component in `pill-link.tsx` for better link handling and event tracking.
- Updated `BridgePage` to include new `PillLink` components and adjusted layout with spacing elements.

> ✨ 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 click tracking for Bridge page links.
  - Introduced pill-style external link buttons on the Bridge page.

- Refactor
  - Simplified FAQ markup to a single semantic section.
  - Extracted and reused a PillLink component across the Bridge page.
  - Reorganized Bridge page layout for clearer structure.

- Style
  - Adjusted vertical spacing and rhythm across Bridge, FAQ, and chain pages.
  - Made hero animation snappier.
  - Updated pill text color, background pattern accents, and veil canvas background.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 23:58:10 +00:00
MananTank a94e9c2396 [MNY-223] Update Bridge Page (#8138)
<!--

## 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 enhancements and new features to the `dashboard` application, including updates to the `Bridge` functionality, improvements in the UI components, and new utility functions for handling themes and FAQs.

### Detailed summary
- Added `ogl` dependency for graphics handling.
- Updated `ChainSeo` type in `chain-seo.ts` for better structure.
- Increased blockchain support from 80+ to 85+ in documentation.
- Enhanced `PageHeader` and `ToggleThemeButton` components for improved styling.
- Refactored `FaqSection` to accept a more flexible props structure.
- Introduced `BridgeVeil` component for visual effects based on themes.
- Added `DataPill` and `PillLink` components for better UI elements.
- Created a `BridgeFaqSection` with FAQs related to bridging.
- Improved `TokenPage` to manage filters and sorting more effectively.

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

<!-- end pr-codex -->
2025-09-26 22:09:35 +00:00
MananTank 64f5e474bb [MNY-216] Update SEO metadata on chain pages, Add FAQ section (#8135)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring and enhancing the codebase for better readability and functionality. It primarily involves renaming functions, updating component styles, and implementing a new SEO fetching mechanism.

### Detailed summary
- Renamed `getChainMetadata` to `getCustomChainMetadata`.
- Updated styles in several components for consistency.
- Added `fetchChainSeo` function for improved SEO handling.
- Replaced `chainMetadata` with `customChainMetadata` in various components.
- Introduced `FaqSection` component for displaying FAQs.
- Adjusted metadata generation to utilize fetched SEO data.

> ✨ 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
  - Chain pages now show an interactive FAQ section when SEO data includes FAQs.
  - SEO data is fetched and cached externally to provide richer titles, descriptions, and OpenGraph/Twitter previews.

- Bug Fixes / Behavior
  - Metadata generation now depends on external SEO (may be unavailable), affecting preview availability and link consistency.
  - Chain header images and gas-sponsored indicators now use the updated metadata source.

- Style
  - More compact Supported Products cards, adjusted overview spacing/corner radius, lighter explorer/info-item typography, and refined link styling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 22:09:34 +00:00
MananTank aa0d23e966 SDK: Fix Embed widget container size, Dashboard: Fix Bridge embed UI width (#8136)
<!--

## 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 embed container size across various components in the `thirdweb` and `dashboard` applications, ensuring better layout and responsiveness.

### Detailed summary
- Updated `maxWidth` in `ConnectEmbed` to `100%` and adjusted `width` based on `modalSize`.
- Removed redundant `layout: "centered"` parameter in `ConnectEmbed` story.
- Simplified `UniversalBridgeEmbed` structure by removing unnecessary wrapping `div`.
- Added `min-w-0` class to `BuyAndSwapEmbed` and `GridPatternEmbedContainer` for better responsiveness.
- Adjusted layout classes in `GridPatternEmbedContainer` for improved alignment.

> ✨ 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
  - Fixed embed container sizing in the Connect Wallet modal to respect modal width and prevent viewport overflow.
  - Resolved overflow and min-width issues in embed containers for smoother responsive behavior.

- Style
  - Updated Bridge page layout to display components side-by-side on larger screens.
  - Improved responsive rounding and overflow handling around embed containers for cleaner visuals.

- Chores
  - Updated thirdweb dependency to the latest patch release.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 20:10:28 +00:00
MananTank b888413ca6 Dashboard: Fix BuyAndSwapWidget width (#8134)
<!--

## 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 minor adjustments to the layout and styling of components in the dashboard, specifically in `grid-pattern-embed-container.tsx` and `BuyAndSwapEmbed.tsx`.

### Detailed summary
- In `grid-pattern-embed-container.tsx`, the width class `sm:w-[420px]` was removed from the `div`, allowing it to take its natural width.
- In `BuyAndSwapEmbed.tsx`, the width class `!w-full` was removed from two instances of `SwapWidget`, maintaining the rounded border style but removing the full width constraint.

> ✨ 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
  * Buy and Swap widgets no longer force full-width, adapting to their container while preserving rounded, borderless styling.
  * Embed container relaxes small-screen width limits, allowing content to use available space more naturally.
  * Improves responsiveness, spacing, and alignment across devices, reducing unnecessary whitespace and preventing overstretched layouts.
  * Visual update only; no changes to widget behavior or controls.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 16:56:25 +00:00
MananTank 108a79ab3e Playground: Use Geist fonts (#8133)
<!--

## 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 integrating the `geist` font into the `playground-web` application, updating the `tailwind` configuration, and replacing existing font imports with `GeistSans` and `GeistMono`.

### Detailed summary
- Added `geist` dependency in `package.json`.
- Updated `tailwind.config.ts` to extend the theme with `geist` font families.
- Replaced `Fira_Code` and `Inter` font imports in `layout.tsx` with `GeistMono` and `GeistSans`.
- Updated the body class to use `GeistSans` and `GeistMono` variables.

> ✨ 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 global typography to Geist Sans and Geist Mono for a cleaner, consistent look.
  - Replaced previous Inter and Fira Code fonts with new Geist font variables in the app layout.
  - Configured Tailwind to use Geist-based sans and mono font families across the app.

- Chores
  - Added Geist font package as a new dependency.

- Notes
  - No functional changes; visual update limited to typography.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 11:47:00 +00:00
MananTank 51390f3af0 Playground: Fix ConnectEmbed width (#8132)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refining the styling of components in the `playground-web` application by removing unnecessary CSS classes that limit width.

### Detailed summary
- In `styled-connect-embed.tsx`, removed the class `!max-w-full`.
- In `RightSection.tsx`, changed the class from `!max-w-full` to `shadow-xl`, removing the width constraint.

> ✨ 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 Connect modal styling for sign-in: removed restrictive max-width constraint to improve responsiveness and layout in the RightSection.
  * Simplified styling by removing custom class overrides from the styled Connect component, relying on default styles for more consistent appearance.
  * Result: more reliable modal sizing, better visual consistency, and cleaner presentation across themes and wallet variants.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 10:37:31 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fb8c95a325 Version Packages (#8130)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-25 19:24:09 -07:00
Joaquim VergesandGitHub cf02cbf3af [wagmi-adapter] Add onConnect callback (#8131) 2025-09-25 19:00:35 -07:00
Joaquim VergesandGitHub f152139d73 [SDK] Support ERC-2612 permit for x402 payments (#8128) 2025-09-25 18:21:58 -07:00
MananTank 16307019be Portal: Add BridgeWidget script docs (#8129)
<!--

## 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 `BridgeWidget` functionality, updating UI components, and improving documentation for better user experience and clarity.

### Detailed summary
- Added `BridgeWidget Script` route in sidebar.
- Updated `Paragraph` and `Heading` components' margin classes.
- Revised `Details` component structure.
- Changed text references from `BuyWidget` to `SwapWidget`.
- Enhanced documentation for `BridgeWidget Script` with detailed integration instructions.
- Introduced new options for customizing the widget's appearance and behavior.

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

<!-- end pr-codex -->
2025-09-25 22:50:37 +00:00
MananTank 10fb338877 Portal: UI tweaks 2 (#8127)
<!--

## 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 and styling updates across various components in the application, including adjustments to CSS, layout changes, and the addition of a new package, `geist`. It aims to improve the overall appearance and functionality of the portal.

### Detailed summary
- Added `font-feature-settings` to `globals.css`.
- Updated `text-rendering`, `-webkit-font-smoothing`, and `text-size-adjust` in `global.css`.
- Included `geist` package in `package.json`.
- Modified layout and spacing in `vault/page.mdx`.
- Adjusted classes for `ApiEndpoint.tsx`, `Paragraph.tsx`, and `InlineCode.tsx`.
- Changed text color in `Subscribe.tsx`.
- Revised spacing and border styles in various components.
- Updated icons in `Anchor.tsx` and `ChatButton.tsx`.
- Enhanced styling of buttons and links across multiple components.
- Improved overall layout structure in `Header.tsx` and sidebar components.
- Refined search modal styles and item presentation in `DocSearch.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

- Style
  - Switched site typography to Geist, enabled ligatures and improved text rendering.
  - Redesigned header with central branding, consolidated controls, mobile menu and subtle backdrop.
  - Refreshed visuals across search, footer, sidebar, platform selector, steps, cards, code/inline code, and mobile sidebar.
  - Updated icons (chat, next-page, anchors), spacing/typography tweaks; vault features now stack vertically.

- Refactor
  - Localized an article card component and reorganized footer/next-page flows; Edit link now wrapped in a button.

- Chores
  - Added Geist dependency and extended Tailwind font settings; global font/rendering tweaks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-25 22:50:36 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
95412c75b9 Version Packages (#8114)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-25 10:12:22 -07:00
MananTank 7bc513c770 [BLD-328] Portal: Update changelog page layout (#8124)
<!--

## 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 UI improvements and code refactoring across various components in the application, enhancing styling consistency, accessibility, and overall user experience.

### Detailed summary
- Updated grid spacing in `Grid.tsx`.
- Increased max-width in `layout.tsx`.
- Modified border styles in multiple components.
- Enhanced button styles and variants in `chat-button.tsx` and `Subscribe.tsx`.
- Improved text styles for better readability.
- Refactored `AuthMethodsTabs.tsx` to use `Button` component.
- Adjusted `DocLayout.tsx` for more responsive design.
- Improved date formatting in `RenderData.tsx`.
- Added animations to `TableOfContents.tsx`.
- Updated feedback dialog in `Feedback.tsx` for better UX.
- General styling updates for consistency across components.

> ✨ 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
  - Widespread visual refinements: typography, spacing, tighter headings, dashed borders for dividers/images/code, smaller author avatar, absolute dates, wider content container, updated buttons (ghost/outline), adjusted icons and shadows, updated link/inline-code/grid/card styles, tabs and header/menu visual updates.
  - Footer, sidebar, docs search, subscribe, feedback, and table-of-contents presentational tweaks (including fade-in and conditional hiding).

- Refactor
  - Simplified changelog and post layout to a single-column header-first structure; removed changelog index TOC.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-25 08:00:13 +00:00
Joaquim VergesandGitHub 9069351e1a [SDK] Support Chain objects in x402 payment utilities (#8126) 2025-09-25 00:53:22 -07:00
MananTank 8f6363f4e4 SDK: Fix toUnits throwing error for values in scientific notation (#8112)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `toUnits` function to handle both scientific notation and fixed-point notation, ensuring accurate conversion of token values with specified decimals. It also adds tests to verify the new functionality.

### Detailed summary
- Modified `toUnits` function to check for scientific notation (e.g., "1e-18").
- Converted scientific notation to fixed-point notation using `Number(tokens).toFixed(decimals)`.
- Added tests for `toUnits` to cover various scientific notation cases, including negative and positive exponents.

> ✨ 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**
  * Unit conversion now accepts scientific notation (positive and negative exponents), normalizing values to the configured decimal precision for consistent fixed-point results.

* **Tests**
  * Added comprehensive tests covering positive and negative exponent cases to validate parsing and conversion from scientific notation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 23:07:53 +00:00
MananTank a83104efff [MNY-200] SDK: Fix onSuccess callback not called when tx is sent in TransactionWidget (#8119)
<!--

## 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 simplifies the `handlePostBuyTransactionComplete` function in the `BridgeOrchestrator` component by removing the `quote` parameter and the associated call to `onComplete`. It streamlines the transaction completion process and adjusts the `closeModal` prop accordingly.

### Detailed summary
- Removed the `quote` parameter from the `handlePostBuyTransactionComplete` function.
- Eliminated the call to `onComplete?.(quote)` within the `handlePostBuyTransactionComplete` function.
- Updated the `closeModal` prop in the `ExecutingTxScreen` component to directly use `handlePostBuyTransactionComplete` without arguments.

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

<!-- end pr-codex -->
2025-09-24 21:43:56 +00:00
MananTank 7936ebb6e9 [MNY-180] SDK: Fix TransactionWidget not respecting currency prop (#8118)
<!--

## 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 handling of the `currency` property in various components and adding a new story for a transaction mode in light theme that showcases an ERC20 token transfer.

### Detailed summary
- Changed `currency` from optional to required in `useTransactionDetails.ts`.
- Added `currency` to the props in `PaymentOverview.tsx` and `TransactionPayment.tsx`.
- Introduced a new story `TransactionJPYCurrency` in `BridgeOrchestrator.stories.tsx` for displaying an ERC20 token transfer in light theme.

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

<!-- end pr-codex -->
2025-09-24 21:43:56 +00:00
MananTank 3b986d52e5 [MNY-201] SDK: Rename leftover of Universal Bridge to Bridge (#8117)
<!--

## 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 renaming references from `Universal Bridge` to `Bridge` across multiple files to standardize terminology and improve clarity.

### Detailed summary
- Updated comments and documentation to replace `Universal Bridge` with `Bridge`.
- Modified error messages in `apps/dashboard/src/@/api/analytics.ts` to reflect the new terminology.
- Changed descriptions in various files related to `Chain`, `Token`, `Quote`, and webhooks to use `Bridge`.

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

<!-- end pr-codex -->
2025-09-24 21:43:56 +00:00
MananTank ee4e13e098 [MNY-198] SDK: Fix StepRunner autoStart not working reliably, Remove extra prepare call (#8116)
<!--

## 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 enhancing the `StepRunner` component in the Bridge UI by integrating a `preparedQuote` prop, improving the handling of swap transactions, and refining the relevant hooks and stories.

### Detailed summary
- Added `preparedQuote` prop to `SwapWidget` and `StepRunner`.
- Updated `StepRunner` to utilize `preparedQuote` for executing steps.
- Modified `useStepExecutor` to accept `preparedQuote` instead of `request`.
- Cleaned up imports in `useStepExecutor`.
- Updated stories to reflect the new `preparedQuote` prop.

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

<!-- end pr-codex -->
2025-09-24 19:29:55 +00:00
MananTank 8dc9e4102d [MNY-197] SDK: SwapWidget chain selection UI improvements (#8115)
<!--

## 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 improving the user experience in the `SwapUI` and `SelectTokenUI` components by updating callback functions and enhancing token selection logic.

### Detailed summary
- Changed `onBack` prop to `onClose` in `SelectToken` and `SelectTokenUI` components.
- Updated modal state management to close modals on token selection.
- Introduced `initiallySelectedChain` to prioritize the selected chain in `SelectBridgeChainUI`.
- Enhanced filtering logic for token selection to consider chain IDs.

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

<!-- end pr-codex -->
2025-09-24 19:29:55 +00:00
MananTank 33fc3e5226 [MNY-196] SDK: Add SwapWidget render event (#8113)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new query using `useQuery` from `@tanstack/react-query` in the `SwapWidget` component to track the rendering event of the swap widget.

### Detailed summary
- Added `useQuery` to the `SwapWidget` component.
- Implemented a query function that calls `trackPayEvent` to log the rendering of the swap widget.
- The tracking event includes the `client` from `props` and an event identifier `"ub:ui:swap_widget:render"`.

> ✨ 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

* **Chores**
  * Added background telemetry for the Swap Widget to record render events for analytics. It runs automatically when the widget is displayed, requires no configuration, and does not change the UI or user experience. This helps track usage and adoption to inform future improvements.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 19:29:54 +00:00
MananTank ed47e93f1d [BLD-338] Dashboard: Fix NFTs table owner row not showing data (#8120)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a temporary change to the `table.tsx` file in the NFTs component of the dashboard. It sets the `useIndexer` property to `false`, indicating a workaround until a future fix is implemented.

### Detailed summary
- Added `useIndexer: false` to the settings in `table.tsx`.
- Marked the addition as temporary with a comment: `// TEMPORARY, TODO: Remove when this is fixed in insight`.

> ✨ 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**
  * Improved reliability of NFT lists in the dashboard, reducing intermittent errors and missing items when viewing NFTs for specific contracts.
  * Ensures more consistent, up-to-date NFT data is displayed during browsing and refreshes.
  * Enhances stability of data loading in environments with inconsistent indexing, minimizing failed loads and retries.
  * No visual/UI changes; behavior is more dependable without altering the interface or workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 19:19:38 +00:00
MananTank 1f7cda6698 [MNY-210] SDK: export a script to render BridgeEmbed (#8106)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `BridgeWidget` component, allowing users to render a widget for token buying and swapping. It also updates related files for better integration and testing, while modifying build configurations and dependencies.

### Detailed summary
- Added `BridgeWidget` component.
- Created a browser script in `dist/scripts/bridge-widget.js`.
- Updated `BuyOrOnrampPrepareResult` type in `BuyWidget.tsx`.
- Excluded test files in `tsconfig.build.tsup.json`.
- Modified `tsup` configuration for building the widget.
- Enhanced documentation for the `BridgeWidget`.
- Added various usage examples in the README.
- Updated `pnpm-lock.yaml` with new dependencies and versions.

> The following files were skipped due to too many changes: `pnpm-lock.yaml`

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

<!-- end pr-codex -->

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

- **New Features**
  - Introduced a Bridge Widget with Swap and Buy tabs, theming, branding toggle, currency presets, and callback support.
  - Added an embeddable script API with a global render function to mount the Bridge Widget on any webpage and exposed it in React exports.

- **Style**
  - Connect Wallet embed now uses full-width with max-width constraints for responsive display.

- **Documentation**
  - Added usage guide and Storybook examples for the Bridge Widget script.

- **Build**
  - Added browser-targeted bundle config to produce a self-contained Bridge Widget script.

- **Chores**
  - Updated build scripts and dev dependencies.

- **Tests**
  - Skipped a batch transaction test.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 17:30:52 +00:00
296 changed files with 14871 additions and 8088 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
View File
@@ -47,6 +47,7 @@
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
"ogl": "^1.0.11",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.256.1",
@@ -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,
},
});
+14
View File
@@ -691,3 +691,17 @@ export function reportProductFeedback(properties: {
source: properties.source,
});
}
/**
* ### Why do we need to report this event?
* - To track conversions for the bridge page links
*
* ### Who is responsible for this event?
* @MananTank
*
*/
export function reportBridgePageLinkClick(params: {
linkType: "integrate-bridge" | "trending-tokens";
}) {
posthog.capture("bridge page link clicked", params);
}
+2 -2
View File
@@ -592,7 +592,7 @@ const cached_getUniversalBridgeUsage = unstable_cache(
if (res?.status !== 200) {
const reason = await res?.text();
console.error(
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
`Failed to fetch bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
);
return [];
}
@@ -636,7 +636,7 @@ const cached_getUniversalBridgeWalletUsage = unstable_cache(
if (res?.status !== 200) {
const reason = await res?.text();
console.error(
`Failed to fetch universal bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
`Failed to fetch bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
);
return [];
}
@@ -0,0 +1,192 @@
"use client";
/* eslint-disable no-restricted-syntax */
import { Mesh, Program, Renderer, Triangle, Vec2 } from "ogl";
import { useEffect, useRef } from "react";
const vertex = `
attribute vec2 position;
void main(){gl_Position=vec4(position,0.0,1.0);}
`;
const fragment = `
#ifdef GL_ES
precision lowp float;
#endif
uniform vec2 uResolution;
uniform float uTime;
uniform float uHueShift;
uniform float uNoise;
uniform float uScan;
uniform float uScanFreq;
uniform float uWarp;
uniform vec3 uBackgroundColor;
uniform float uBackgroundOpacity;
uniform float uPatternLightness;
#define iTime uTime
#define iResolution uResolution
vec4 buf[8];
float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}
mat3 rgb2yiq=mat3(0.299,0.587,0.114,0.596,-0.274,-0.322,0.211,-0.523,0.312);
mat3 yiq2rgb=mat3(1.0,0.956,0.621,1.0,-0.272,-0.647,1.0,-1.106,1.703);
vec3 hueShiftRGB(vec3 col,float deg){
vec3 yiq=rgb2yiq*col;
float rad=radians(deg);
float cosh=cos(rad),sinh=sin(rad);
vec3 yiqShift=vec3(yiq.x,yiq.y*cosh-yiq.z*sinh,yiq.y*sinh+yiq.z*cosh);
return clamp(yiq2rgb*yiqShift,0.0,1.0);
}
vec4 sigmoid(vec4 x){return 1./(1.+exp(-x));}
vec4 cppn_fn(vec2 coordinate,float in0,float in1,float in2){
buf[6]=vec4(coordinate.x,coordinate.y,0.3948333106474662+in0,0.36+in1);
buf[7]=vec4(0.14+in2,sqrt(coordinate.x*coordinate.x+coordinate.y*coordinate.y),0.,0.);
buf[0]=mat4(vec4(6.5404263,-3.6126034,0.7590882,-1.13613),vec4(2.4582713,3.1660357,1.2219609,0.06276096),vec4(-5.478085,-6.159632,1.8701609,-4.7742867),vec4(6.039214,-5.542865,-0.90925294,3.251348))*buf[6]+mat4(vec4(0.8473259,-5.722911,3.975766,1.6522468),vec4(-0.24321538,0.5839259,-1.7661959,-5.350116),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(0.21808943,1.1243913,-1.7969975,5.0294676);
buf[1]=mat4(vec4(-3.3522482,-6.0612736,0.55641043,-4.4719114),vec4(0.8631464,1.7432913,5.643898,1.6106541),vec4(2.4941394,-3.5012043,1.7184316,6.357333),vec4(3.310376,8.209261,1.1355612,-1.165539))*buf[6]+mat4(vec4(5.24046,-13.034365,0.009859298,15.870829),vec4(2.987511,3.129433,-0.89023495,-1.6822904),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-5.9457836,-6.573602,-0.8812491,1.5436668);
buf[0]=sigmoid(buf[0]);buf[1]=sigmoid(buf[1]);
buf[2]=mat4(vec4(-15.219568,8.095543,-2.429353,-1.9381982),vec4(-5.951362,4.3115187,2.6393783,1.274315),vec4(-7.3145227,6.7297835,5.2473326,5.9411426),vec4(5.0796127,8.979051,-1.7278991,-1.158976))*buf[6]+mat4(vec4(-11.967154,-11.608155,6.1486754,11.237008),vec4(2.124141,-6.263192,-1.7050359,-0.7021966),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-4.17164,-3.2281182,-4.576417,-3.6401186);
buf[3]=mat4(vec4(3.1832156,-13.738922,1.879223,3.233465),vec4(0.64300746,12.768129,1.9141049,0.50990224),vec4(-0.049295485,4.4807224,1.4733979,1.801449),vec4(5.0039253,13.000481,3.3991797,-4.5561905))*buf[6]+mat4(vec4(-0.1285731,7.720628,-3.1425676,4.742367),vec4(0.6393625,3.714393,-0.8108378,-0.39174938),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-1.1811101,-21.621881,0.7851888,1.2329718);
buf[2]=sigmoid(buf[2]);buf[3]=sigmoid(buf[3]);
buf[4]=mat4(vec4(5.214916,-7.183024,2.7228765,2.6592617),vec4(-5.601878,-25.3591,4.067988,0.4602802),vec4(-10.57759,24.286327,21.102104,37.546658),vec4(4.3024497,-1.9625226,2.3458803,-1.372816))*buf[0]+mat4(vec4(-17.6526,-10.507558,2.2587414,12.462782),vec4(6.265566,-502.75443,-12.642513,0.9112289),vec4(-10.983244,20.741234,-9.701768,-0.7635988),vec4(5.383626,1.4819539,-4.1911616,-4.8444734))*buf[1]+mat4(vec4(12.785233,-16.345072,-0.39901125,1.7955981),vec4(-30.48365,-1.8345358,1.4542528,-1.1118771),vec4(19.872723,-7.337935,-42.941723,-98.52709),vec4(8.337645,-2.7312303,-2.2927687,-36.142323))*buf[2]+mat4(vec4(-16.298317,3.5471997,-0.44300047,-9.444417),vec4(57.5077,-35.609753,16.163465,-4.1534753),vec4(-0.07470326,-3.8656476,-7.0901804,3.1523974),vec4(-12.559385,-7.077619,1.490437,-0.8211543))*buf[3]+vec4(-7.67914,15.927437,1.3207729,-1.6686112);
buf[5]=mat4(vec4(-1.4109162,-0.372762,-3.770383,-21.367174),vec4(-6.2103205,-9.35908,0.92529047,8.82561),vec4(11.460242,-22.348068,13.625772,-18.693201),vec4(-0.3429052,-3.9905605,-2.4626114,-0.45033523))*buf[0]+mat4(vec4(7.3481627,-4.3661838,-6.3037653,-3.868115),vec4(1.5462853,6.5488915,1.9701879,-0.58291394),vec4(6.5858274,-2.2180402,3.7127688,-1.3730392),vec4(-5.7973905,10.134961,-2.3395722,-5.965605))*buf[1]+mat4(vec4(-2.5132585,-6.6685553,-1.4029363,-0.16285264),vec4(-0.37908727,0.53738135,4.389061,-1.3024765),vec4(-0.70647055,2.0111287,-5.1659346,-3.728635),vec4(-13.562562,10.487719,-0.9173751,-2.6487076))*buf[2]+mat4(vec4(-8.645013,6.5546675,-6.3944063,-5.5933375),vec4(-0.57783127,-1.077275,36.91025,5.736769),vec4(14.283112,3.7146652,7.1452246,-4.5958776),vec4(2.7192075,3.6021907,-4.366337,-2.3653464))*buf[3]+vec4(-5.9000807,-4.329569,1.2427121,8.59503);
buf[4]=sigmoid(buf[4]);buf[5]=sigmoid(buf[5]);
buf[6]=mat4(vec4(-1.61102,0.7970257,1.4675229,0.20917463),vec4(-28.793737,-7.1390953,1.5025433,4.656581),vec4(-10.94861,39.66238,0.74318546,-10.095605),vec4(-0.7229728,-1.5483948,0.7301322,2.1687684))*buf[0]+mat4(vec4(3.2547753,21.489103,-1.0194173,-3.3100595),vec4(-3.7316632,-3.3792162,-7.223193,-0.23685838),vec4(13.1804495,0.7916005,5.338587,5.687114),vec4(-4.167605,-17.798311,-6.815736,-1.6451967))*buf[1]+mat4(vec4(0.604885,-7.800309,-7.213122,-2.741014),vec4(-3.522382,-0.12359311,-0.5258442,0.43852118),vec4(9.6752825,-22.853785,2.062431,0.099892326),vec4(-4.3196306,-17.730087,2.5184598,5.30267))*buf[2]+mat4(vec4(-6.545563,-15.790176,-6.0438633,-5.415399),vec4(-43.591583,28.551912,-16.00161,18.84728),vec4(4.212382,8.394307,3.0958717,8.657522),vec4(-5.0237565,-4.450633,-4.4768,-5.5010443))*buf[3]+mat4(vec4(1.6985557,-67.05806,6.897715,1.9004834),vec4(1.8680354,2.3915145,2.5231109,4.081538),vec4(11.158006,1.7294737,2.0738268,7.386411),vec4(-4.256034,-306.24686,8.258898,-17.132736))*buf[4]+mat4(vec4(1.6889864,-4.5852966,3.8534803,-6.3482175),vec4(1.3543309,-1.2640043,9.932754,2.9079645),vec4(-5.2770967,0.07150358,-0.13962056,3.3269649),vec4(28.34703,-4.918278,6.1044083,4.085355))*buf[5]+vec4(6.6818056,12.522166,-3.7075126,-4.104386);
buf[7]=mat4(vec4(-8.265602,-4.7027016,5.098234,0.7509808),vec4(8.6507845,-17.15949,16.51939,-8.884479),vec4(-4.036479,-2.3946867,-2.6055532,-1.9866527),vec4(-2.2167742,-1.8135649,-5.9759874,4.8846445))*buf[0]+mat4(vec4(6.7790847,3.5076547,-2.8191125,-2.7028968),vec4(-5.743024,-0.27844876,1.4958696,-5.0517144),vec4(13.122226,15.735168,-2.9397483,-4.101023),vec4(-14.375265,-5.030483,-6.2599335,2.9848232))*buf[1]+mat4(vec4(4.0950394,-0.94011575,-5.674733,4.755022),vec4(4.3809423,4.8310084,1.7425908,-3.437416),vec4(2.117492,0.16342592,-104.56341,16.949184),vec4(-5.22543,-2.994248,3.8350096,-1.9364246))*buf[2]+mat4(vec4(-5.900337,1.7946124,-13.604192,-3.8060522),vec4(6.6583457,31.911177,25.164474,91.81147),vec4(11.840538,4.1503043,-0.7314397,6.768467),vec4(-6.3967767,4.034772,6.1714606,-0.32874924))*buf[3]+mat4(vec4(3.4992442,-196.91893,-8.923708,2.8142626),vec4(3.4806502,-3.1846354,5.1725626,5.1804223),vec4(-2.4009497,15.585794,1.2863957,2.0252278),vec4(-71.25271,-62.441242,-8.138444,0.50670296))*buf[4]+mat4(vec4(-12.291733,-11.176166,-7.3474145,4.390294),vec4(10.805477,5.6337385,-0.9385842,-4.7348723),vec4(-12.869276,-7.039391,5.3029537,7.5436664),vec4(1.4593618,8.91898,3.5101583,5.840625))*buf[5]+vec4(2.2415268,-6.705987,-0.98861027,-2.117676);
buf[6]=sigmoid(buf[6]);buf[7]=sigmoid(buf[7]);
buf[0]=mat4(vec4(1.6794263,1.3817469,2.9625452,0.),vec4(-1.8834411,-1.4806935,-3.5924516,0.),vec4(-1.3279216,-1.0918057,-2.3124623,0.),vec4(0.2662234,0.23235129,0.44178495,0.))*buf[0]+mat4(vec4(-0.6299101,-0.5945583,-0.9125601,0.),vec4(0.17828953,0.18300213,0.18182953,0.),vec4(-2.96544,-2.5819945,-4.9001055,0.),vec4(1.4195864,1.1868085,2.5176322,0.))*buf[1]+mat4(vec4(-1.2584374,-1.0552157,-2.1688404,0.),vec4(-0.7200217,-0.52666044,-1.438251,0.),vec4(0.15345335,0.15196142,0.272854,0.),vec4(0.945728,0.8861938,1.2766753,0.))*buf[2]+mat4(vec4(-2.4218085,-1.968602,-4.35166,0.),vec4(-22.683098,-18.0544,-41.954372,0.),vec4(0.63792,0.5470648,1.1078634,0.),vec4(-1.5489894,-1.3075932,-2.6444845,0.))*buf[3]+mat4(vec4(-0.49252132,-0.39877754,-0.91366625,0.),vec4(0.95609266,0.7923952,1.640221,0.),vec4(0.30616966,0.15693925,0.8639857,0.),vec4(1.1825981,0.94504964,2.176963,0.))*buf[4]+mat4(vec4(0.35446745,0.3293795,0.59547555,0.),vec4(-0.58784515,-0.48177817,-1.0614829,0.),vec4(2.5271258,1.9991658,4.6846647,0.),vec4(0.13042648,0.08864098,0.30187556,0.))*buf[5]+mat4(vec4(-1.7718065,-1.4033192,-3.3355875,0.),vec4(3.1664357,2.638297,5.378702,0.),vec4(-3.1724713,-2.6107926,-5.549295,0.),vec4(-2.851368,-2.249092,-5.3013067,0.))*buf[6]+mat4(vec4(1.5203838,1.2212278,2.8404984,0.),vec4(1.5210563,1.2651345,2.683903,0.),vec4(2.9789467,2.4364579,5.2347264,0.),vec4(2.2270417,1.8825914,3.8028636,0.))*buf[7]+vec4(-1.5468478,-3.6171484,0.24762098,0.);
buf[0]=sigmoid(buf[0]);
return vec4(buf[0].x,buf[0].y,buf[0].z,1.);
}
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 uv=fragCoord/uResolution.xy*2.-1.;
uv.y*=-1.;
uv+=uWarp*vec2(sin(uv.y*6.283+uTime*0.5),cos(uv.x*6.283+uTime*0.5))*0.05;
fragColor=cppn_fn(uv,0.1*sin(0.3*uTime),0.1*sin(0.69*uTime),0.1*sin(0.44*uTime));
}
void main(){
vec4 col;mainImage(col,gl_FragCoord.xy);
col.rgb=hueShiftRGB(col.rgb,uHueShift);
float scanline_val=sin(gl_FragCoord.y*uScanFreq)*0.5+0.5;
col.rgb*=1.-(scanline_val*scanline_val)*uScan;
col.rgb+=(rand(gl_FragCoord.xy+uTime)-0.5)*uNoise;
// Apply lightness control to pattern
col.rgb = col.rgb * uPatternLightness;
// Use background color as base, then add pattern on top
vec3 finalColor = uBackgroundColor + col.rgb * uBackgroundOpacity;
gl_FragColor=vec4(clamp(finalColor,0.0,1.0),1.0);
}
`;
type Props = {
hueShift?: number;
noiseIntensity?: number;
scanlineIntensity?: number;
speed?: number;
scanlineFrequency?: number;
warpAmount?: number;
resolutionScale?: number;
backgroundColor?: [number, number, number];
backgroundOpacity?: number;
patternLightness?: number;
};
export function DarkVeil({
hueShift = 0,
noiseIntensity = 0,
scanlineIntensity = 0,
speed = 0.5,
scanlineFrequency = 0,
warpAmount = 0,
resolutionScale = 1,
backgroundColor = [0, 0, 0],
backgroundOpacity = 1,
patternLightness = 1.0,
}: Props) {
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = ref.current as HTMLCanvasElement;
const parent = canvas.parentElement as HTMLElement;
const renderer = new Renderer({
dpr: Math.min(window.devicePixelRatio, 2),
canvas,
});
const gl = renderer.gl;
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex,
fragment,
uniforms: {
uTime: { value: 0 },
uResolution: { value: new Vec2() },
uHueShift: { value: hueShift },
uNoise: { value: noiseIntensity },
uScan: { value: scanlineIntensity },
uScanFreq: { value: scanlineFrequency },
uWarp: { value: warpAmount },
uBackgroundColor: { value: backgroundColor },
uBackgroundOpacity: { value: backgroundOpacity },
uPatternLightness: { value: patternLightness },
},
});
const mesh = new Mesh(gl, { geometry, program });
const resize = () => {
const w = parent.clientWidth,
h = parent.clientHeight;
renderer.setSize(w * resolutionScale, h * resolutionScale);
program.uniforms.uResolution.value.set(w, h);
};
window.addEventListener("resize", resize);
resize();
const start = performance.now();
let frame = 0;
const loop = () => {
program.uniforms.uTime.value =
((performance.now() - start) / 1000) * speed;
program.uniforms.uHueShift.value = hueShift;
program.uniforms.uNoise.value = noiseIntensity;
program.uniforms.uScan.value = scanlineIntensity;
program.uniforms.uScanFreq.value = scanlineFrequency;
program.uniforms.uWarp.value = warpAmount;
program.uniforms.uBackgroundColor.value = backgroundColor;
program.uniforms.uBackgroundOpacity.value = backgroundOpacity;
program.uniforms.uPatternLightness.value = patternLightness;
renderer.render({ scene: mesh });
frame = requestAnimationFrame(loop);
};
loop();
return () => {
cancelAnimationFrame(frame);
window.removeEventListener("resize", resize);
};
}, [
hueShift,
noiseIntensity,
scanlineIntensity,
speed,
scanlineFrequency,
warpAmount,
resolutionScale,
backgroundColor,
backgroundOpacity,
patternLightness,
]);
return <canvas ref={ref} className="w-full h-full block bg-background" />;
}
@@ -67,7 +67,7 @@ export function BuyAndSwapEmbed(props: {
}, [props.pageType]);
return (
<div className="bg-card rounded-2xl border overflow-hidden flex flex-col relative z-10 shadow-xl">
<div className="bg-card rounded-2xl border overflow-hidden flex flex-col relative z-10 shadow-xl min-w-0">
<div className="flex gap-2.5 p-4 border-b border-dashed">
<TabButton
label="Swap"
@@ -85,7 +85,7 @@ export function BuyAndSwapEmbed(props: {
<BuyWidget
amount={props.buyAmount || "1"}
chain={props.chain}
className="!rounded-2xl !w-full !border-none"
className="!rounded-2xl !border-none"
title=""
client={client}
connectOptions={{
@@ -179,7 +179,7 @@ export function BuyAndSwapEmbed(props: {
<SwapWidget
client={client}
theme={themeObj}
className="!rounded-2xl !border-none !w-full"
className="!rounded-2xl !border-none"
prefill={{
// buy this token by default
buyToken: {
@@ -4,9 +4,10 @@ import { MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { ClientOnly } from "@/components/blocks/client-only";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Skeleton } from "../ui/skeleton";
export function ToggleThemeButton() {
export function ToggleThemeButton(props: { className?: string }) {
const { setTheme, theme } = useTheme();
return (
@@ -15,7 +16,10 @@ export function ToggleThemeButton() {
>
<Button
aria-label="Toggle theme"
className="h-auto w-auto rounded-full border border-border bg-background p-2"
className={cn(
"h-auto w-auto rounded-full border border-border bg-background p-2",
props.className,
)}
onClick={() => {
setTheme(theme === "dark" ? "light" : "dark");
}}
@@ -0,0 +1,73 @@
"use client";
import { ChevronDownIcon } from "lucide-react";
import { useId, useState } from "react";
import { Button } from "@/components/ui/button";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { cn } from "@/lib/utils";
export function FaqSection(props: {
faqs: Array<{ title: string; description: string }>;
}) {
return (
<section>
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
Frequently asked questions
</h2>
<div className="flex flex-col">
{props.faqs.map((faq, faqIndex) => (
<FaqItem
key={faq.title}
title={faq.title}
description={faq.description}
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
/>
))}
</div>
</section>
);
}
function FaqItem(props: {
title: string;
description: string;
className?: string;
}) {
const [isOpen, setIsOpenn] = useState(false);
const contentId = useId();
return (
<DynamicHeight>
<div className={cn("border-b border-dashed", props.className)}>
<h3>
<Button
variant="ghost"
onClick={() => setIsOpenn(!isOpen)}
aria-controls={contentId}
aria-expanded={isOpen}
className={cn(
"w-full justify-between h-auto py-5 text-base text-muted-foreground hover:bg-transparent pl-0 text-wrap text-left gap-6",
isOpen && "text-foreground",
)}
>
{props.title}
<ChevronDownIcon
className={cn(
"size-4 shrink-0 transition-transform duration-200",
isOpen && "rotate-180",
)}
/>
</Button>
</h3>
<p
className={cn(
"text-muted-foreground text-base leading-7 pb-6 max-w-4xl",
!isOpen && "hidden",
)}
id={contentId}
>
{props.description}
</p>
</div>
</DynamicHeight>
);
}
@@ -4,7 +4,7 @@ export function GridPatternEmbedContainer(props: {
children: React.ReactNode;
}) {
return (
<div className="sm:flex sm:justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 rounded-xl overflow-hidden relative">
<div className="flex justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 sm:rounded-xl sm:overflow-hidden relative">
<GridPattern
width={30}
height={30}
@@ -17,7 +17,7 @@ export function GridPatternEmbedContainer(props: {
"linear-gradient(to bottom right,white,transparent,transparent)",
}}
/>
<div className="sm:w-[420px] z-10">{props.children}</div>
<div className="z-10 min-w-0">{props.children}</div>
</div>
);
}
@@ -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),
@@ -0,0 +1,50 @@
import "server-only";
import { unstable_cache } from "next/cache";
type ChainSeo = {
title: string;
description: string;
og: {
title: string;
description: string;
site_name: string;
url: string;
};
faqs: Array<{
title: string;
description: string;
}>;
chain: {
chainId: number;
name: string;
slug: string;
nativeCurrency: {
name: string;
symbol: string;
decimals: number;
};
testnet: boolean;
is_deprecated: boolean;
};
};
export const fetchChainSeo = unstable_cache(
async (chainId: number) => {
const url = new URL(
`https://seo-pages-generator-5814.zeet-nftlabs.zeet.app/chain/${chainId}`,
);
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
return undefined;
}
return res.json() as Promise<ChainSeo>;
},
["chain-seo"],
{ revalidate: 60 * 60 * 24 }, // 24 hours
);
@@ -11,7 +11,7 @@ export function ChainOverviewSection(props: { chain: ChainMetadata }) {
return (
<section>
<SectionTitle title="Chain Overview" />
<div className="grid grid-cols-1 gap-6 rounded-lg border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-8">
<div className="grid grid-cols-1 gap-6 rounded-xl border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-6">
{/* Info */}
{chain.infoURL && (
<PrimaryInfoItem title="Info">
@@ -1,4 +1,3 @@
import { CircleCheckIcon } from "lucide-react";
import Link from "next/link";
import type { ChainMetadataWithServices } from "@/types/chain";
import { products } from "../../../../components/server/products";
@@ -24,15 +23,18 @@ export function SupportedProductsSection(props: {
{enabledProducts.map((product) => {
return (
<div
className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border"
className="relative rounded-xl border bg-card p-4 hover:border-active-border"
key={product.id}
>
<CircleCheckIcon className="absolute top-4 right-4 size-5 text-success-text" />
<product.icon className="mt-0.5 size-5 shrink-0" />
<div className="flex mb-4">
<div className="p-2 rounded-full border bg-background">
<product.icon className="size-4 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="mb-1.5 font-medium">
<h3 className="mb-1">
<Link
className="before:absolute before:inset-0"
className="before:absolute before:inset-0 text-base font-medium"
href={product.link}
rel="noopener noreferrer"
target="_blank"
@@ -17,7 +17,7 @@ export function ExplorersSection(props: {
key={explorer.url}
>
<ExternalLinkIcon className="absolute top-4 right-4 size-4 text-muted-foreground" />
<h3 className="mb-1 font-semibold text-base capitalize">
<h3 className="mb-1 font-medium text-base capitalize">
{explorer.name}
</h3>
<Link
@@ -6,9 +6,7 @@ export function PrimaryInfoItem(props: {
return (
<div>
<div className="flex items-center gap-2">
<h3 className="font-medium text-base text-muted-foreground">
{props.title}
</h3>
<h3 className="text-base text-muted-foreground">{props.title}</h3>
{props.titleIcon}
</div>
{props.children}
@@ -22,39 +22,51 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
import { TeamHeader } from "../../../../team/components/TeamHeader/team-header";
import { StarButton } from "../../components/client/star-button";
import { getChain, getChainMetadata } from "../../utils";
import { getChain, getCustomChainMetadata } from "../../utils";
import { fetchChainSeo } from "./apis/chain-seo";
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
import { ChainPageView } from "./components/client/chain-pageview";
import { ChainHeader } from "./components/server/chain-header";
// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
type Params = Promise<{ chain_id: string }>;
export async function generateMetadata(props: {
params: Promise<{ chain_id: string }>;
}): Promise<Metadata> {
params: Params;
}): Promise<Metadata | undefined> {
const params = await props.params;
const chain = await getChain(params.chain_id);
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
const title = `${sanitizedChainName}: RPC and Chain Settings`;
const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch(
() => undefined,
);
const description = `Use the best ${sanitizedChainName} RPC and add to your wallet. Discover the chain ID, native token, explorers, and ${
chain.testnet && chain.faucets?.length ? "faucet options" : "more"
}.`;
if (!chainSeo) {
return undefined;
}
return {
description,
title: chainSeo.title,
description: chainSeo.description,
metadataBase: new URL("https://thirdweb.com"),
openGraph: {
description,
title,
title: chainSeo.og.title,
description: chainSeo.og.description,
siteName: "thirdweb",
type: "website",
url: "https://thirdweb.com",
},
twitter: {
title: chainSeo.og.title,
description: chainSeo.og.description,
card: "summary_large_image",
creator: "@thirdweb",
site: "@thirdweb",
},
title,
};
}
// this is the dashboard layout file
export default async function ChainPageLayout(props: {
children: React.ReactNode;
params: Promise<{ chain_id: string }>;
params: Params;
}) {
const params = await props.params;
const { children } = props;
@@ -67,8 +79,10 @@ export default async function ChainPageLayout(props: {
redirect(chain.slug);
}
const chainMetadata = await getChainMetadata(chain.chainId);
const customChainMetadata = getCustomChainMetadata(chain.chainId);
const chainSeo = await fetchChainSeo(chain.chainId);
const client = getClientThirdwebClient(undefined);
const description = customChainMetadata?.about || chainSeo?.description;
return (
<div className="flex grow flex-col">
@@ -123,7 +137,7 @@ export default async function ChainPageLayout(props: {
<ChainHeader
chain={chain}
client={client}
headerImageUrl={chainMetadata?.headerImgUrl}
headerImageUrl={customChainMetadata?.headerImgUrl}
logoUrl={chain.icon?.url}
/>
@@ -131,7 +145,7 @@ export default async function ChainPageLayout(props: {
<div className="flex flex-col gap-3 md:gap-2">
{/* Gas Sponsored badge - Mobile */}
{chainMetadata?.gasSponsored && (
{customChainMetadata?.gasSponsored && (
<div className="flex md:hidden">
<GasSponsoredBadge />
</div>
@@ -153,7 +167,7 @@ export default async function ChainPageLayout(props: {
)}
{/* Gas Sponsored badge - Desktop */}
{chainMetadata?.gasSponsored && (
{customChainMetadata?.gasSponsored && (
<div className="hidden md:block">
<GasSponsoredBadge />
</div>
@@ -161,9 +175,9 @@ export default async function ChainPageLayout(props: {
</div>
{/* description */}
{chainMetadata?.about && (
<p className="mb-2 whitespace-pre-line text-muted-foreground text-sm lg:text-base">
{chainMetadata.about}
{description && (
<p className="mb-2 whitespace-pre-line text-muted-foreground text-sm lg:text-base text-pretty max-w-3xl">
{description}
</p>
)}
@@ -1,7 +1,9 @@
import { CircleAlertIcon } from "lucide-react";
import { getRawAccount } from "@/api/account/get-account";
import { FaqSection } from "@/components/blocks/faq-section";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getChain, getChainMetadata } from "../../utils";
import { getChain, getCustomChainMetadata } from "../../utils";
import { fetchChainSeo } from "./apis/chain-seo";
import { BuyFundsSection } from "./components/client/BuyFundsSection";
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
import { ChainCTA } from "./components/server/cta-card";
@@ -9,22 +11,27 @@ import { ExplorersSection } from "./components/server/explorer-section";
import { FaucetSection } from "./components/server/FaucetSection";
import { SupportedProductsSection } from "./components/server/SupportedProductsSection";
export default async function Page(props: {
type Props = {
params: Promise<{ chain_id: string }>;
}) {
};
export default async function Page(props: Props) {
const params = await props.params;
const chain = await getChain(params.chain_id);
const chainMetadata = await getChainMetadata(chain.chainId);
const customChainMetadata = getCustomChainMetadata(Number(params.chain_id));
const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch(
() => undefined,
);
const client = getClientThirdwebClient();
const isDeprecated = chain.status === "deprecated";
const account = await getRawAccount();
return (
<div className="flex flex-col gap-10">
{/* Custom CTA */}
{(chainMetadata?.cta?.title || chainMetadata?.cta?.description) && (
<ChainCTA {...chainMetadata.cta} />
{(customChainMetadata?.cta?.title ||
customChainMetadata?.cta?.description) && (
<ChainCTA {...customChainMetadata.cta} />
)}
{/* Deprecated Alert */}
@@ -58,6 +65,12 @@ export default async function Page(props: {
{chain.services.filter((s) => s.enabled).length > 0 && (
<SupportedProductsSection services={chain.services} />
)}
{chainSeo?.faqs && chainSeo.faqs.length > 0 && (
<div className="py-10">
<FaqSection faqs={chainSeo.faqs} />
</div>
)}
</div>
);
}
@@ -58,6 +58,7 @@ export function NFTGetAllTable({
includeOwners: true,
start: currentPage * pageSize,
tokenByIndex,
useIndexer: false, // TEMPORARY, TODO: Remove when this is fixed in insight
},
);
@@ -5,7 +5,7 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { ChainSupportedService } from "@/types/chain";
import { ChainIcon } from "../../../components/server/chain-icon";
import { getChainMetadata } from "../../../utils";
import { getCustomChainMetadata } from "../../../utils";
type ChainListCardProps = {
favoriteButton: JSX.Element | undefined;
@@ -18,7 +18,7 @@ type ChainListCardProps = {
iconUrl?: string;
};
export async function ChainListCard({
export function ChainListCard({
isDeprecated,
chainId,
chainName,
@@ -28,7 +28,7 @@ export async function ChainListCard({
favoriteButton,
iconUrl,
}: ChainListCardProps) {
const chainMetadata = await getChainMetadata(chainId);
const customChainMetadata = getCustomChainMetadata(chainId);
return (
<div className="relative h-full">
@@ -90,9 +90,9 @@ export async function ChainListCard({
</tbody>
</table>
{(isDeprecated || chainMetadata?.gasSponsored) && (
{(isDeprecated || customChainMetadata?.gasSponsored) && (
<div className="mt-5 flex gap-5 border-t pt-4">
{!isDeprecated && chainMetadata?.gasSponsored && (
{!isDeprecated && customChainMetadata?.gasSponsored && (
<div className="flex items-center gap-1.5">
<TicketCheckIcon className="size-5 text-foreground" />
<p className="text-sm">Gas Sponsored</p>
@@ -13,7 +13,7 @@ import { cn } from "@/lib/utils";
import type { ChainSupportedService } from "@/types/chain";
import { ChainIcon } from "../../../components/server/chain-icon";
import { products } from "../../../components/server/products";
import { getChainMetadata } from "../../../utils";
import { getCustomChainMetadata } from "../../../utils";
type ChainListRowProps = {
favoriteButton: JSX.Element | undefined;
@@ -26,7 +26,7 @@ type ChainListRowProps = {
iconUrl?: string;
};
export async function ChainListRow({
export function ChainListRow({
isDeprecated,
chainId,
chainName,
@@ -36,7 +36,7 @@ export async function ChainListRow({
favoriteButton,
iconUrl,
}: ChainListRowProps) {
const chainMetadata = await getChainMetadata(chainId);
const chainMetadata = getCustomChainMetadata(chainId);
return (
<TableRow className="hover:bg-accent/50" linkBox>
{/* Name */}
@@ -914,9 +914,9 @@ const chainMetaRecord = {
} satisfies Record<number, ExtraChainMetadata>;
// END TEMPORARY
export async function getChainMetadata(
export function getCustomChainMetadata(
chainId: number,
): Promise<(ExtraChainMetadata & { gasSponsored?: true }) | null> {
): (ExtraChainMetadata & { gasSponsored?: true }) | null {
// TODO: fetch this from the API
if (chainId in chainMetaRecord) {
return {
@@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query";
import { Input } from "@workspace/ui/components/input";
import { cn } from "@workspace/ui/lib/utils";
import { ActivityIcon, SearchIcon, TrendingUpIcon } from "lucide-react";
import { useState } from "react";
import { useCallback, useState } from "react";
import { Bridge } from "thirdweb";
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { Button } from "@/components/ui/button";
@@ -15,11 +15,22 @@ const client = getClientThirdwebClient();
const pageSize = 20;
type Filter =
| { type: "filter"; value: "volume" | "market_cap" }
| { type: "search"; value: string };
export function TokenPage(props: { chains: Bridge.chains.Result }) {
const [page, setPage] = useState(1);
const [chainId, setChainId] = useState(1);
const [search, setSearch] = useState("");
const [sortBy, setSortBy] = useState<"volume" | "market_cap">("volume");
const [filter, _setFilter] = useState<Filter>({
type: "filter",
value: "volume",
});
const setFilter = useCallback((filter: Filter) => {
_setFilter(filter);
setPage(1);
}, []);
const tokensQuery = useQuery({
queryKey: [
@@ -27,8 +38,7 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
{
page,
chainId,
sortBy,
search,
filter,
},
],
queryFn: () => {
@@ -37,8 +47,8 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
chainId: chainId,
limit: pageSize,
offset: (page - 1) * pageSize,
sortBy: search ? undefined : sortBy,
query: search ? search : undefined,
sortBy: filter.type === "filter" ? filter.value : undefined,
query: filter.type === "search" ? filter.value : undefined,
});
},
refetchOnMount: false,
@@ -62,19 +72,19 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
<SortButton
label="Popular"
onClick={() => {
setSortBy("market_cap");
setSearch("");
setFilter({ type: "filter", value: "market_cap" });
}}
isSelected={sortBy === "market_cap" && !search}
isSelected={
filter.type === "filter" && filter.value === "market_cap"
}
icon={ActivityIcon}
/>
<SortButton
label="Trending"
onClick={() => {
setSortBy("volume");
setSearch("");
setFilter({ type: "filter", value: "volume" });
}}
isSelected={sortBy === "volume" && !search}
isSelected={filter.type === "filter" && filter.value === "volume"}
icon={TrendingUpIcon}
/>
</div>
@@ -83,9 +93,11 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
<SearchIcon className="size-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search Tokens"
value={search}
value={filter.type === "search" ? filter.value : ""}
className="rounded-xl bg-card flex-1 pl-9"
onChange={(e) => setSearch(e.target.value)}
onChange={(e) =>
setFilter({ type: "search", value: e.target.value })
}
/>
</div>
</div>
@@ -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"
>
@@ -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");
@@ -218,6 +231,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 +294,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 +446,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,
});
},
});
}
}
@@ -357,41 +399,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={() => {
@@ -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">
@@ -215,7 +207,7 @@ export function ServerWalletsTableUI({
<PaginationContent>
<PaginationItem>
<Link
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
currentPage > 1 ? currentPage - 1 : 1
}`}
legacyBehavior
@@ -232,7 +224,7 @@ export function ServerWalletsTableUI({
(pageNumber) => (
<PaginationItem key={`page-${pageNumber}`}>
<Link
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${pageNumber}`}
href={`/team/${teamSlug}/${project.slug}/transactions?page=${pageNumber}`}
passHref
>
<PaginationLink isActive={currentPage === pageNumber}>
@@ -244,7 +236,7 @@ export function ServerWalletsTableUI({
)}
<PaginationItem>
<Link
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
currentPage < totalPages ? currentPage + 1 : totalPages
}`}
passHref
@@ -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,
},
};
}
@@ -0,0 +1,36 @@
"use client";
import { useTheme } from "next-themes";
import { DarkVeil } from "@/components/DarkVeil";
const bgBackground: Record<string, [number, number, number]> = {
dark: [0, 0, 0],
light: [250 / 255, 250 / 255, 250 / 255],
};
const hues = {
light: 65,
dark: -85,
};
const patternLightnesss = {
light: -1,
dark: 0.8,
};
export function BridgeVeil() {
const theme = useTheme();
const isLight = theme.theme === "light";
const patternLightness = isLight
? patternLightnesss.light
: patternLightnesss.dark;
const hueShift = isLight ? hues.light : hues.dark;
return (
<DarkVeil
backgroundColor={bgBackground[isLight ? "light" : "dark"]}
patternLightness={patternLightness}
hueShift={hueShift}
warpAmount={0.5}
/>
);
}
@@ -16,13 +16,11 @@ export function UniversalBridgeEmbed({
const chain = useV5DashboardChain(chainId || 1);
return (
<div className="w-full lg:w-[400px]">
<BuyAndSwapEmbed
chain={chain}
buyAmount={amount}
tokenAddress={token?.address}
pageType="bridge"
/>
</div>
<BuyAndSwapEmbed
chain={chain}
buyAmount={amount}
tokenAddress={token?.address}
pageType="bridge"
/>
);
}
@@ -0,0 +1,33 @@
"use client";
import { ChevronRightIcon, DollarSignIcon, TrendingUpIcon } from "lucide-react";
import Link from "next/link";
import { reportBridgePageLinkClick } from "@/analytics/report";
import { cn } from "@/lib/utils";
export function PillLink(props: {
children: React.ReactNode;
href: string;
linkType: "integrate-bridge" | "trending-tokens";
}) {
const Icon =
props.linkType === "integrate-bridge" ? DollarSignIcon : TrendingUpIcon;
return (
<Link
href={props.href}
target="_blank"
onClick={() => reportBridgePageLinkClick({ linkType: props.linkType })}
className={cn(
"shadow-sm text-center justify-center inline-flex items-center gap-4 text-foreground group",
"text-sm bg-card/50 backdrop-blur-lg border border-border/70",
"rounded-full px-4 py-2.5 transition-colors duration-300 text-pretty leading-5",
"hover:bg-pink-300/20 dark:hover:bg-pink-950/30 hover:text-foreground hover:border-pink-400 dark:hover:border-pink-800",
)}
>
<div className="flex items-center gap-2.5">
<Icon className="size-4 shrink-0 text-pink-600" />
{props.children}
</div>
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground/50 group-hover:translate-x-1 transition-all duration-300 group-hover:text-foreground" />
</Link>
);
}
@@ -6,10 +6,10 @@ import { ThirdwebMiniLogo } from "../../(app)/components/ThirdwebMiniLogo";
export function PageHeader(props: { containerClassName?: string }) {
return (
<div className="border-b border-dashed">
<div className="border-b border-border/50 border-dashed">
<header
className={cn(
"container flex max-w-7xl justify-between py-3",
"container flex max-w-7xl justify-between py-4",
props.containerClassName,
)}
>
@@ -30,7 +30,7 @@ export function PageHeader(props: { containerClassName?: string }) {
>
Docs
</Link>
<ToggleThemeButton />
<ToggleThemeButton className="bg-transparent" />
<PublicPageConnectButton connectButtonClassName="!rounded-full" />
</div>
</header>
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+117 -22
View File
@@ -4,14 +4,17 @@ import type { Address } from "thirdweb";
import { defineChain } from "thirdweb/chains";
import { getContract } from "thirdweb/contract";
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
import { FaqSection } from "@/components/blocks/faq-section";
import { AppFooter } from "@/components/footers/app-footer";
import { BridgeVeil } from "./components/BridgeVeil";
import { PillLink } from "./components/client/pill-link";
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
import { PageHeader } from "./components/header";
import { bridgeAppThirdwebClient } from "./constants";
const title = "thirdweb Payments: Swap, Bridge, and Onramp";
const title = "thirdweb Bridge: Buy, Bridge & Swap Crypto on 85+ Chains";
const description =
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Payments.";
"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";
export const metadata: Metadata = {
description,
@@ -52,37 +55,97 @@ export default async function BridgePage({
return (
<div className="grow flex flex-col relative overflow-hidden">
<PageHeader />
<div className="flex grow flex-col items-center justify-center py-36 px-4 min-h-dvh relative overflow-hidden">
<DotsBackgroundPattern />
<UniversalBridgeEmbed
amount={amount as string}
chainId={chainId ? Number(chainId) : undefined}
token={
symbol && decimals && tokenName
? {
address: tokenAddress as Address,
name: tokenName,
symbol,
}
: undefined
}
/>
<div className="absolute top-0 left-0 right-0 h-dvh md:h-[1200px] fade-in-0 animate-in duration-700">
<BridgeVeil />
</div>
<div className="bg-background relative z-10">
<AppFooter />
<div className="relative z-10">
<PageHeader />
<div className="h-20" />
<div className="relative z-10">
<div className="container">
<h1 className="text-5xl md:text-6xl font-bold mb-6 tracking-tighter text-center leading-none text-pretty">
Bridge and Swap tokens <br className="max-sm:hidden" /> across any
chain, instantly
</h1>
<div className="flex-wrap flex gap-3 items-center justify-center">
<DataPill>85+ Chains Supported</DataPill>
<DataPill>3200+ Tokens Supported</DataPill>
<DataPill>9+ Million Routes Available</DataPill>
</div>
</div>
<div className="h-16" />
<div className="flex grow items-center justify-center px-4 relative">
<DotsBackgroundPattern />
<UniversalBridgeEmbed
amount={amount as string}
chainId={chainId ? Number(chainId) : undefined}
token={
symbol && decimals && tokenName
? {
address: tokenAddress as Address,
name: tokenName,
symbol,
}
: undefined
}
/>
</div>
</div>
<div className="h-32" />
<div className="flex flex-col gap-4 items-center container">
<PillLink
href="https://portal.thirdweb.com/bridge"
linkType="integrate-bridge"
>
Integrate Bridge in your apps in minutes, and start generating
revenue
</PillLink>
<PillLink
href="https://thirdweb.com/tokens"
linkType="trending-tokens"
>
Discover Trending Tokens
</PillLink>
</div>
<div className="h-32" />
<div className="container max-w-2xl">
<BridgeFaqSection />
</div>
<div className="h-32" />
<div className="relative">
<AppFooter />
</div>
</div>
</div>
);
}
function DataPill(props: { children: React.ReactNode }) {
return (
<p className="flex items-center gap-1.5 text-foreground/50 text-xs bg-accent/30 backdrop-blur-lg border border-border/70 rounded-full px-3 py-1.5 hover:text-foreground transition-colors duration-300">
{props.children}
</p>
);
}
function DotsBackgroundPattern(props: { className?: string }) {
return (
<div
className={cn(
"pointer-events-none absolute inset-0 text-foreground/30 dark:text-foreground/10",
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-pink-700/30 dark:text-pink-500/20",
props.className,
)}
style={{
@@ -94,3 +157,35 @@ function DotsBackgroundPattern(props: { className?: string }) {
/>
);
}
const bridgeFaqs: Array<{ title: string; description: string }> = [
{
title: "What is bridging in crypto?",
description:
"Crypto bridging (cross-chain bridging) moves tokens between blockchains so you can use assets across networks. In thirdweb Bridge, connect your wallet, choose the source token/network and destination token/network, review the route and price, then confirm. Assets arrive after finality, often under ~10 seconds on fast routes, though timing depends on networks and congestion.",
},
{
title: "How does crypto bridging work?",
description:
"Bridge smart contracts lock or burn tokens on the source chain and mint or release equivalents on the destination via verified cross-chain providers. thirdweb Bridge automatically finds the fastest, lowest-cost route and may use different mechanisms based on networks and liquidity. Arrival can range from seconds to minutes depending on finality; many routes complete in ~10 seconds",
},
{
title: "What is a crypto asset swap?",
description:
"A crypto swap exchanges one token for another via a DEX or aggregator. thirdweb Bridge lets you bridge + swap in one step. For example, ETH on Ethereum to USDC on Base, by selecting your start and end tokens/networks and confirming.",
},
{
title: "How can I get stablecoins like USDC or USDT?",
description:
"Use thirdweb Bridge to convert assets you hold into USDC or USDT on your chosen network: select your current token/network, pick the stablecoin (USDC, USDT, etc) on the destination, and confirm. You can also buy stablecoins with fiat in the Buy flow and bridge if needed. Always verify official token contract addresses.",
},
{
title: "What is the cost of bridging and swapping?",
description:
"Costs include gas on each chain, bridge/liquidity provider fees, and any DEX swap fees or price impact. thirdweb Bridge compares routes and selects the best price route. Save by using lower-gas times or combining bridge + swap in one flow.",
},
];
function BridgeFaqSection() {
return <FaqSection faqs={bridgeFaqs} />;
}
+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 [
{
+1
View File
@@ -26,6 +26,7 @@
"clsx": "^2.1.1",
"date-fns": "4.1.0",
"fetch-event-stream": "0.1.5",
"geist": "^1.5.1",
"lucide-react": "0.525.0",
"next": "15.3.5",
"next-themes": "^0.4.6",
@@ -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: "Congratulations! 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,
});
}
+4 -15
View File
@@ -1,26 +1,15 @@
import type { Metadata } from "next";
import { Fira_Code, Inter } from "next/font/google";
import { metadataBase } from "@/lib/constants";
import { cn } from "@/lib/utils";
import { AppSidebarLayout } from "./AppSidebar";
import { Providers } from "./providers";
import "@workspace/ui/global.css";
import { GeistMono } from "geist/font/mono";
import { GeistSans } from "geist/font/sans";
import { ThemeProvider } from "next-themes";
import NextTopLoader from "nextjs-toploader";
import { Toaster } from "sonner";
const sansFont = Inter({
subsets: ["latin"],
variable: "--font-sans",
weight: "variable",
});
const monoFont = Fira_Code({
subsets: ["latin"],
variable: "--font-mono",
weight: "variable",
});
export const metadata: Metadata = {
description: "thirdweb playground",
metadataBase,
@@ -37,8 +26,8 @@ export default async function RootLayout({
<body
className={cn(
"bg-background font-sans antialiased ",
sansFont.variable,
monoFont.variable,
GeistSans.variable,
GeistMono.variable,
)}
>
<ThemeProvider
@@ -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: {},
@@ -0,0 +1,168 @@
"use client";
import type React from "react";
import { useId, useState } from "react";
import { defineChain } from "thirdweb/chains";
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TokenSelector } from "@/components/ui/TokenSelector";
import { THIRDWEB_CLIENT } from "@/lib/client";
import type { TokenMetadata } from "@/lib/types";
import type { X402PlaygroundOptions } from "./types";
export function X402LeftSection(props: {
options: X402PlaygroundOptions;
setOptions: React.Dispatch<React.SetStateAction<X402PlaygroundOptions>>;
}) {
const { options, setOptions } = props;
// Local state for chain and token selection
const [selectedChain, setSelectedChain] = useState<number | undefined>(() => {
return options.chain?.id;
});
const [selectedToken, setSelectedToken] = useState<
{ chainId: number; address: string } | undefined
>(() => {
if (options.tokenAddress && options.chain?.id) {
return {
address: options.tokenAddress,
chainId: options.chain.id,
};
}
return undefined;
});
const chainId = useId();
const tokenId = useId();
const amountId = useId();
const waitUntilId = useId();
const handleChainChange = (chainId: number) => {
setSelectedChain(chainId);
// Clear token selection when chain changes
setSelectedToken(undefined);
setOptions((v) => ({
...v,
chain: defineChain(chainId),
tokenAddress: "0x0000000000000000000000000000000000000000" as const,
tokenSymbol: "",
tokenDecimals: 18,
}));
};
const handleTokenChange = (token: TokenMetadata) => {
setSelectedToken({
address: token.address,
chainId: selectedChain!,
});
setOptions((v) => ({
...v,
tokenAddress: token.address as `0x${string}`,
tokenSymbol: token.symbol ?? "",
tokenDecimals: token.decimals ?? 18,
}));
};
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setOptions((v) => ({
...v,
amount: e.target.value,
}));
};
const handleWaitUntilChange = (
value: "simulated" | "submitted" | "confirmed",
) => {
setOptions((v) => ({
...v,
waitUntil: value,
}));
};
return (
<div className="space-y-6">
<div>
<h2 className="mb-4 text-xl font-semibold">Configuration</h2>
<div className="space-y-4">
{/* Chain selection */}
<div className="flex flex-col gap-2">
<Label htmlFor={chainId}>Chain</Label>
<BridgeNetworkSelector
chainId={selectedChain}
onChange={handleChainChange}
placeholder="Select a chain"
className="bg-card"
/>
</div>
{/* Token selection - only show if chain is selected */}
{selectedChain && (
<div className="flex flex-col gap-2">
<Label htmlFor={tokenId}>Token</Label>
<TokenSelector
chainId={selectedChain}
client={THIRDWEB_CLIENT}
enabled={true}
onChange={handleTokenChange}
placeholder="Select a token"
selectedToken={selectedToken}
className="bg-card"
/>
</div>
)}
{/* Amount input */}
<div className="flex flex-col gap-2">
<Label htmlFor={amountId}>Amount</Label>
<Input
id={amountId}
type="text"
placeholder="0.01"
value={options.amount}
onChange={handleAmountChange}
className="bg-card"
/>
{options.tokenSymbol && (
<p className="text-sm text-muted-foreground">
Amount in {options.tokenSymbol}
</p>
)}
</div>
{/* Wait Until selection */}
<div className="flex flex-col gap-2">
<Label htmlFor={waitUntilId}>Wait Until</Label>
<Select
value={options.waitUntil}
onValueChange={handleWaitUntilChange}
>
<SelectTrigger className="bg-card">
<SelectValue placeholder="Select wait condition" />
</SelectTrigger>
<SelectContent>
<SelectItem value="simulated">Simulated</SelectItem>
<SelectItem value="submitted">Submitted</SelectItem>
<SelectItem value="confirmed">Confirmed</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
When to consider the payment settled: simulated (fastest),
submitted (medium), or confirmed (most secure)
</p>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,45 @@
"use client";
import React, { useState } from "react";
import { useActiveAccount } from "thirdweb/react";
import { chain, token } from "./constants";
import type { X402PlaygroundOptions } from "./types";
import { X402LeftSection } from "./X402LeftSection";
import { X402RightSection } from "./X402RightSection";
const defaultOptions: X402PlaygroundOptions = {
chain: chain,
tokenAddress: token.address as `0x${string}`,
tokenSymbol: token.symbol,
tokenDecimals: token.decimals,
amount: "0.01",
payTo: "0x0000000000000000000000000000000000000000",
waitUntil: "simulated",
};
export function X402Playground() {
const [options, setOptions] = useState<X402PlaygroundOptions>(defaultOptions);
const activeAccount = useActiveAccount();
// Update payTo address when wallet connects, but only if it's still the default
React.useEffect(() => {
if (
activeAccount?.address &&
options.payTo === "0x0000000000000000000000000000000000000000"
) {
setOptions((prev) => ({
...prev,
payTo: activeAccount.address as `0x${string}`,
}));
}
}, [activeAccount?.address, options.payTo]);
return (
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
<X402LeftSection options={options} setOptions={setOptions} />
</div>
<X402RightSection options={options} />
</div>
);
}
@@ -0,0 +1,307 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { Badge } from "@workspace/ui/components/badge";
import { CodeClient } from "@workspace/ui/components/code/code.client";
import { CodeIcon, LockIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { useState } from "react";
import {
ConnectButton,
useActiveAccount,
useActiveWallet,
} from "thirdweb/react";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { THIRDWEB_CLIENT } from "@/lib/client";
import { cn } from "@/lib/utils";
import type { X402PlaygroundOptions } from "./types";
type Tab = "ui" | "client-code" | "server-code";
export function X402RightSection(props: { options: X402PlaygroundOptions }) {
const pathname = usePathname();
const [previewTab, _setPreviewTab] = useState<Tab>(() => {
return "ui";
});
function setPreviewTab(tab: "ui" | "client-code" | "server-code") {
_setPreviewTab(tab);
window.history.replaceState({}, "", `${pathname}?tab=${tab}`);
}
const activeWallet = useActiveWallet();
const activeAccount = useActiveAccount();
const paidApiCall = useMutation({
mutationFn: async () => {
if (!activeWallet) {
throw new Error("No active wallet");
}
const fetchWithPay = wrapFetchWithPayment(
fetch,
THIRDWEB_CLIENT,
activeWallet,
BigInt(1 * 10 ** 18),
);
const searchParams = new URLSearchParams();
searchParams.set("chainId", props.options.chain.id.toString());
searchParams.set("payTo", props.options.payTo);
searchParams.set("amount", props.options.amount);
searchParams.set("tokenAddress", props.options.tokenAddress);
searchParams.set("decimals", props.options.tokenDecimals.toString());
searchParams.set("waitUntil", props.options.waitUntil);
const url =
"/api/paywall" +
(searchParams.size > 0 ? "?" + searchParams.toString() : "");
const response = await fetchWithPay(url.toString());
return response.json();
},
});
const handlePayClick = async () => {
paidApiCall.mutate();
};
const clientCode = `import { createThirdwebClient } from "thirdweb";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { useActiveWallet } from "thirdweb/react";
const client = createThirdwebClient({ clientId: "your-client-id" });
export default function Page() {
const wallet = useActiveWallet();
const onClick = async () => {
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
const response = await fetchWithPay('/api/paid-endpoint');
}
return (
<Button onClick={onClick}>Pay Now</Button>
);
}`;
const serverCode = `// Usage in a Next.js API route
import { settlePayment, facilitator } from "thirdweb/x402";
import { createThirdwebClient } from "thirdweb";
import { defineChain } from "thirdweb/chains";
const client = createThirdwebClient({
secretKey: process.env.THIRDWEB_SECRET_KEY,
});
const thirdwebFacilitator = facilitator({
client,
serverWalletAddress: "0xYourServerWalletAddress",
waitUtil: "${props.options.waitUntil}",
});
export async function POST(request: Request) {
const paymentData = request.headers.get("x-payment");
// verify and process the payment
const result = await settlePayment({
resourceUrl: "https://api.example.com/premium-content",
method: "POST",
paymentData,
payTo: "${props.options.payTo}",
network: defineChain(${props.options.chain.id}),
price: {
amount: "${Number(props.options.amount) * 10 ** props.options.tokenDecimals}",
asset: {
address: "${props.options.tokenAddress}",
},
},
facilitator: thirdwebFacilitator,
});
if (result.status === 200) {
// Payment verified and settled successfully
return Response.json({ data: "premium content" });
} else {
// Payment required
return Response.json(result.responseBody, {
status: result.status,
headers: result.responseHeaders,
});
}
}`;
return (
<div className="flex shrink-0 flex-col gap-4 xl:sticky xl:top-0 xl:w-[764px]">
<TabButtons
tabs={[
{
isActive: previewTab === "ui",
name: "UI",
onClick: () => setPreviewTab("ui"),
},
{
isActive: previewTab === "client-code",
name: "Client Code",
onClick: () => setPreviewTab("client-code"),
},
{
isActive: previewTab === "server-code",
name: "Server Code",
onClick: () => setPreviewTab("server-code"),
},
]}
/>
<div
className={cn(
"relative flex min-h-[200px] grow justify-center rounded-lg",
previewTab === "ui" && "items-center",
)}
>
<BackgroundPattern />
{previewTab === "ui" && (
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
<ConnectButton
client={THIRDWEB_CLIENT}
chain={props.options.chain}
detailsButton={{
displayBalanceToken:
props.options.tokenAddress !==
"0x0000000000000000000000000000000000000000"
? {
[props.options.chain.id]: props.options.tokenAddress,
}
: undefined,
}}
supportedTokens={
props.options.tokenAddress !==
"0x0000000000000000000000000000000000000000"
? {
[props.options.chain.id]: [
{
address: props.options.tokenAddress,
symbol: props.options.tokenSymbol,
name: props.options.tokenSymbol,
},
],
}
: undefined
}
/>
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<LockIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">Paid API Call</span>
<Badge variant="success">
<span className="text-xl font-bold">
{props.options.amount}{" "}
{props.options.tokenSymbol || "tokens"}
</span>
</Badge>
</div>
<Button
onClick={handlePayClick}
className="w-full mb-4"
size="lg"
disabled={paidApiCall.isPending || !activeAccount}
>
Access Premium Content
</Button>
<p className="text-sm text-muted-foreground">
Pay for access with {props.options.tokenSymbol || "tokens"} on{" "}
{props.options.chain.name || `chain ${props.options.chain.id}`}
</p>
</Card>
<Card className="p-6">
<div className="flex items-center gap-3 mb-2">
<CodeIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">API Call Response</span>
</div>
{paidApiCall.isPending && (
<div className="text-center">Loading...</div>
)}
{paidApiCall.isError && (
<div className="text-center">
Error: {paidApiCall.error.message}
</div>
)}
{paidApiCall.data && (
<CodeClient
code={JSON.stringify(paidApiCall.data, null, 2)}
lang="json"
/>
)}
</Card>
</div>
)}
{previewTab === "client-code" && (
<div className="w-full">
<CodeClient
className="h-full rounded-none border-none"
code={clientCode}
lang="tsx"
/>
</div>
)}
{previewTab === "server-code" && (
<div className="w-full">
<CodeClient
className="h-full rounded-none border-none"
code={serverCode}
lang="typescript"
/>
</div>
)}
</div>
</div>
);
}
function BackgroundPattern() {
const color = "hsl(var(--foreground)/15%)";
return (
<div
className="absolute inset-0 z-[-1]"
style={{
backgroundImage: `radial-gradient(${color} 1px, transparent 1px)`,
backgroundSize: "24px 24px",
maskImage:
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 60%)",
}}
/>
);
}
function TabButtons(props: {
tabs: Array<{
name: string;
isActive: boolean;
onClick: () => void;
}>;
}) {
return (
<div>
<div className="flex justify-start gap-1 rounded-lg border bg-card p-2 shadow-md md:inline-flex">
{props.tabs.map((tab) => (
<Button
className={cn(
"gap-2 px-4 text-base",
tab.isActive
? "bg-accent text-foreground"
: "bg-transparent text-muted-foreground",
)}
key={tab.name}
onClick={tab.onClick}
variant="ghost"
>
{tab.name}
</Button>
))}
</div>
</div>
);
}
@@ -0,0 +1,33 @@
import { base } from "thirdweb/chains";
export const chain = base;
export const token = {
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
name: "USD Coin",
symbol: "USDC",
decimals: 6,
};
// export const chain = base;
// export const token = {
// address: "0x0578d8A44db98B23BF096A382e016e29a5Ce0ffe",
// decimals: 18,
// name: "Higher",
// symbol: "HIGHER",
// version: "1",
// };
// export const token = {
// address: "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798",
// decimals: 18,
// name: "Stable Coin",
// symbol: "SBC",
// version: "1",
// // primaryType: "Permit",
// }
// export const chain = defineChain(3338);
// export const token = {
// address: "0xbbA60da06c2c5424f03f7434542280FCAd453d10",
// decimals: 6,
// name: "USDC",
// symbol: "USDC",
// version: "2",
// }
@@ -0,0 +1,12 @@
import type { Chain } from "thirdweb/chains";
import type { Address } from "thirdweb/utils";
export type X402PlaygroundOptions = {
chain: Chain;
tokenAddress: Address;
tokenSymbol: string;
tokenDecimals: number;
amount: string;
payTo: Address;
waitUntil: "simulated" | "submitted" | "confirmed";
};
@@ -1,102 +0,0 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { CodeClient } from "@workspace/ui/components/code/code.client";
import { CodeIcon, LockIcon } from "lucide-react";
import { arbitrumSepolia } from "thirdweb/chains";
import {
ConnectButton,
getDefaultToken,
useActiveAccount,
useActiveWallet,
} from "thirdweb/react";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { THIRDWEB_CLIENT } from "../../../../lib/client";
const chain = arbitrumSepolia;
const token = getDefaultToken(chain, "USDC");
export function X402ClientPreview() {
const activeWallet = useActiveWallet();
const activeAccount = useActiveAccount();
const paidApiCall = useMutation({
mutationFn: async () => {
if (!activeWallet) {
throw new Error("No active wallet");
}
const fetchWithPay = wrapFetchWithPayment(
fetch,
THIRDWEB_CLIENT,
activeWallet,
);
const response = await fetchWithPay("/api/paywall");
return response.json();
},
});
const handlePayClick = async () => {
paidApiCall.mutate();
};
return (
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
<ConnectButton
client={THIRDWEB_CLIENT}
chain={chain}
detailsButton={{
displayBalanceToken: {
[chain.id]: token!.address,
},
}}
supportedTokens={{
[chain.id]: [token!],
}}
/>
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<LockIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">Paid API Call</span>
<span className="text-xl font-bold text-red-600">$0.01</span>
</div>
<Button
onClick={handlePayClick}
className="w-full mb-4"
size="lg"
disabled={paidApiCall.isPending || !activeAccount}
>
Pay Now
</Button>
<p className="text-sm text-muted-foreground">
{" "}
<a
className="underline"
href={"https://faucet.circle.com/"}
target="_blank"
rel="noopener noreferrer"
>
Click here to get USDC on {chain.name}
</a>
</p>
</Card>
<Card className="p-6">
<div className="flex items-center gap-3 mb-2">
<CodeIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">API Call Response</span>
</div>
{paidApiCall.isPending && <div className="text-center">Loading...</div>}
{paidApiCall.isError && (
<div className="text-center">Error: {paidApiCall.error.message}</div>
)}
{paidApiCall.data && (
<CodeClient
code={JSON.stringify(paidApiCall.data, null, 2)}
lang="json"
/>
)}
</Card>
</div>
);
}
@@ -1,10 +1,8 @@
import { CodeServer } from "@workspace/ui/components/code/code.server";
import { CircleDollarSignIcon, Code2Icon } from "lucide-react";
import { CodeExample, TabName } from "@/components/code/code-example";
import { CircleDollarSignIcon } from "lucide-react";
import ThirdwebProvider from "@/components/thirdweb-provider";
import { PageLayout } from "../../../components/blocks/APIHeader";
import { createMetadata } from "../../../lib/metadata";
import { X402ClientPreview } from "./components/x402-client-preview";
import { X402Playground } from "./components/X402Playground";
const title = "x402 Payments";
const description =
@@ -30,111 +28,8 @@ export default function Page() {
description={description}
docsLink="https://portal.thirdweb.com/payments/x402?utm_source=playground"
>
<X402Example />
<div className="h-8" />
<ServerCodeExample />
<X402Playground />
</PageLayout>
</ThirdwebProvider>
);
}
function ServerCodeExample() {
return (
<>
<div className="mb-4">
<h2 className="font-semibold text-xl tracking-tight">
Next.js Server Code Example
</h2>
<p className="max-w-4xl text-muted-foreground text-balance text-sm md:text-base">
Create middleware with the thirdweb facilitator to settle transactions
with your server wallet.
</p>
</div>
<div className="overflow-hidden rounded-lg border bg-card">
<div className="flex grow flex-col border-b md:border-r md:border-b-0">
<TabName icon={Code2Icon} name="Server Code" />
<CodeServer
className="h-full rounded-none border-none"
code={`// src/middleware.ts
import { facilitator, settlePayment } from "thirdweb/x402";
import { createThirdwebClient } from "thirdweb";
const client = createThirdwebClient({ secretKey: "your-secret-key" });
const thirdwebX402Facilitator = facilitator({
client,
serverWalletAddress: "0xYourWalletAddress",
});
export async function middleware(request: NextRequest) {
const method = request.method.toUpperCase();
const resourceUrl = request.nextUrl.toString();
const paymentData = request.headers.get("X-PAYMENT");
const result = await settlePayment({
resourceUrl,
method,
paymentData,
payTo: "0xYourWalletAddress",
network: "eip155:11155111", // or any other chain id
price: "$0.01", // can also be a ERC20 token amount
facilitator: thirdwebX402Facilitator,
});
if (result.status === 200) {
// payment successful, execute the request
return NextResponse.next();
}
// 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/paid-endpoint"],
};
`}
lang="tsx"
/>
</div>
</div>
</>
);
}
function X402Example() {
return (
<CodeExample
header={{
title: "Client Code Example",
description:
"Wrap your fetch requests with the `wrapFetchWithPayment` function to enable x402 payments.",
}}
code={`import { createThirdwebClient } from "thirdweb";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { useActiveWallet } from "thirdweb/react";
const client = createThirdwebClient({ clientId: "your-client-id" });
export default function Page() {
const wallet = useActiveWallet();
const onClick = async () => {
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
const response = await fetchWithPay('/api/paid-endpoint');
}
return (
<Button onClick={onClick}>Pay Now</Button>
);
}`}
lang="tsx"
preview={<X402ClientPreview />}
/>
);
}
@@ -197,7 +197,7 @@ export function RightSection(props: {
}
auth={connectOptions.enableAuth ? playgroundAuth : undefined}
autoConnect={false}
className="shadow-xl !max-w-full"
className="shadow-xl"
client={THIRDWEB_CLIENT}
header={{
title: connectOptions.modalTitle,
@@ -49,7 +49,6 @@ export function StyledConnectEmbed(
abstract,
]}
client={THIRDWEB_CLIENT}
className="!max-w-full"
theme={theme === "light" ? "light" : "dark"}
wallets={WALLETS}
{...props}
-61
View File
@@ -1,61 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { createThirdwebClient } from "thirdweb";
import { arbitrumSepolia } from "thirdweb/chains";
import { facilitator, settlePayment } from "thirdweb/x402";
const client = createThirdwebClient({
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
});
const chain = arbitrumSepolia;
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_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,
});
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 result = await settlePayment({
resourceUrl,
method,
paymentData,
payTo: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
network: `eip155:${chain.id}`,
price: "$0.01",
routeConfig: {
description: "Access to paid content",
},
facilitator: twFacilitator,
});
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"],
};
+10
View File
@@ -3,6 +3,16 @@ import type { Config } from "tailwindcss";
const config: Config = {
...tailwindConfig,
theme: {
...tailwindConfig.theme,
extend: {
...tailwindConfig.theme?.extend,
fontFamily: {
sans: ["var(--font-geist-sans)"],
mono: ["var(--font-geist-mono)"],
},
},
},
content: [
"./src/**/*.{ts,tsx}",
// add contents of ui package
+1
View File
@@ -17,6 +17,7 @@
"clsx": "^2.1.1",
"date-fns": "4.1.0",
"flexsearch": "^0.7.43",
"geist": "^1.5.1",
"github-slugger": "^2.0.0",
"he": "^1.2.0",
"lucide-react": "0.525.0",
+1 -1
View File
@@ -414,7 +414,7 @@ const reactNativeRedirects = {
const unityRedirects = {
// top level
"/unity": "/unity/v5",
"/unity": "/unity/v6",
// blocks
"/unity/blocks/getblock": "/unity/v4/blocks/getblock",
"/unity/blocks/getblockwithtransactions":
+94 -96
View File
@@ -213,133 +213,129 @@ export function Header() {
const pathname = usePathname();
return (
<header className="flex w-full flex-col gap-2 border-b bg-background p-4 xl:pb-0 lg:px-8 overflow-hidden">
{/* Top row */}
<div className="container flex items-center justify-between gap-6">
<div className="flex items-center gap-2">
<Link
aria-label="thirdweb Docs"
className="flex items-center gap-2"
href="/"
title="thirdweb Docs"
>
<ThirdwebIcon className="size-8" />
<span className="font-bold text-[23px] text-foreground leading-none tracking-tight">
Docs
</span>
</Link>
</div>
<div className="flex items-center gap-3">
<div className="hidden xl:block">
<header className="flex w-full border-b overflow-hidden relative">
<div className="container flex-col">
{/* Top row */}
<div className="flex items-center justify-between gap-6 relative z-10 py-4 lg:pt-5">
<div className="flex items-center gap-2">
<Link
className="text-foreground"
href="https://github.com/thirdweb-dev"
target="_blank"
aria-label="thirdweb Docs"
className="flex items-center gap-2"
href="/"
title="thirdweb Docs"
>
<GithubIcon className="size-6 lg:size-5" />
<ThirdwebIcon className="size-8" />
<span className="font-bold text-[23px] text-foreground leading-none tracking-tight">
Docs
</span>
</Link>
</div>
<div className="hidden xl:block">
<ThemeSwitcher className="border-none bg-transparent" />
</div>
<div className="flex items-center gap-4">
<div className="hidden xl:block">
<Link
className="text-foreground"
href="https://github.com/thirdweb-dev"
target="_blank"
>
<GithubIcon className="size-6 lg:size-5" />
</Link>
</div>
<div className="hidden xl:block">
<DocSearch variant="search" />
</div>
<div className="hidden xl:block">
<ThemeSwitcher className="border-none bg-transparent" />
</div>
<div className="hidden xl:block">
<ChatButton />
</div>
<div className="hidden xl:block">
<DocSearch variant="search" />
</div>
<div className="flex items-center gap-1 xl:hidden">
<ThemeSwitcher className="border-none bg-transparent" />
<DocSearch variant="icon" />
<ChatButton />
<Button
className="p-2"
onClick={() => setShowBurgerMenu(!showBurgerMenu)}
variant="ghost"
>
<MenuIcon className="size-7" />
</Button>
<div className="hidden xl:block">
<ChatButton />
</div>
<div className="flex items-center gap-1 xl:hidden">
<ThemeSwitcher className="border-none bg-transparent" />
<DocSearch variant="icon" />
<ChatButton />
<Button
className="p-2"
onClick={() => setShowBurgerMenu(!showBurgerMenu)}
variant="ghost"
>
<MenuIcon className="size-7" />
</Button>
</div>
</div>
</div>
</div>
{/* Bottom row - hidden on mobile */}
<div className="container hidden items-center justify-between gap-6 xl:flex mt-1">
<nav className="flex grow gap-5">
<ul className="flex flex-row items-center gap-0 mb-1">
{links.map((link) => {
return (
<li
className="flex items-center py-2 relative px-2.5 hover:text-foreground"
key={link.href}
onClick={() => {
setShowBurgerMenu(false);
}}
onKeyDown={() => {
setShowBurgerMenu(false);
}}
>
<NavLink href={link.href} name={link.name} />
{pathname?.startsWith(link.href) && (
<div className="bg-violet-700 h-[2px] inset-x-0 rounded-full absolute -bottom-1" />
)}
</li>
);
})}
</ul>
</nav>
{/* Bottom row - hidden on mobile */}
<div className="hidden items-start justify-between gap-6 xl:flex relative z-10">
<nav className="flex grow gap-5">
<ul className="flex flex-row items-center gap-0 mb-1.5">
{links.map((link) => {
return (
<li
className="flex items-center relative"
key={link.href}
onClick={() => {
setShowBurgerMenu(false);
}}
onKeyDown={() => {
setShowBurgerMenu(false);
}}
>
<NavLink
href={link.href}
name={link.name}
className="py-2.5 px-3 hover:bg-accent rounded-lg hover:text-foreground font-normal"
/>
{pathname?.startsWith(link.href) && (
<div className="bg-foreground h-[2px] inset-x-0 rounded-full absolute -bottom-1.5" />
)}
</li>
);
})}
</ul>
</nav>
<div className="flex items-center gap-3">
<div className="px-1">
<div className="flex items-center">
<DropdownLinks
category="AI"
links={aiLinks}
onLinkClick={() => setShowBurgerMenu(false)}
/>
</div>
<div className="px-1">
<DropdownLinks
category="SDKs"
links={sdkLinks}
onLinkClick={() => setShowBurgerMenu(false)}
/>
</div>
<div className="px-1">
<DropdownLinks
category="APIs"
links={apisLinks}
onLinkClick={() => setShowBurgerMenu(false)}
/>
</div>
<div className="px-1">
<DropdownLinks
category="Tools"
links={toolLinks}
onLinkClick={() => setShowBurgerMenu(false)}
/>
</div>
<div className="px-1">
<DropdownLinks
category="Support"
links={supportLinks}
onLinkClick={() => setShowBurgerMenu(false)}
/>
</div>
<NavLink
href="/changelog"
name="Changelog"
onClick={() => {
setShowBurgerMenu(false);
}}
/>
<NavLink
href="/changelog"
className="px-3 py-2.5 hover:bg-accent hover:text-foreground rounded-lg"
name="Changelog"
onClick={() => {
setShowBurgerMenu(false);
}}
/>
</div>
</div>
</div>
@@ -428,6 +424,8 @@ export function Header() {
</div>
</div>
)}
<div className="absolute inset-0 bg-card/70 backdrop-blur-xl z-0" />
</header>
);
}
@@ -448,31 +446,29 @@ function DropdownLinks(props: {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="inline-flex items-center gap-1 p-0 font-medium text-muted-foreground text-sm hover:bg-transparent hover:text-foreground"
className="inline-flex items-center gap-1.5 font-normal text-muted-foreground text-sm hover:bg-accent hover:text-foreground rounded-lg px-3 py-2.5"
variant="ghost"
>
{props.category}
<ChevronDownIcon className="size-4 text-muted-foreground opacity-70" />
<ChevronDownIcon className="size-3.5 text-muted-foreground opacity-70" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="flex flex-col gap-1 bg-card p-3"
style={{
minWidth: "150px",
}}
className="flex flex-col gap-1 bg-card p-1 rounded-xl min-w-[200px]"
sideOffset={14}
>
{props.links.map((info) => {
return (
<DropdownMenuItem asChild key={info.name}>
<div
className={clsx(
"relative flex cursor-pointer gap-2 font-medium text-foreground",
"relative flex cursor-pointer gap-3 font-medium text-foreground !rounded-lg px-3 py-2",
"hover:bg-accent",
)}
>
{info.icon && (
<info.icon className="size-5 text-foreground" />
<info.icon className="size-5 text-muted-foreground" />
)}
<Link
className="before:absolute before:inset-0"
@@ -524,6 +520,7 @@ function NavLink(props: {
name: string;
onClick?: () => void;
icon?: React.FC<{ className?: string }>;
className?: string;
}) {
const pathname = usePathname();
return (
@@ -534,6 +531,7 @@ function NavLink(props: {
? "text-foreground"
: "text-muted-foreground",
props.icon ? "flex flex-row gap-3" : "",
props.className,
)}
href={props.href}
onClick={props.onClick}
Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

@@ -0,0 +1,467 @@
import {
Details,
createMetadata,
DocImage
} from "@doc";
import bridgeWidgetDark from "./bridge-widget-dark.png";
import bridgeWidgetLight from "./bridge-widget-light.png";
export const metadata = createMetadata({
image: {
title: "BridgeWidget Script",
icon: "payments",
},
title: "Bridge Widget",
description: "Add a widget to add cross swaps and fiat onramp to your app",
});
# BridgeWidget Script
The BridgeWidget Script makes it easy to embed cross-chain swaps and fiat onramp UI into your app. Just add a script tag to your HTML and get a fully customizable widget — no build setup required.
## Key Features
- Cross-chain token swaps across 85+ blockchains
- Fiat onramp support to buy tokens with credit/debit cards
- Customizable UI - use prebuilt themes or override with your brand colors
- Prefill token selections for a smoother user experience
- Display fiat values in multiple currencies
- Event callbacks to track user actions (success, error, cancel, disconnect)
<div className='dark-only'>
<DocImage src={bridgeWidgetDark} />
</div>
<div className='light-only'>
<DocImage src={bridgeWidgetLight} />
</div>
## Script Integration
```html
<!-- Add this script in <head> -->
<script src="https://unpkg.com/thirdweb/dist/scripts/bridge-widget.js"></script>
<!-- Add a container element where you want to render the UI -->
<div id="bridge-widget-container"></div>
<!-- Initialize the widget at the end of <body> -->
<script>
const container = document.querySelector('#bridge-widget-container');
BridgeWidget.render(container, {
clientId: "your-thirdweb-client-id",
theme: "dark",
buy: {
chainId: 8453,
amount: "0.1",
},
});
</script>
```
## Options
You can customize theme, currencies, token selections, and more. See the full list of options below
<Details summary="View full type">
```ts
type Options = {
clientId: string;
theme?: "dark" | "light" | {
type: "dark" | "light";
accentButtonBg?: string;
accentButtonText?: string;
accentText?: string;
borderColor?: string;
connectedButtonBg?: string;
connectedButtonBgHover?: string;
danger?: string;
inputAutofillBg?: string;
modalBg?: string;
modalOverlayBg?: string;
primaryButtonBg?: string;
primaryButtonText?: string;
primaryText?: string;
scrollbarBg?: string;
secondaryButtonBg?: string;
secondaryButtonHoverBg?: string;
secondaryButtonText?: string;
secondaryIconColor?: string;
secondaryIconHoverBg?: string;
secondaryIconHoverColor?: string;
secondaryText?: string;
selectedTextBg?: string;
selectedTextColor: string;
separatorLine?: string;
skeletonBg: string;
success?: string;
tertiaryBg?: string;
tooltipBg?: string;
tooltipText?: string;
};
showThirdwebBranding?: boolean;
currency?: SupportedFiatCurrency;
swap?: {
className?: string;
style?: React.CSSProperties;
onSuccess?: (quote: SwapPreparedQuote) => void;
onError?: (error: Error, quote: SwapPreparedQuote) => void;
onCancel?: (quote: SwapPreparedQuote) => void;
onDisconnect?: () => void;
persistTokenSelections?: boolean;
prefill?: {
buyToken?: {
tokenAddress?: string;
chainId: number;
amount?: string;
};
sellToken?: {
tokenAddress?: string;
chainId: number;
amount?: string;
};
};
};
buy: {
amount: string;
chainId: number;
tokenAddress?: string;
buttonLabel?: string;
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
onError?: (
error: Error,
quote: BuyOrOnrampPrepareResult | undefined,
) => void;
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
className?: string;
country?: string;
presetOptions?: [number, number, number];
purchaseData?: PurchaseData;
};
}
```
</Details>
### clientId
Set your Project's client ID to identify your project. You create a project for free on [thirdweb dashboard](https://thirdweb.com/team)
### swap
The options to configure the "Swap" tab
```ts
type Swap = {
className?: string;
style?: React.CSSProperties;
onSuccess?: (quote: SwapPreparedQuote) => void;
onError?: (error: Error, quote: SwapPreparedQuote) => void;
onCancel?: (quote: SwapPreparedQuote) => void;
onDisconnect?: () => void;
persistTokenSelections?: boolean;
prefill?: {
buyToken?: {
tokenAddress?: string;
chainId: number;
amount?: string;
};
sellToken?: {
tokenAddress?: string;
chainId: number;
amount?: string;
};
};
}
```
<Details summary="prefill">
You can prefill Buy and/or Sell tokens for the swap tab. If `tokenAddress` is not provided, the native token will be used. Here are some examples:
#### Set an ERC20 token as the buy token
```js
BridgeWidget.render(container, {
clientId: "your-thirdweb-client-id",
swap: {
prefill: {
// Base USDC
buyToken: {
chainId: 8453,
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
},
},
},
});
```
#### Set a native token as the sell token
```js
BridgeWidget.render(container, {
clientId: "your-thirdweb-client-id",
swap: {
prefill: {
// Base native token (ETH)
sellToken: {
chainId: 8453,
},
},
},
});
```
#### Set amount and token to Buy by default
```js
BridgeWidget.render(container, {
clientId: "your-thirdweb-client-id",
swap: {
prefill: {
buyToken: {
chainId: 8453,
amount: "0.1",
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
},
},
},
});
```
#### Set both buy and sell tokens by default
```ts
BridgeWidget.render(container, {
clientId: "your-thirdweb-client-id",
swap: {
prefill: {
// Base USDC
buyToken: {
chainId: 8453,
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
},
// Polygon native token (POL)
sellToken: {
chainId: 137,
},
},
},
});
```
</Details>
<Details summary="persistTokenSelections">
Whether to persist the token selections to localStorage so that if the user revisits the widget, the last used tokens are pre-selected.
The last used tokens do not override the tokens specified in the `prefill` option. By default, it is set to `true`.
</Details>
<Details summary="onCancel">
Pass a callback to be called when the user cancels the purchase.
</Details>
<Details summary="onError">
Pass a callback to be called when the user encounters an error during the purchase.
</Details>
<Details summary="onSuccess">
Pass a callback to be called when the user successfully completes the purchase.
</Details>
<Details summary="className">
Apply a custom class name to the "Buy" tab content container.
</Details>
<Details summary="onDisconnect">
Pass a callback to be called when the user disconnects the active wallet.
</Details>
### buy
The options to configure the "Buy" tab UI.
```ts
type Buy = {
amount: string; // Required
chainId: number; // Required
tokenAddress?: string;
buttonLabel?: string;
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
onError?: (
error: Error,
quote: BuyOrOnrampPrepareResult | undefined,
) => void;
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
className?: string;
country?: string;
presetOptions?: [number, number, number];
}
```
<Details summary="amount" tags={["Required"]}>
Set the default token amount shown in the "Buy" tab.
</Details>
<Details summary="chainId" tags={["Required"]}>
Specify which chain should 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.
</Details>
<Details summary="buttonLabel">
Override the label for main action button
</Details>
<Details summary="onCancel">
Pass a callback to be called when the user cancels the purchase.
</Details>
<Details summary="onError">
Pass a callback to be called when the user encounters an error during the purchase.
</Details>
<Details summary="onSuccess">
Pass a callback to be called when the user successfully completes the purchase.
</Details>
<Details summary="className">
Apply a custom class name to the "Buy" tab content container.
</Details>
<Details summary="country">
The user's ISO 3166 alpha-2 country code. This is used to determine onramp provider support.
</Details>
<Details summary="presetOptions">
Preset fiat amounts to display in the UI. Defaults to [5, 10, 20].
</Details>
### theme
Set the theme for the widget. You can either set it to `"dark"`, `"light"` or overrides some or all the colors by passing an object. By default it set to `"dark"`.
When overriding the colors, you must set the `type` to either `"dark"` or `"light"` so that the widget knows which theme to apply the overrides to.
```ts
type Theme = "dark" | "light" | {
type: "dark" | "light"; // required
accentButtonBg?: string;
accentButtonText?: string;
accentText?: string;
borderColor?: string;
connectedButtonBg?: string;
connectedButtonBgHover?: string;
danger?: string;
inputAutofillBg?: string;
modalBg?: string;
modalOverlayBg?: string;
primaryButtonBg?: string;
primaryButtonText?: string;
primaryText?: string;
scrollbarBg?: string;
secondaryButtonBg?: string;
secondaryButtonHoverBg?: string;
secondaryButtonText?: string;
secondaryIconColor?: string;
secondaryIconHoverBg?: string;
secondaryIconHoverColor?: string;
secondaryText?: string;
selectedTextBg?: string;
selectedTextColor: string;
separatorLine?: string;
skeletonBg: string;
success? : string;
tertiaryBg?: string;
tooltipBg?: string;
tooltipText?: string;
}
```
#### Example
```js
BridgeWidget.render(container, {
clientId: "your-thirdweb-client-id",
theme: {
type: "dark",
colors: {
modalBg: "black",
primaryText: "white",
},
}
});
```
### currency
The token amounts fiat values will be displayed in this currency. By default, it is set to `"USD"`.
```ts
type Currency = "USD" | "EUR" | "GBP" | "JPY" | "KRW" | "CNY" | "INR" | "NOK" | "SEK" | "CHF" | "AUD" | "CAD" | "NZD" | "MXN" | "BRL" | "CLP" | "CZK" | "DKK" | "HKD" | "HUF" | "IDR" | "ILS" | "ISK";
```
### showThirdwebBranding
Whether to show thirdweb branding at the bottom of the widget. By default, it is set to `true`.
+13 -17
View File
@@ -102,17 +102,17 @@ const preparedQuote = await Bridge.Buy.prepare({
</TabsContent>
<TabsContent value="react">
The quickest way to setup Bridge in your React app is with the [`BuyWidget`](/references/typescript/v5/widgets/BuyWidget) component.
The quickest way to setup Bridge in your React app is with the [`SwapWidget`](/references/typescript/v5/widgets/SwapWidget) component.
### Live Playground
<ArticleIconCard
title="Payment Widgets Playground"
description="Try out the Payment Widgets in our live playground"
title="Swap Widget Playground"
description="Try out the Swap Widget in our live playground"
icon={ReactIcon}
href="https://playground.thirdweb.com/connect/pay/transactions"
href="https://playground.thirdweb.com/bridge/swap-widget"
/>
### Installation
Install the thirdweb SDK in your React project:
@@ -127,7 +127,7 @@ const preparedQuote = await Bridge.Buy.prepare({
### Create a Client
First, create a client file (e.g., `thirdwebClient.ts`) for reuse throughout your app:
First, create a client file for reuse throughout your app:
```typescript
// thirdwebClient.ts
@@ -140,7 +140,7 @@ const preparedQuote = await Bridge.Buy.prepare({
### Setup the Provider
Wrap your application with the ThirdwebProvider:
Wrap your application with the `ThirdwebProvider`
```tsx
// app.tsx / _app.tsx
@@ -155,21 +155,17 @@ const preparedQuote = await Bridge.Buy.prepare({
}
```
### Buy Widget
### Add Swap Widget
Use the BuyWidget to let users purchase tokens:
Use the SwapWidget to let users swap tokens
```tsx
import { BuyWidget } from "thirdweb/react";
import { base } from "thirdweb/chains";
import { SwapWidget } from "thirdweb/react";
function PaymentComponent() {
function Example() {
return (
<BuyWidget
<SwapWidget
client={client}
chain={base}
amount="10" // 10 USDC
tokenAddress="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
/>
);
}
+4
View File
@@ -32,6 +32,10 @@ export const sidebar: SideBar = {
href: `${bridgeSlug}/routes`,
name: "Get Routes",
},
{
href: `${bridgeSlug}/bridge-widget-script`,
name: "BridgeWidget Script",
},
],
name: "Guides",
},
@@ -1,6 +1,6 @@
export default function Layout(props: { children: React.ReactNode }) {
return (
<main className="container max-w-3xl py-10" data-noindex>
<main className="container max-w-4xl py-10" data-noindex>
{props.children}
</main>
);
@@ -25,7 +25,7 @@ export default async function Page(props: {
return (
<div className="changelog-page">
<Button asChild>
<Button asChild variant="ghost">
<Link
className="-translate-x-1/4 !p-2 !text-muted-foreground hover:!text-foreground mb-4 bg-transparent"
href="/changelog"
@@ -64,7 +64,7 @@ export default async function Page(props: {
})}
</div>
<div className="mb-8 border-t-2" />
<div className="mb-8 border-t border-dashed" />
{ReactHtmlParser(data.html || "", {
transform,
@@ -2,15 +2,6 @@
display: none;
}
.changelog-page a {
text-decoration: underline;
text-decoration-color: hsl(var(--muted-foreground));
text-decoration-style: dotted;
color: hsl(var(--foreground));
text-underline-offset: 5px;
transition: color 200ms ease;
}
.changelog-page h2 a {
text-decoration: none;
}
@@ -6,13 +6,11 @@ export function Author(props: { name: string; profileImage?: string | null }) {
{props.profileImage && (
<img
alt=""
className="size-8 rounded-[50%] border"
className="size-6 rounded-[50%] border"
src={props.profileImage}
/>
)}
<span className="font-medium text-muted-foreground text-sm">
{props.name}
</span>
<span className="text-muted-foreground text-sm">{props.name}</span>
</div>
);
}
@@ -1,11 +0,0 @@
"use client";
import { TableOfContentsSideBar } from "../../../components/others/TableOfContents";
const onlyShowPostTitle = (h: HTMLHeadingElement) => {
return h.classList.contains("changelog-title");
};
export function ChangelogIndexTOC() {
return <TableOfContentsSideBar filterHeading={onlyShowPostTitle} />;
}
@@ -1,16 +1,10 @@
import { formatDistance } from "date-fns";
import { formatDate } from "date-fns";
import { cn } from "../../../lib/utils";
export function RenderDate(props: { iso: string; className?: string }) {
return (
<time
className={cn(
"font-medium text-muted-foreground text-sm",
props.className,
)}
dateTime={props.iso}
>
{formatDistance(new Date(props.iso), new Date())} ago
<time className={cn("text-sm", props.className)} dateTime={props.iso}>
{formatDate(new Date(props.iso), "MMM d, yyyy")}
</time>
);
}
+41 -46
View File
@@ -1,11 +1,8 @@
import { createMetadata } from "@doc";
import GithubSlugger from "github-slugger";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
import ReactHtmlParser from "react-html-parser";
import { Heading } from "@/components/Document";
import { Author } from "./components/Author";
import { ChangelogIndexTOC } from "./components/ChangeLogIndexTOC";
import { RenderDate } from "./components/RenderData";
import { fetchChangeLogs, fetchPost } from "./ghost";
import { transform } from "./utils/transform";
@@ -23,12 +20,15 @@ export const metadata = createMetadata({
export default async function Page() {
return (
<main className="container" data-noindex>
<div className="flex justify-between gap-16">
<div className="w-full max-w-[850px] grow-0 ">
<PageContent />
<main data-noindex>
<div className="border-b py-12 border-dashed">
<div className="container">
<h1 className="font-semibold text-4xl tracking-tight">Changelog</h1>
</div>
<ChangelogIndexTOC />
</div>
<div className="container">
<PageContent />
</div>
</main>
);
@@ -39,53 +39,48 @@ async function PageContent() {
const slugger = new GithubSlugger();
return (
<div className="changelog-page py-6">
<h1 className="mb-10 font-semibold text-3xl tracking-tighter">
Changelog
</h1>
<div className="flex flex-col gap-10 xl:border-l-2 xl:pl-12">
<div className="changelog-page overflow-hidden">
<div className="flex flex-col gap-10 xl:border-l border-dashed xl:pl-16 max-w-4xl ml-auto pb-20">
{posts.map((post) => {
return (
<div className="relative pb-10" key={post.id}>
<div className="mb-2 flex items-center gap-5">
<div className="relative pt-10" key={post.id}>
<div className="-left-16 -translate-x-full absolute top-12 hidden xl:flex gap-5 ml-3 items-center">
{post.published_at && (
<RenderDate className="text-base" iso={post.published_at} />
<div className="hidden xl:block text-sm text-muted-foreground">
<RenderDate iso={post.published_at} />
</div>
)}
<div className="flex gap-5">
{post.authors?.map((author) => {
return (
<Author
key={author.id}
name={author.name || ""}
profileImage={author.profile_image}
/>
);
})}
<div className="size-6 rounded-[50%] border bg-background flex items-center justify-center">
<div className="size-2 bg-blue-500 rounded-full" />
</div>
</div>
<div className="-left-12 -translate-x-1/2 absolute top-12 hidden size-7 items-center justify-center rounded-[50%] bg-foreground md:size-10 xl:flex">
<PlusIcon className="size-6 text-background" />
</div>
<div>
<Heading
anchorClassName="[&>a]:hidden m-0 border-b pb-5 mb-5"
anchorId={slugger.slug(post.title || "")}
// eslint-disable-next-line tailwindcss/no-custom-classname
className="changelog-title"
level={2}
>
<Link
className="!text-foreground font-bold text-3xl tracking-tighter hover:underline md:text-4xl xl:text-5xl"
href={`/changelog/${post.slug}`}
>
{post.title}
</Link>
</Heading>
{post.published_at && (
<div className="xl:hidden inline-flex rounded-full border px-3 py-2 mb-4 text-muted-foreground text-xs bg-card">
<RenderDate iso={post.published_at} />
</div>
)}
<RenderChangelogContent slug={post.slug} />
<div className="space-y-3 mb-5">
<Heading
anchorClassName="[&>a]:hidden m-0"
anchorId={slugger.slug(post.title || "")}
// eslint-disable-next-line tailwindcss/no-custom-classname
className="changelog-title"
level={2}
>
<Link
className="!text-foreground font-semibold text-3xl md:text-4xl tracking-tight hover:underline !leading-tight"
href={`/changelog/${post.slug}`}
>
{post.title}
</Link>
</Heading>
</div>
<div className="text-muted-foreground">
<RenderChangelogContent slug={post.slug} />
</div>
</div>
</div>
);
@@ -64,7 +64,7 @@ export const transform: Transform = (node, index: number) => {
node.attribs.class = "text-center text-sm text-muted-foreground";
return;
}
return <Paragraph>{getChildren()}</Paragraph>;
return <Paragraph className="leading-7">{getChildren()}</Paragraph>;
}
if (node.name === "ul") {
@@ -77,7 +77,7 @@ export const transform: Transform = (node, index: number) => {
}
node.attribs.class = "rounded-lg";
return (
<div className="my-5 flex justify-center rounded-lg border p-4">
<div className="my-6 flex justify-center rounded-lg border border-dashed p-2 bg-card/50">
{convertNodeToElement(node, index, transform)}
</div>
);
@@ -109,7 +109,13 @@ export const transform: Transform = (node, index: number) => {
// }
}
if (code) {
return <CodeBlock code={code} lang={lang} />;
return (
<CodeBlock
code={code}
lang={lang}
className="bg-card/50 border-dashed"
/>
);
}
}
@@ -0,0 +1,55 @@
import { Details, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "ThirdwebApi | Thirdweb .NET SDK",
description:
"Low-level interaction with the Thirdweb API for enhanced SDK functionality and extensions.",
});
# [ThirdwebApi](/reference) .NET Integration
`ThirdwebApi` is the low-level HTTP client that powers the higher-level Thirdweb .NET SDK. Reach for it when you need to call https://api.thirdweb.com directly, build custom extensions, or inspect raw API payloads. The client works everywhere .NET runs, including Unity and MAUI.
## What you unlock with ThirdwebApi
| Capability | Why teams use it |
| --- | --- |
| **Authentication** | Orchestrate SMS/email codes, SIWE, OAuth redirects, and passkeys from your own backend before handing off JWTs to wallets. |
| **Wallet services** | Look up user/server wallets, enumerate tokens/NFTs, stream transaction history, sign messages, or send value programmatically. |
| **Smart contracts** | Batch read/write invocations, fetch metadata & signatures, deploy bytecode, and capture emitted events with pagination. |
| **Transactions pipeline** | Queue, monitor, and introspect Engine-backed transactions with execution metadata and status tracking. |
| **Tokens & bridge** | Launch ERC20s, list circulating assets, price fiat ↔ crypto, or bridge/swap liquidity across chains. |
| **Payments & x402** | Spin up hosted purchases, reconcile payment history, and prepare X402 machine-payable requests. |
| **AI assistant** | Embed natural-language orchestration that can read state, prepare contract calls, swap assets, or route transactions for end-users.
> **Backend first** — All of the above accept your project secret via `x-secret-key`. Client-side use cases can opt into `x-client-id` + user JWT flows where supported.
## When to reach for ThirdwebApi
- You need endpoints that aren't yet wrapped by the higher-level SDK helpers.
- You're building backend services that must authenticate with a Thirdweb secret key.
- You want full visibility into contract metadata, ABI definitions, and compilation artifacts.
- You plan to orchestrate auth, payments, or cross-chain flows without managing disparate providers.
## Quickstart workflow
### Instantiate an authenticated client
Create a reusable `ThirdwebClient` instance with your backend secret key. This unlocks every raw API surface exposed by Thirdweb.
```csharp
// Program.cs / dependency container registration
var client = ThirdwebClient.Create(secretKey: Environment.GetEnvironmentVariable("THIRDWEB_SECRET_KEY"));
```
### Example operation
Contract metadata is the canonical example for low-level reads. The same pattern applies to other namespaces like `/v1/wallets/*`, `/v1/payments/*`, or `/ai/chat`—swap the method call but keep your authenticated client.
```csharp
// using Newtonsoft.Json;
var metadata = await client.Api.GetContractMetadataAsync(
chainId: 1,
address: "0xBd3531dA5CF5857e7CfAA92426877b022e612cf8"
);
Console.WriteLine($"ABI: {JsonConvert.SerializeObject(metadata.Result.Output.Abi, Formatting.Indented)}");
Console.WriteLine($"Compiler version: {metadata.Result.Compiler.Version}");
```
@@ -36,8 +36,4 @@ var calldata = contract.CreateCallData("myFunction", param1, param2);
## Available Contract Extensions
Please refer to the `ThirdwebExtensions` [full reference](https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.ThirdwebExtensions.html) for a complete list of available contract extensions.
If you are using our [Marketplace](https://thirdweb.com/thirdweb.eth/MarketplaceV3) contract, check out our [Marketplace-specific](https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.ThirdwebMarketplaceExtensions.html) extensions.
Please refer to the `ThirdwebExtensions` [full reference](https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.ThirdwebExtensions.html) for a complete list of available contract extensions.
@@ -1,51 +0,0 @@
import { Details, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "ThirdwebContract.Prepare | Thirdweb .NET SDK",
description:
"Instantiate a low level Transaction object from a contract prepare call.",
});
# ThirdwebContract.Prepare
Instantiate a low level [Transaction](/dotnet/transactions/create) object from a contract prepare call.
Useful for preparing, simulating and manipulating transactions before sending them to the blockchain.
## Usage
```csharp
ThirdwebTransaction transaction = await contract.Prepare(wallet, contract, "methodName", weiValue, parameters);
```
<Details summary="Parameters">
### wallet (required)
An instance of `IThirdwebWallet`. This represents the signer of the transaction, which can be any type of wallet provider.
### contract (required)
An instance of `ThirdwebContract`. Represents the smart contract you wish to interact with.
### methodName (required)
The name of the smart contract method you intend to call. Must be a `string`.
### weiValue (required)
The amount of Wei to send with payable transactions as a `BigInteger`. This is often set to 0 for methods that don't require sending Ether.
### parameters (optional)
The parameters to pass to the smart contract method, if any. Provide these as a comma-separated list after weiValue.
</Details>
<Details summary="Return Value">
### Transaction
Returns an instance of `ThirdwebTransaction` configured with the provided parameters, ready to be sent to the blockchain.
</Details>
@@ -76,16 +76,16 @@ Check the SDK documentation for more advanced features like interacting with sma
<Stack>
<ArticleIconCard
href="/unity"
icon={GraduationCap}
title="Unity Integration"
/>
<ArticleIconCard
href="/dotnet/godot"
icon={GraduationCap}
title="Godot Integration"
/>
<ArticleIconCard
href="/unity/v5"
icon={GraduationCap}
title="Unity Integration"
/>
</Stack>
+1 -1
View File
@@ -24,7 +24,7 @@ To add the Thirdweb .NET SDK to your Godot project, you will need to manually ed
```xml
<ItemGroup>
<!-- Other NuGet package references -->
<PackageReference Include="Thirdweb" Version="2.4.0" />
<PackageReference Include="Thirdweb" Version="3.0.0" />
<!-- Update with the latest version when necessary -->
</ItemGroup>
```
@@ -1,98 +0,0 @@
import { Details, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "ThirdwebInsight | Thirdweb .NET SDK",
description:
"Instantiate Insight to fetch all the blockchain data you can fathom.",
});
# [Insight](https://thirdweb.com/insight) Indexer .NET Integration
Insight is an extremely useful and performant tool to query blockchain data and can decorate it quite nicely too.
Did you know that vitalik, on Ethereum, Polygon and Arbitrum alone owns 7811 ERC20s, 34,362 ERC721s and 2,818 ERC1155s?
Let's go through some examples!
### Instantiation
```csharp
using Thirdweb.Indexer;
// Create a ThirdwebInsight instance
var insight = await ThirdwebInsight.Create(client);
```
### Simple Filters
```csharp
// Setup some filters
var address = await Utils.GetAddressFromENS(client, "vitalik.eth");
var chains = new BigInteger[] { 1, 137, 42161 };
```
### Fetching all tokens owned by a given address
```csharp
// Fetch all token types
var tokens = await insight.GetTokens(address, chains);
Console.WriteLine($"ERC20 Count: {tokens.erc20Tokens.Length} | ERC721 Count: {tokens.erc721Tokens.Length} | ERC1155 Count: {tokens.erc1155Tokens.Length}");
```
### Fetching a specific type of token owned by a given address
```csharp
// Fetch ERC20s only
var erc20Tokens = await insight.GetTokens_ERC20(address, chains);
Console.WriteLine($"ERC20 Tokens: {JsonConvert.SerializeObject(erc20Tokens, Formatting.Indented)}");
// Fetch ERC721s only
var erc721Tokens = await insight.GetTokens_ERC721(address, chains);
Console.WriteLine($"ERC721 Tokens: {JsonConvert.SerializeObject(erc721Tokens, Formatting.Indented)}");
// Fetch ERC1155s only
var erc1155Tokens = await insight.GetTokens_ERC1155(address, chains);
Console.WriteLine($"ERC1155 Tokens: {JsonConvert.SerializeObject(erc1155Tokens, Formatting.Indented)}");
```
### Fetching Events
Note: most of these parameters are optional (and some are not showcased here)
```csharp
// Fetch events (great amount of optional filters available)
var events = await insight.GetEvents(
chainIds: new BigInteger[] { 1 }, // ethereum
contractAddress: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", // bored apes
eventSignature: "Transfer(address,address,uint256)", // transfer event
fromTimestamp: Utils.GetUnixTimeStampNow() - 3600, // last hour
sortBy: SortBy.TransactionIndex, // block number, block timestamp or transaction index
sortOrder: SortOrder.Desc, // latest first
limit: 5 // last 5 transfers
);
Console.WriteLine($"Events: {JsonConvert.SerializeObject(events, Formatting.Indented)}");
```
### Fetching Transactions
```csharp
// Fetch transactions (great amount of optional filters available)
var transactions = await insight.GetTransactions(
chainIds: new BigInteger[] { 1 }, // ethereum
contractAddress: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", // bored apes
fromTimestamp: Utils.GetUnixTimeStampNow() - 3600, // last hour
sortBy: SortBy.TransactionIndex, // block number, block timestamp or transaction index
sortOrder: SortOrder.Desc, // latest first
limit: 5 // last 5 transactions
);
Console.WriteLine($"Transactions: {JsonConvert.SerializeObject(transactions, Formatting.Indented)}");
```
### Fetching NFTs with Metadata
Insight can return rich NFT Metadata. Extensions can turn Insight results into familiar NFT objects.
```csharp
// Fetch ERC721s with extra metadata returned
var erc721Tokens = await insight.GetTokens_ERC721(address, chains, withMetadata: true);
// Use ToNFT or ToNFTList extensions
var convertedNft = erc721Tokens[0].ToNFT();
var convertedNfts = erc721Tokens.ToNFTList();
// Use NFT Extensions (GetNFTImageBytes, or GetNFTSprite in Unity)
var imageBytes = await convertedNft.GetNFTImageBytes(client);
var pathToSave = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "nft.png");
await File.WriteAllBytesAsync(pathToSave, imageBytes);
Console.WriteLine($"NFT image saved to: {pathToSave}");
```
@@ -1,100 +0,0 @@
import { Details, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "ThirdwebNebula | Thirdweb .NET SDK",
description:
"Instantiate Nebula to interact with a blockchain-powered AI assistant.",
});
# [Nebula AI](https://thirdweb.com/nebula) .NET Integration
The last piece of the puzzle required to create .NET apps and games leveraging a blockchain-powered AI assistant or NPC that also has the power to fully execute transactions.
## Read, Write, Reason.
The best way to understand is to look at examples.
We'll prepare some context for the AI - here, we'll generate a basic `PrivateKeyWallet` and upgrade it to a `SmartWallet` on Sepolia.
```csharp
// Prepare some context
var myChain = 11155111; // Sepolia
var mySigner = await PrivateKeyWallet.Generate(client);
var myWallet = await SmartWallet.Create(mySigner, myChain);
var myContractAddress = "0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8"; // DropERC1155
var usdcAddress = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"; // Sepolia USDC
```
A one liner creates a Nebula session.
```csharp
// Create a Nebula session
var nebula = await ThirdwebNebula.Create(client);
```
You can **Chat** with Nebula. The Chat method accepts any `IThirdwebWallet` as an optional parameter, context will automatically be updated.
```csharp
// Chat, passing wallet context
var response1 = await nebula.Chat(message: "What is my wallet address?", wallet: myWallet);
Console.WriteLine($"Response 1: {response1.Message}");
```
You may also pass it smart contract context.
```csharp
// Chat, passing contract context
var response2 = await nebula.Chat(
message: "What's the total supply of token id 0 for this contract?",
context: new NebulaContext(contractAddresses: new List<string> { myContractAddress }, chainIds: new List<BigInteger> { myChain })
);
Console.WriteLine($"Response 2: {response2.Message}");
```
You can have a full on conversation with it with our **Chat** override accepting multiple messages.
```csharp
// Chat, passing multiple messages and context
var response3 = await nebula.Chat(
messages: new List<NebulaChatMessage>
{
new($"Tell me the name of this contract: {myContractAddress}", NebulaChatRole.User),
new("The name of the contract is CatDrop", NebulaChatRole.Assistant),
new("What's the symbol of this contract?", NebulaChatRole.User),
},
context: new NebulaContext(contractAddresses: new List<string> { myContractAddress }, chainIds: new List<BigInteger> { myChain })
);
Console.WriteLine($"Response 3: {response3.Message}");
```
Chatting is cool, but what if I just want it to _**do**_ things and stop talking so much?
You can **Execute** transactions directly with a simple prompt.
```csharp
// Execute, this directly sends transactions
var executionResult = await nebula.Execute("Approve 1 USDC to vitalik.eth", wallet: myWallet, context: new NebulaContext(contractAddresses: new List<string>() { usdcAddress }));
if (executionResult.TransactionReceipts != null && executionResult.TransactionReceipts.Count > 0)
{
Console.WriteLine($"Receipt: {executionResult.TransactionReceipts[0]}");
}
else
{
Console.WriteLine($"Message: {executionResult.Message}");
}
```
Similarly, **Execute** can take in multiple messages.
```csharp
// Batch execute
var batchExecutionResult = await nebula.Execute(
new List<NebulaChatMessage>
{
new("What's the address of vitalik.eth", NebulaChatRole.User),
new("The address of vitalik.eth is 0xd8dA6BF26964aF8E437eEa5e3616511D7G3a3298", NebulaChatRole.Assistant),
new("Approve 1 USDC to them", NebulaChatRole.User),
},
wallet: myWallet,
context: new NebulaContext(contractAddresses: new List<string>() { usdcAddress })
);
if (batchExecutionResult.TransactionReceipts != null && batchExecutionResult.TransactionReceipts.Count > 0)
{
Console.WriteLine($"Receipts: {JsonConvert.SerializeObject(batchExecutionResult.TransactionReceipts, Formatting.Indented)}");
}
else
{
Console.WriteLine($"Message: {batchExecutionResult.Message}");
}
```
+32 -85
View File
@@ -2,35 +2,6 @@ import { CodeIcon, ExternalLinkIcon, ZapIcon } from "lucide-react";
import type { SideBar } from "@/components/Layouts/DocLayout";
import type { SidebarLink } from "@/components/others/Sidebar";
const walletProviders: SidebarLink = (() => {
const parentSlug = "/dotnet/wallets/providers";
return {
links: [
{
href: `${parentSlug}/in-app-wallet`,
name: "In App Wallet",
},
{
href: `${parentSlug}/ecosystem-wallet`,
name: "Ecosystem Wallet",
},
{
href: `${parentSlug}/account-abstraction`,
name: "Account Abstraction",
},
{
href: `${parentSlug}/private-key`,
name: "Private Key Wallet",
},
{
href: `${parentSlug}/engine-wallet`,
name: "Engine Wallet",
},
],
name: "Wallet Providers",
};
})();
const walletActions: SidebarLink = (() => {
const parentSlug = "/dotnet/wallets/actions";
return {
@@ -179,15 +150,12 @@ const walletActions: SidebarLink = (() => {
})();
const contracts: SidebarLink = {
isCollapsible: false,
links: [
{
href: "/dotnet/contracts/create",
name: "Create Contract",
},
{
href: "/dotnet/contracts/extensions",
name: "Contract Extensions",
},
{
href: "/dotnet/contracts/read",
name: "Read Contract",
@@ -197,15 +165,20 @@ const contracts: SidebarLink = {
name: "Write Contract",
},
{
href: "/dotnet/contracts/prepare",
name: "Prepare Transaction",
href: "/dotnet/contracts/extensions",
name: "Contract Extensions",
},
],
name: "Contract Interactions",
name: "Contracts",
};
const transactions: SidebarLink = {
isCollapsible: false,
links: [
{
href: "/dotnet/transactions/prepare",
name: "Prepare Contract Transaction",
},
{
href: "/dotnet/transactions/create",
name: "Create Transaction",
@@ -219,7 +192,7 @@ const transactions: SidebarLink = {
name: "Static Methods",
},
],
name: "Transaction Builder",
name: "Transactions",
};
export const sidebar: SideBar = {
@@ -245,7 +218,7 @@ export const sidebar: SideBar = {
isCollapsible: true,
links: [
{
href: "/unity/v5",
href: "/unity",
name: "Unity",
},
{
@@ -285,67 +258,41 @@ export const sidebar: SideBar = {
},
{
isCollapsible: false,
links: [walletProviders, walletActions],
links: [
{
href: `/dotnet/wallets/user-wallet`,
name: "User Wallet",
},
{
href: `/dotnet/wallets/server-wallet`,
name: "Server Wallet",
},
{
href: `/dotnet/wallets/account-abstraction`,
name: "Account Abstraction",
},
walletActions,
],
name: "Wallets",
},
{
separator: true,
},
{
isCollapsible: false,
links: [contracts, transactions],
name: "Transactions",
},
{
separator: true,
},
contracts,
transactions,
{
isCollapsible: false,
links: [
{
href: "/dotnet/universal-bridge/quickstart",
href: "/dotnet/api/quickstart",
name: "Quickstart",
},
{
href: "https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.Bridge.ThirdwebBridge.html",
name: "Full Reference",
href: "/reference",
name: "API Full Reference",
},
],
name: "Payments",
},
{
separator: true,
},
{
isCollapsible: false,
links: [
{
href: "/dotnet/insight/quickstart",
name: "Quickstart",
},
{
href: "https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.Indexer.ThirdwebInsight.html",
name: "Insight Full Reference",
},
],
name: "Indexer",
},
{
separator: true,
},
{
isCollapsible: false,
links: [
{
href: "/dotnet/nebula/quickstart",
name: "Quickstart",
},
{
href: "https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.AI.ThirdwebNebula.html",
name: "Nebula Full Reference",
},
],
name: "AI",
name: "API",
},
{
separator: true,
@@ -8,9 +8,10 @@ export const metadata = createMetadata({
# ThirdwebTransaction.Create
<Callout variant="warning" title="Advanced Use Case">
If you are interacting with a contract, it is best to use
[ThirdwebContract.Prepare](/dotnet/contracts/prepare) to create a transaction.
<Callout variant="warning" title="Preferred Workflow">
When you're starting from a contract method, use
[ThirdwebContract.Prepare](/dotnet/transactions/prepare) to build the transaction
before customizing it here.
</Callout>
The `ThirdwebTransaction.Create` method allows advanced users to manually create transactions using specific wallet credentials and transaction inputs. This method is ideal for users who need precise control over the transaction details.
@@ -0,0 +1,83 @@
import { Details, Callout, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "ThirdwebContract.Prepare | Thirdweb .NET SDK",
description:
"Generate a ThirdwebTransaction from a contract call before customizing or sending it.",
});
# ThirdwebContract.Prepare
`ThirdwebContract.Prepare` bridges contract interactions and the transaction tooling within the Thirdweb .NET SDK. Use it when you want to start from a contract method, but need fine-grained control over the resulting `ThirdwebTransaction` before dispatching it.
<Callout type="info">
Looking for the contract APIs themselves? Check out the [Contracts guides](/dotnet/contracts/create) for creating, reading, and writing before you promote a call into a transaction.
</Callout>
## Why prepare first?
- **Simulation & inspection** – Prepare the transaction and run [`ThirdwebTransaction.Simulate`](/dotnet/transactions/static) before sending.
- **Customization** – Chain the [`ThirdwebTransaction` instance methods](/dotnet/transactions/instance) to adjust calldata, value, gas, or paymaster options.
- **Wallet-agnostic** – Works with any `IThirdwebWallet`, letting you bridge user wallets, smart wallets, or server wallets into the same flow.
## Usage
```csharp
ThirdwebTransaction transaction = await contract.Prepare(
wallet,
contract,
"methodName",
weiValue,
parameters
);
```
Once prepared, you can tweak or send the transaction:
```csharp
// Optional: override calldata, value, or other fields
transaction = transaction
.SetValue(BigInteger.Zero)
.SetGasLimit(250000);
// Optional: simulate or estimate costs
await ThirdwebTransaction.Simulate(transaction);
var totalCosts = await ThirdwebTransaction.EstimateTotalCosts(transaction);
// Finally: send the transaction and wait for the receipt
TransactionReceipt receipt = await ThirdwebTransaction.SendAndWaitForTransactionReceipt(transaction);
```
## Parameters
<Details summary="Prepare parameters">
### wallet (required)
An instance of `IThirdwebWallet`. This signer provides the context for populating sender-specific defaults and will ultimately authorize the transaction.
### contract (required)
An instance of `ThirdwebContract`. Represents the smart contract whose method you're calling.
### methodName (required)
The name of the contract method to invoke. Must be a `string`.
### weiValue (required)
The amount of Wei to send with the transaction, as a `BigInteger`. Use `BigInteger.Zero` when no value transfer is needed.
### parameters (optional)
The arguments to forward to the contract method. Provide them in the same order as the ABI definition.
</Details>
## Return value
<Details summary="ThirdwebTransaction">
Returns a fully populated `ThirdwebTransaction` that mirrors the contract invocation. You can send it immediately or pipe it through additional configuration helpers before execution.
</Details>
@@ -1,171 +0,0 @@
import { Details, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "Payments | Thirdweb .NET SDK",
description:
"Enable your users to trade assets across any supported chain using any token.",
});
# Payments .NET Integration
Payments enables your users to trade assets across any supported chain using any token.
To streamline onchain asset trading, we've added .NET extensions that work seamlessly with any `IThirdwebWallet` nicely.
## Core APIs
The design is akin to letting us know what your intent is.
- Buy: "I want to buy x USDC on y Chain using z Token"
- Sell: "I want to sell x USDC on y Chain for z Token"
- Transfer: "Just transfer all my money to vitalik"
- Onramp: "I want to buy x USDC on y Chain by paying with card"
We will return the transactions needed to achieve whatever you desire.
You may then handle execution yourself or use our extensions.
### Instantiation
```csharp
using Thirdweb.Bridge;
// Create a ThirdwebBridge instance
var bridge = await ThirdwebBridge.Create(client);
```
### Buy - Get a quote for buying a specific amount of tokens
```csharp
var buyQuote = await bridge.Buy_Quote(
originChainId: 1,
originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
destinationChainId: 324,
destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync
buyAmountWei: BigInteger.Parse("0.1".ToWei())
);
Console.WriteLine($"Buy quote: {JsonConvert.SerializeObject(buyQuote, Formatting.Indented)}");
```
### Buy - Get an executable set of transactions (alongside a quote) for buying a specific amount of tokens
```csharp
var preparedBuy = await bridge.Buy_Prepare(
originChainId: 1,
originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
destinationChainId: 324,
destinationTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync
buyAmountWei: BigInteger.Parse("0.1".ToWei()),
sender: await Utils.GetAddressFromENS(client, "vitalik.eth"),
receiver: await myWallet.GetAddress()
);
Console.WriteLine($"Prepared Buy contains {preparedBuy.Transactions.Count} transaction(s)!");
```
### Sell - Get a quote for selling a specific amount of tokens
```csharp
var sellQuote = await bridge.Sell_Quote(
originChainId: 324,
originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync
destinationChainId: 1,
destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
sellAmountWei: BigInteger.Parse("0.1".ToWei())
);
Console.WriteLine($"Sell quote: {JsonConvert.SerializeObject(sellQuote, Formatting.Indented)}");
```
### Sell - Get an executable set of transactions (alongside a quote) for selling a specific amount of tokens
```csharp
var preparedSell = await bridge.Sell_Prepare(
originChainId: 324,
originTokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync
destinationChainId: 1,
destinationTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
sellAmountWei: BigInteger.Parse("0.1".ToWei()),
sender: await Utils.GetAddressFromENS(client, "vitalik.eth"),
receiver: await myWallet.GetAddress()
);
Console.WriteLine($"Prepared Sell contains {preparedSell.Transactions.Count} transaction(s)!");
```
### Transfer - Get an executable transaction for transferring a specific amount of tokens
_Why not just transfer with the SDK? Stay tuned for webhooks, think direct payments!_
```csharp
var preparedTransfer = await bridge.Transfer_Prepare(
chainId: 137,
tokenAddress: Constants.NATIVE_TOKEN_ADDRESS, // ETH on zkSync
transferAmountWei: BigInteger.Parse("0.1".ToWei()),
sender: await Utils.GetAddressFromENS(client, "vitalik.eth"),
receiver: await myWallet.GetAddress()
);
Console.WriteLine($"Prepared Transfer: {JsonConvert.SerializeObject(preparedTransfer, Formatting.Indented)}");
```
## Manual Execution
_This is not production code, we're just showcasing some of the APIs that would help you execute and poll status here._
```csharp
// You may use our extensions to execute yourself...
var myTx = await preparedTransfer.Transactions[0].ToThirdwebTransaction(myWallet);
var myHash = await ThirdwebTransaction.Send(myTx);
// ...and poll for the status...
var status = await bridge.Status(transactionHash: myHash, chainId: 1);
var isComplete = status.StatusType == StatusType.COMPLETED;
Console.WriteLine($"Status: {JsonConvert.SerializeObject(status, Formatting.Indented)}");
// Or use our Execute extensions directly to handle everything for you!
```
## Managed Execution
The SDK comes with some extensions that simplify the use of `ThirdwebBridge`.
```csharp
// Execute a prepared Buy
var buyResult = await bridge.Execute(myWallet, preparedBuy);
var buyHashes = buyResult.Select(receipt => receipt.TransactionHash).ToList();
Console.WriteLine($"Buy hashes: {JsonConvert.SerializeObject(buyHashes, Formatting.Indented)}");
// Execute a prepared Sell
var sellResult = await bridge.Execute(myWallet, preparedSell);
var sellHashes = sellResult.Select(receipt => receipt.TransactionHash).ToList();
Console.WriteLine($"Sell hashes: {JsonConvert.SerializeObject(sellHashes, Formatting.Indented)}");
// Execute a prepared Transfer
var transferResult = await bridge.Execute(myWallet, preparedTransfer);
var transferHashes = transferResult.Select(receipt => receipt.TransactionHash).ToList();
Console.WriteLine($"Transfer hashes: {JsonConvert.SerializeObject(transferHashes, Formatting.Indented)}");
```
## Onramping with Fiat
The onramp flow will return a link for you to display/open as you please. You may poll the status of that onramp by its ID.
In some cases, you may receive an additional set of onchain steps required to get to your destination token post on-ramp, in such cases, you may use our extension `IsSwapRequiredPostOnramp` to check, and if a swap is indeed required, you may use our `Execute` extensions to execute the transactions, or manually execute them by going through each `Step`.
```csharp
// Onramp - Get a quote for buying crypto with Fiat
var preparedOnramp = await bridge.Onramp_Prepare(
onramp: OnrampProvider.Coinbase,
chainId: 8453,
tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
amount: "10000000",
receiver: await myWallet.GetAddress()
);
Console.WriteLine($"Onramp link: {preparedOnramp.Link}");
Console.WriteLine($"Full onramp quote and steps data: {JsonConvert.SerializeObject(preparedOnramp, Formatting.Indented)}");
while (true)
{
var onrampStatus = await bridge.Onramp_Status(id: preparedOnramp.Id);
Console.WriteLine($"Full Onramp Status: {JsonConvert.SerializeObject(onrampStatus, Formatting.Indented)}");
if (onrampStatus.StatusType is StatusType.COMPLETED or StatusType.FAILED)
{
break;
}
await ThirdwebTask.Delay(5000);
}
if (preparedOnramp.IsSwapRequiredPostOnramp())
{
// Execute additional steps that are required post-onramp to get to your token, manually or via the Execute extension
var receipts = await bridge.Execute(myWallet, preparedOnramp);
Console.WriteLine($"Onramp receipts: {JsonConvert.SerializeObject(receipts, Formatting.Indented)}");
}
else
{
Console.WriteLine("No additional steps required post-onramp, you can use the tokens directly!");
}
```
+243 -467
View File
@@ -1,534 +1,310 @@
import { Details, createMetadata } from "@doc";
import { Callout, Details, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "Common Utils | Thirdweb .NET SDK",
description: "Useful utilities offered by thirdweb.",
title: "Common Utils | Thirdweb .NET SDK",
description: "Frequently used helpers for working with the Thirdweb .NET SDK.",
});
# Utils
A collection of common utility functions that are useful for interacting with the Thirdweb .NET SDK or the blockchain in general. Full list available [here](https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.Utils.html).
`Utils` bundles the SDK's most handy helpers for formatting values, working with addresses, and preparing typed data. If you need an API not covered here, check the [full reference](https://thirdweb-dev.github.io/dotnet/docs/Thirdweb.Utils.html).
## Constants.ADDRESS_ZERO
A `string` representing the zero address `0x0000000000000000000000000000000000000000`.
Useful for passing the zero address to various functions.
## Constants.NATIVE_TOKEN_ADDRESS
A `string` representing the native token address `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`.
Useful for passing the native token address (ETH or equivalent) to various functions.
## Constants.ENTRYPOINT_ADDRESS_V06
A `string` representing the entrypoint address for Account Abstraction 0.7.0 `0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789`.
## Constants.ENTRYPOINT_ADDRESS_V07
A `string` representing the entrypoint address for Account Abstraction 0.7.0 `0x0000000071727De22E5E9d8BAf0edAc6f37da032`.
## GetSocialProfiles
Fetches the social profiles for the given address or ENS using a Thirdweb client.
## Useful constants
```csharp
var socialProfiles = await Utils.GetSocialProfiles(client, addressOrEns);
Constants.ADDRESS_ZERO // 0x0000…0000 (zero address)
Constants.NATIVE_TOKEN_ADDRESS // 0xEeee…EEeE (native token sentinel)
Constants.DECIMALS_18 // 1e18 as a double (decimal scaling helper)
Constants.IERC20_INTERFACE_ID // ERC-165 ID for ERC-20
Constants.IERC721_INTERFACE_ID // ERC-165 ID for ERC-721
Constants.IERC1155_INTERFACE_ID // ERC-165 ID for ERC-1155
Constants.ENTRYPOINT_ADDRESS_V06 // Default ERC-4337 entry point v0.6
Constants.ENTRYPOINT_ADDRESS_V07 // Default ERC-4337 entry point v0.7
Constants.DEFAULT_FACTORY_ADDRESS_V06 // Default Thirdweb factory v0.6
Constants.DEFAULT_FACTORY_ADDRESS_V07 // Default Thirdweb factory v0.7
Constants.EIP_1271_MAGIC_VALUE // Expected return value for EIP-1271 signature validation
Constants.ERC_6492_MAGIC_VALUE // Magic value appended to ERC-6492 signatures
```
## Chain helpers
### Utils.GetChainMetadata
Fetch metadata (native token, RPCs, explorers, stack type, etc.) for a given chain ID. Results are cached in-memory.
```csharp
ThirdwebChainData data = await Utils.GetChainMetadata(client, chainId);
```
<Details summary="Parameters">
### client
A `ThirdwebClient` object representing the client to use for fetching social profiles.
### addressOrEns
A `string` representing the wallet address or ENS to fetch social profiles for.
</Details>
<Details summary="Return Value">
### socialProfiles
A `SocialProfiles` object containing the fetched social profiles.
</Details>
## GetENSFromAddress
Fetches the ENS name for the given address using a Thirdweb client.
```csharp
var ensName = await Utils.GetENSFromAddress(client, address);
```
<Details summary="Parameters">
### client
A `ThirdwebClient` object representing the client to use for fetching the ENS name.
### address
A `string` representing the wallet address to fetch the ENS name for.
</Details>
<Details summary="Return Value">
### ensName
A `string` representing the fetched ENS name.
</Details>
## GetAddressFromENS
Fetches the address for the given ENS name using a Thirdweb client.
```csharp
var address = await Utils.GetAddressFromENS(client, ensName);
```
<Details summary="Parameters">
### client
A `ThirdwebClient` object representing the client to use for fetching the address.
### ensName
A `string` representing the ENS name to fetch the address for.
</Details>
<Details summary="Return Value">
### address
A `string` representing the fetched address.
</Details>
## GetChainMetadata
Fetches chain data for a specified chain ID using a Thirdweb client.
```csharp
var chainData = await Utils.GetChainMetadata (client, chainId);
```
<Details summary="Parameters">
### client
A `ThirdwebClient` object representing the client to use for fetching chain data.
An initialized `ThirdwebClient`.
### chainId
A `BigInteger` representing the chain ID to fetch data for.
`BigInteger` chain identifier.
</Details>
<Details summary="Return Value">
<Details summary="Return value">
### chainData
A `ThirdwebChainData` object containing the fetched chain data such as native token name, symbol, decimals, etc.
`ThirdwebChainData` describing the chain.
</Details>
## HexConcat
### Utils.IsZkSync
Concatenates the given hex strings into a single hex string.
Quickly check if a chain uses zkSync infrastructure. Falls back to chain metadata when the ID is not one of the known zkSync networks.
```csharp
var concatenatedHex = Utils.HexConcat("0x1234", "0x5678");
bool isZkSync = await Utils.IsZkSync(client, chainId);
```
<Details summary="Parameters">
### hexStrings
### client
An array of hexadecimal `string` values to concatenate.
</Details>
<Details summary="Return Value">
### concatenatedHex
The concatenated hexadecimal `string`.
</Details>
## HashPrefixedMessage
Hashes the given message bytes with a prefixed message for Ethereum signing.
```csharp
var hashedMessage = messageBytes.HashPrefixedMessage();
```
<Details summary="Parameters">
### messageBytes
A `byte[]` representing the message to hash.
</Details>
<Details summary="Return Value">
### hashedMessage
A `byte[]` representing the hashed message.
</Details>
## HashMessage
Hashes the given message using SHA3 Keccak hashing.
```csharp
var hashedMessage = message.HashMessage();
```
<Details summary="Parameters">
### message
A `string` representing the message to hash.
</Details>
<Details summary="Return Value">
### hashedMessage
A `string` representing the hashed message in hexadecimal format.
</Details>
## BytesToHex
Converts a byte array to a hexadecimal string.
```csharp
var hexString = byteArray.BytesToHex();
```
<Details summary="Parameters">
### bytes
A `byte[]` representing the byte array to convert.
</Details>
<Details summary="Return Value">
### hexString
A `string` representing the converted hexadecimal string.
</Details>
## HexToBytes
Converts a hexadecimal string to a byte array.
```csharp
var byteArray = hexString.HexToBytes();
```
<Details summary="Parameters">
### hex
A `string` representing the hexadecimal string to convert.
</Details>
<Details summary="Return Value">
### byteArray
A `byte[]` representing the converted byte array.
</Details>
## StringToHex
Converts a regular string to a hexadecimal string.
```csharp
var hexString = "Hello".StringToHex();
```
<Details summary="Parameters">
### str
A `string` representing the regular string to convert.
</Details>
<Details summary="Return Value">
### hexString
A `string` representing the converted hexadecimal string.
</Details>
## GetUnixTimeStampNow
Gets the current Unix timestamp in seconds.
```csharp
var timestampNow = Utils.GetUnixTimeStampNow();
```
<Details summary="Return Value">
### timestampNow
A `long` representing the current Unix timestamp.
</Details>
## GetUnixTimeStampIn10Years
Gets the Unix timestamp for 10 years from now.
```csharp
var timestampIn10Years = Utils.GetUnixTimeStampIn10Years();
```
<Details summary="Return Value">
### timestampIn10Years
A `long` representing the Unix timestamp 10 years from now.
</Details>
## ReplaceIPFS
Replaces an IPFS URI with a specified gateway.
```csharp
var gatewayUri = ipfsUri.ReplaceIPFS("https://ipfs.io/ipfs/");
```
<Details summary="Parameters">
### uri
A `string` representing the IPFS URI to replace.
### gateway
A `string` representing the gateway to use. Default is `Constants.FALLBACK_IPFS_GATEWAY`.
</Details>
<Details summary="Return Value">
### gatewayUri
A `string` representing the replaced URI with the specified gateway.
</Details>
## ToWei
Converts Ether values to Wei.
```csharp
var weiValue = ethValue.ToWei();
```
<Details summary="Parameters">
### eth
A `string` representing the Ether value to convert.
</Details>
<Details summary="Return Value">
### weiValue
A `string` representing the converted Wei value.
</Details>
## ToEth
Converts Wei values to Ether.
```csharp
var ethValue = weiValue.ToEth();
```
<Details summary="Parameters">
### wei
A `string` representing the Wei value to convert.
### decimalsToDisplay
An `int` specifying the number of decimals to display. Default is `4`.
### addCommas
A `bool` indicating whether to add commas to the output. Default is `false`.
</Details>
<Details summary="Return Value">
### ethValue
A `string` representing the converted Ether value.
</Details>
## GenerateSIWE
Generates a Sign-In With Ethereum (SIWE) message.
```csharp
var siweMessage = Utils.GenerateSIWE(loginPayloadData);
```
<Details summary="Parameters">
### loginPayloadData
A `LoginPayloadData` object containing the payload data for generating the SIWE message.
</Details>
<Details summary="Return Value">
### siweMessage
A `string` representing the generated SIWE message.
</Details>
## IsZkSync
Checks if a given chain ID corresponds to zkSync.
```csharp
var isZkSync = Utils.IsZkSync(chainId);
```
<Details summary="Parameters">
`ThirdwebClient` used for metadata lookups.
### chainId
A `BigInteger` representing the chain ID to check.
`BigInteger` chain identifier.
</Details>
<Details summary="Return Value">
<Details summary="Return value">
### isZkSync
A `bool` indicating whether the chain ID corresponds to zkSync.
`true` if the chain is zkSync-based.
</Details>
## ToChecksumAddress
## Address & identity helpers
Converts an Ethereum address to its checksum format.
### Utils.ToChecksumAddress
Normalize any hex address to its EIP-55 checksum representation.
```csharp
var checksummedAddress = address.ToChecksumAddress();
string checksum = address.ToChecksumAddress();
```
<Details summary="Parameters">
### Utils.IsValidAddress
### address
A `string` representing the Ethereum address to convert.
</Details>
<Details summary="Return Value">
### checksummedAddress
A `string` representing the checksummed Ethereum address.
</Details>
## AdjustDecimals
Adjusts the decimal places of a value.
Lightweight validation for EVM addresses.
```csharp
var adjustedValue = value.AdjustDecimals(18, 6);
if (!input.IsValidAddress()) {
throw new ArgumentException("Expected an EVM address");
}
```
<Details summary="Parameters">
### Utils.GetENSFromAddress / Utils.GetAddressFromENS
### value
A `BigInteger` representing the value to adjust.
### fromDecimals
An `int` specifying the original number of decimals.
### toDecimals
An `int` specifying the target number of decimals.
</Details>
<Details summary="Return Value">
### adjustedValue
A `BigInteger` representing the value adjusted to the new decimals.
</Details>
## ToJsonExternalWalletFriendly
Converts typed data into a JSON string suitable for external wallet interaction.
Resolve between ENS names and addresses. Lookups are cached per process.
```csharp
var jsonString = Utils.ToJsonExternalWalletFriendly(typedData, message);
string ens = await Utils.GetENSFromAddress(client, address);
string addr = await Utils.GetAddressFromENS(client, ensName);
```
<Details summary="Parameters">
### Utils.GetSocialProfiles
### typedData
Pull public social metadata (Lens, Farcaster, etc.) for an address or ENS.
A `TypedData<TDomain>` object representing the typed data to convert.
```csharp
SocialProfiles profiles = await Utils.GetSocialProfiles(client, "vitalik.eth");
```
### message
## Encoding & hashing
A `TMessage` object representing the message to include in the JSON.
### Utils.HexConcat
</Details>
Join multiple hex strings (with `0x` prefixes) into a single blob.
<Details summary="Return Value">
```csharp
string callData = Utils.HexConcat("0x1234", "0x5566");
```
### jsonString
### BytesToHex / HexToBytes
A `string` representing the JSON formatted data.
Extension helpers for toggling between byte arrays and hex.
```csharp
byte[] bytes = hex.HexToBytes();
string hexAgain = bytes.BytesToHex();
```
### HexToNumber / NumberToHex
Convert between hex strings and `BigInteger` values.
```csharp
BigInteger gas = "0x5208".HexToNumber();
string hexValue = gas.NumberToHex();
```
### StringToHex / HexToString
Encode or decode UTF-8 strings as hex.
```csharp
string payload = "Hello".StringToHex();
string message = payload.HexToString();
```
### HashPrefixedMessage & HashMessage
Generate Ethereum-style prefixed hashes or raw Keccak hashes.
```csharp
byte[] prefixed = messageBytes.HashPrefixedMessage();
string keccak = message.HashMessage();
```
### Utils.HexToBytes32
Left-pad a hex string so it fits into exactly 32 bytes.
```csharp
byte[] padded = someHex.HexToBytes32();
```
### Utils.TrimZeroes
Strip leading zero bytes—handy before ABI encoding.
```csharp
byte[] trimmed = signature.TrimZeroes();
```
### Utils.PreprocessTypedDataJson
Stringify large integers inside a typed-data payload before sending it to browsers or wallets.
```csharp
string normalizedJson = Utils.PreprocessTypedDataJson(rawJson);
```
### Utils.ToJsonExternalWalletFriendly
Convert strongly typed EIP-712 data structures into wallet-ready JSON.
```csharp
string json = Utils.ToJsonExternalWalletFriendly(typedData, message);
```
### Utils.SerializeErc6492Signature
Wrap signatures for ERC-6492 (counterfactual ERC-4337 accounts) with the required magic value.
```csharp
string wrapped = Utils.SerializeErc6492Signature(factoryAddress, deploymentCalldata, signatureBytes);
```
## Value & time helpers
### Utils.ToWei / Utils.ToEth / Utils.FormatERC20
Convert between human-readable token amounts and Wei.
```csharp
string wei = "0.05".ToWei();
string eth = wei.ToEth(decimalsToDisplay: 6, addCommas: true);
string usdc = Utils.FormatERC20(usdcRawValue, decimalsToDisplay: 2, decimals: 6);
```
### Utils.AdjustDecimals
Scale a `BigInteger` between two decimal systems—useful when toggling between token decimals.
```csharp
BigInteger stableValue = amount.AdjustDecimals(18, 6);
```
### Utils.GenerateSIWE
Assemble a Sign-In With Ethereum message from a `LoginPayloadData` object. Throws if required fields are missing to help surface misconfigurations early.
```csharp
string messageToSign = Utils.GenerateSIWE(payload);
```
### Utils.GetUnixTimeStampNow / Utils.GetUnixTimeStampIn10Years
Timestamp helpers for authentication windows or scheduled expirations.
```csharp
long now = Utils.GetUnixTimeStampNow();
long farFuture = Utils.GetUnixTimeStampIn10Years();
```
### Utils.ReplaceIPFS
Swap `ipfs://` URIs for a gateway of your choice (defaults to the Thirdweb fallback gateway).
```csharp
string httpUrl = ipfsUri.ReplaceIPFS("https://w3s.link/ipfs/");
```
## Gas & network costs
### Utils.FetchGasPrice / Utils.FetchGasFees
Retrieve legacy gas prices or EIP-1559 fee suggestions with optional safety bumps. Polygon, Celo, and Arbitrum chains get tailor-made logic.
```csharp
BigInteger gasPrice = await Utils.FetchGasPrice(client, chainId);
(BigInteger maxFee, BigInteger maxPriority) = await Utils.FetchGasFees(client, chainId);
```
### Utils.IsEip155Enforced / Utils.IsEip1559Supported
Runtime checks that tell you whether a chain requires EIP-155 signatures and whether it supports EIP-1559 style fees.
```csharp
bool needsProtectedTx = await Utils.IsEip155Enforced(client, chainId);
bool supports1559 = Utils.IsEip1559Supported(chainId.ToString());
```
## Transactions & monitoring
### Utils.WaitForTransactionReceipt
Poll for a transaction receipt with built-in timeouts from the client's fetch options.
```csharp
var receipt = await Utils.WaitForTransactionReceipt(client, chainId, txHash);
```
### Utils.IsDeployed
Check whether bytecode exists at an address.
```csharp
bool deployed = await Utils.IsDeployed(client, chainId, contractAddress);
```
### Utils.IsDelegatedAccount
Detect delegated accounts (EIP-7702 style) by comparing on-chain code against the delegation contract.
```csharp
bool delegated = await Utils.IsDelegatedAccount(client, chainId, address, delegationContract);
```
## IP & social enrichment
### Utils.PacketToBytes
Encode ENS-style DNS packets. Exposed for advanced ENS tooling; most integrations can rely on the higher-level ENS helpers above.
```csharp
byte[] packet = Utils.PacketToBytes("subdomain.example.eth");
```
</Details>
@@ -1,351 +0,0 @@
import { Details, Callout, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "EcosystemWallet.Create | Thirdweb .NET SDK",
description:
"Instantiate an EcosystemWallet for secure user authentication and interaction.",
});
# EcosystemWallet.Create
Create an instance of `EcosystemWallet` using a user's email, phone number or OAuth. This wallet type facilitates secure user authentication through OTP verification, making it suitable for client-facing applications where handling private keys directly is not ideal.
## Login Methods
Ecosystem Wallets support a variety of login methods:
- Email (OTP Login)
- Phone (OTP Login)
- Socials (Google, Apple, Facebook, Telegram, Farcaster, Line, Github, Twitch, Steam, TikTok etc.)
- Custom Auth (OIDC Compatible)
- Custom Auth (Generic Auth Endpoint)
- Guest (Onboard easily, link other accounts later)
- Backend (Server Wallets)
- Siwe (Login with a separate wallet supported by the SDK)
- SiweExternal (Login with an external wallet that only supports web using a browser loading a static thirdweb React page temporarily)
## Usage
```csharp
// NOTE: All creation examples below may take in an `ecosystemPartnerId` if you are the ecosystem partner integrating with a third-party ecosystem
// Email
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", email: "userEmail");
// Phone
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", phoneNumber: "+1234567890");
// Google, Apple, Facebook, etc.
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Google);
// Custom Auth - JWT
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.JWT);
// Custom Auth - AuthEndpoint
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.AuthEndpoint);
// Guest Login
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Guest);
// Server Login
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Backend, walletSecret: "very-secret");
// Siwe
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Siwe, siweSigner: anyExternalWallet);
// SiweExternal
var wallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.SiweExternal);
// Session resuming supported for all methods
var isConnected = await wallet.IsConnected();
// If not connected, initiate login flow based on the auth provider you are using
// Email & Phone (OTP)
await wallet.SendOTP(); // and fetch the otp
var address = await wallet.LoginWithOtp("userEnteredOTP"); // try catch and retry if needed
// Socials (OAuth)
var address = await wallet.LoginWithOauth(
// Windows console app example, adaptable to any runtime
isMobile: false,
browserOpenAction: (url) =>
{
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
_ = Process.Start(psi);
},
mobileRedirectScheme: "myBundleId://"
);
// Custom Auth (JWT)
var address = await wallet.LoginWithCustomAuth(jwt: "myjwt");
// Custom Auth (AuthEndpoint)
var address = await wallet.LoginWithAuthEndpoint(payload: "mypayload");
// Guest Login (Easy onboarding)
var address = await wallet.LoginWithGuest();
// Backend (Server Wallets)
var address = await wallet.LoginWithBackend();
// SIWE (Wallet)
var address = await siweWallet.LoginWithSiwe(chainId: 1);
// SiweExternal (React-only wallet)
var address = await wallet.LoginWithSiweExternal(
// Windows console app example, adaptable to any runtime
isMobile: false,
browserOpenAction: (url) =>
{
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
_ = Process.Start(psi);
},
forceWalletIds: new List<string> { "io.metamask", "com.coinbase.wallet", "xyz.abs" }
);
```
<Details summary="Parameters">
### client (required)
An instance of `ThirdwebClient`.
### ecosystemId (required)
The ID of the ecosystem wallet you created on the dashboard.
### ecosystemPartnerId (optional)
The partner ID you were provided by the Ecosystem owner.
### email (optional)
The user's email address. Required if `phoneNumber` is not provided.
### phoneNumber (optional)
The user's phone number. Required if `email` is not provided.
### authProvider (optional)
The OAuth provider to use for authentication. Supported values are `AuthProvider.Google`, `AuthProvider.Apple`, `AuthProvider.Facebook`.
### storageDirectoryPath (optional)
The path to the directory where the wallet data is stored. Defaults to the application's data directory.
### siweSigner (optional)
An external wallet instance to use for SIWE authentication.
### legacyEncryptionKey (optional)
The encryption key that is no longer required but was used in the past. Only pass this if you had used custom auth before this was deprecated.
### walletSecret (optional)
The secret identifier to use when creating server-side wallets with backend authentication.
</Details>
<Details summary="Return Value">
### EcosystemWallet
Returns an instance of EcosystemWallet, initialized for the user based on the provided email or phone number. This wallet is ready for OTP authentication and further blockchain interactions.
</Details>
## OTP Authentication Flow
The OTP authentication flow involves sending an OTP to the user's email or phone and then verifying the OTP to complete authentication:
**Send OTP:** Initiate the login process by calling SendOTP on the EcosystemWallet instance. This sends an OTP to the user's email or phone number.
```csharp
await wallet.SendOTP();
```
**Submit OTP:** Once the user receives the OTP, they submit it back to the application, which then calls LoginWithOtp on the EcosystemWallet instance to verify the OTP and complete the login process.
```csharp
var address = await wallet.LoginWithOtp("userEnteredOTP");
// If this fails, feel free to catch and take in another OTP and retry the login process
```
## Example
Here's an example of creating an `EcosystemWallet` with a user's email and completing the OTP authentication flow:
```csharp
// Create EcosystemWallet wallet as signer to unlock web2 auth
var ecosystemWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", email: "[email protected]"); // or email: null, phoneNumber: "+1234567890"
// Resume session (if `EcosystemWallet` wallet was not logged in)
if (!await ecosystemWallet.IsConnected())
{
await ecosystemWallet.SendOTP();
Console.WriteLine("Please submit the OTP.");
var otp = Console.ReadLine();
var ecosystemWalletAddress = await ecosystemWallet.LoginWithOtp(otp); // try catch and retry if needed
}
Console.WriteLine($"EcosystemWallet address: {await ecosystemWallet.GetAddress()}");
// Sign a message
var message = "Hello, Thirdweb!";
var signature = await wallet.PersonalSign(message);
Console.WriteLine($"Signature: {signature}");
```
**Note:** EcosystemWallet leverages the security of OTP-based authentication to ensure a secure and user-friendly experience in blockchain applications.
## OAuth Authentication Flow
**LoginWithOauth:** Initiate the login process by calling LoginWithOauth on the EcosystemWallet instance. This redirects the user to the OAuth provider's login page.
```csharp
// Windows console app example
var address = await ecosystemWallet.LoginWithOauth(
isMobile: false,
browserOpenAction: (url) =>
{
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
_ = Process.Start(psi);
},
);
// Godot standalone example
var address = await ThirdwebManager.Instance.EcosystemWallet.LoginWithOauth(
isMobile: OS.GetName() == "Android" || OS.GetName() == "iOS",
browserOpenAction: (url) => OS.ShellOpen(url),
mobileRedirectScheme: "thirdweb://"
);
```
<Details summary="Parameters">
### isMobile
A `bool` indicating whether the application is running on a mobile platform.
### browserOpenAction
An `Action<string>` that opens the OAuth provider's login page in a browser.
### mobileRedirectScheme
The redirect scheme to use for mobile platforms. Defaults to `"thirdweb://"`.
### browser
An instance of `IThirdwebBrowser` to use for the OAuth login process. Defaults to `null`.
### cancellationToken
A `CancellationToken` to cancel the operation. Defaults to `default`.
</Details>
<Details summary="Return Value">
### string
The EcosystemWallet address as a hexadecimal `string`.
</Details>
## Example
Here's an example of creating an `EcosystemWallet` using OAuth.
```csharp
// Create EcosystemWallet wallet as signer to unlock web2 auth
var ecosystemWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Google);
// Resume session (if `EcosystemWallet` wallet was not logged in)
if (!await ecosystemWallet.IsConnected())
{
try {
var address = await ecosystemWallet.LoginWithOauth(
isMobile: false,
browserOpenAction: (url) =>
{
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
_ = Process.Start(psi);
},
);
Console.WriteLine($"OAuth login successful. EcosystemWallet address: {address}");
} catch (Exception ex) {
Console.WriteLine($"OAuth login failed: {ex.Message}");
return;
}
}
```
**Note:** The `LoginWithOauth` API allows for custom browser handling, making it suitable for various application types and platforms.
## External Wallet Auth (Siwe & SiweExternal)
**LoginWithSiwe:** Initiate the login process by calling LoginWithSiwe on the EcosystemWallet instance. This will prompt the external wallet to sign a message instantly.
```csharp
var address = await siweWallet.LoginWithSiwe(chainId: 1);
```
**LoginWithSiweExternal:** Initiate the login process by calling LoginWithSiweExternal on the EcosystemWallet instance. This will initiate a browser-based login flow for external wallets that only support web platforms.
```csharp
var address = await wallet.LoginWithSiweExternal(
// Windows console app example, adaptable to any runtime
isMobile: false,
browserOpenAction: (url) =>
{
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
_ = Process.Start(psi);
},
forceWalletIds: new List<string> { "io.metamask", "com.coinbase.wallet", "xyz.abs" }
);
```
Note: The parameters are similar to the OAuth flow, with the addition of `forceWalletIds` to specify the wallet IDs to force the user to use. Using a single wallet id will skip the wallet selection screen and directly open the wallet.
## Unified Identity - Account Linking
EcosystemWallet supports linking multiple authentication methods to a single user account. This feature enables users to access their account using different authentication methods, such as email, phone, or OAuth, without creating separate accounts for each method.
### Linking Accounts
```csharp
var ecosystemWalletMain = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Google);
if (!await ecosystemWalletMain.IsConnected())
{
_ = await ecosystemWalletMain.LoginWithOauth(
isMobile: false,
(url) =>
{
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
_ = Process.Start(psi);
},
"thirdweb://",
new EcosystemWalletBrowser()
);
}
Console.WriteLine($"Main EcosystemWallet address: {await ecosystemWalletMain.GetAddress()}");
// Prepare Telegram
var socialWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", authProvider: AuthProvider.Telegram);
// Link Telegram
_ = await ecosystemWalletMain.LinkAccount(walletToLink: socialWallet,);
// Prepare Phone
var phoneWallet = await EcosystemWallet.Create(client: client, ecosystemId: "ecosystem.my-ecosystem", phoneNumber: "+1234567890");
_ = await phoneWallet.SendOTP();
var otp = Console.ReadLine();
// Link Phone
_ = await ecosystemWalletMain.LinkAccount(walletToLink: phoneWallet, otp: otp);
```
### Getting Linked Accounts
```csharp
List<LinkedAccount> linkedAccounts = await ecosystemWalletMain.GetLinkedAccounts();
```
### Unlinking Accounts
```csharp
List<LinkedAccount> linkedAccounts = await ecosystemWallet.GetLinkedAccounts();
List<LinkedAccount> linkedAccountsAfterUnlinking = await ecosystemWallet.UnlinkAccount(linkedAccounts[0]);
```
@@ -1,71 +0,0 @@
import { Details, Callout, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "EngineWallet.Create | Thirdweb .NET SDK",
description:
"Instantiate a EngineWallet to sign transactions and messages.",
});
# EngineWallet.Create
A .NET integration of thirdweb Transactions.
Instantiate a `Engine` with a given private key. This wallet is capable of signing transactions and messages, interacting with smart contracts, and performing other blockchain operations. **This wallet type is intended for backend applications only,** due to the sensitive nature of private keys.
## Usage
```csharp
// EngineWallet is compatible with IThirdwebWallet and can be used with any SDK method/extension
var engineWallet = EngineWallet.Create(
client: client,
engineUrl: Environment.GetEnvironmentVariable("ENGINE_URL"),
authToken: Environment.GetEnvironmentVariable("ENGINE_ACCESS_TOKEN"),
walletAddress: Environment.GetEnvironmentVariable("ENGINE_BACKEND_WALLET_ADDRESS"),
timeoutSeconds: null, // no timeout
additionalHeaders: null // can set things like x-account-address if using basic session keys
);
// Simple self transfer
var receipt = await engineWallet.Transfer(chainId: 11155111, toAddress: await engineWallet.GetAddress(), weiAmount: 0);
Console.WriteLine($"Receipt: {receipt}");
```
<Callout variant="warning" title="Important">
This method involves using an access token, it is meant to be used in a secure backend environment. Never expose access tokens in client-side code or store them in an insecure manner. Ideally, use environment variables or secure vaults to manage access tokens in backend services.
</Callout>
<Details summary="Parameters">
### client (required)
An instance of `ThirdwebClient`.
### engineUrl (required)
The URL of the Thirdweb Engine instance, get it from your Engine dashboard.
### authToken (required)
The access token for the Engine instance, get it from your Engine dashboard.
### walletAddress (required)
The address of the backend wallet that will be used to sign transactions. You can create a backend wallet using the Engine dashboard.
### timeoutSeconds (optional)
Enforce a timeout for requests to the Engine instance, in seconds.
### additionalHeaders (optional)
Additional headers to include in requests to the Engine instance, such as `x-account-address` for Account Abstraction/Session Key flows.
</Details>
<Details summary="Return Value">
### EngineWallet
Returns an instance of `EngineWallet`, ready to sign transactions and messages with the provided access token.
</Details>

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