Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
bc0295487b Version Packages (#8339)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-30 07:07:28 +07:00
FirekeeperandGitHub 90b779107d Add Solana wallet balance endpoint and UI integration (#8338) 2025-10-30 06:56:00 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
afe187de95 Version Packages (#8336)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-29 16:10:10 -07:00
759b93beff [SDK] Fix: move @storybook/react package to be a dev dependency (#8306)
Co-authored-by: Jonas Daniels <[email protected]>
2025-10-29 15:58:10 -07:00
Jonas DanielsandGitHub 8e357b3cb3 setup @thirdweb-dev/nexus package (#8332) 2025-10-29 15:55:12 -07:00
kumaryash90 6c318f83d6 Update implementations (#8334)
<!--

## 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 `MarketplaceV3` contract addresses and the `ZKSYNC_WETH` token addresses in the `implementations.ts` file.

### Detailed summary
- Updated `MarketplaceV3` address for network `232` from `0x9742f5ac11958cFAd151eBF0Fc31302fA409036E` to `0xB7387cF425eC8Da1b04b8E34De86245cb32bCA6D`.
- Updated `MarketplaceV3` address for network `37111` from `0x56Abb6a3f25DCcdaDa106191053b1CC54C196DEE` to `0xF8fd1016420c2B7832714241d36Efe55D41df126`.
- Updated `ZKSYNC_WETH` address for network `232` from `0xE5ecd226b3032910CEaa43ba92EE8232f8237553` to `0x6bDc36E20D267Ff0dd6097799f82e78907105e2F`.
- Updated `ZKSYNC_WETH` address for network `37111` from `0xaA91D645D7a6C1aeaa5988e0547267B77d33fe16` to `0xeee5a340Cdc9c179Db25dea45AcfD5FE8d4d3eB8`.

> ✨ 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**
  * Updated contract addresses for zkSync networks: MarketplaceV3 and WETH addresses replaced for chain 232 and chain 37111.
  * Added a changeset marking a patch release (release metadata).
  * No public API or exported signatures changed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-29 21:14:05 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f790c9fae9 Version Packages (#8328)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-29 02:14:23 -07:00
Joaquim VergesandGitHub bcbb3f540a [X402] Simplify FacilitatorNetworkSchema to accept any string (#8331) 2025-10-29 02:11:13 -07:00
Joaquim VergesandGitHub 931c730a08 [Paywall] Support string price format when tokenAddress is not provided (#8330) 2025-10-28 12:09:59 -07:00
YashandGitHub e0c83121c2 add arc testnet to chains package (#8325) 2025-10-28 12:09:02 -07:00
MananTank a82a31e5ad Dashboard: Use monotone curve type for AreaChart to avoid clipping (#8329)
<!--

## 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 `type` property for charts from `natural` to `monotone` in various components to improve the visual representation of the data.

### Detailed summary

- In `empty-chart-state.tsx`, changed `type` from `natural` to `monotone`.
- In `area-chart.tsx`, changed `type` from `natural` to `monotone`.
- In `ErrorRate.tsx`, changed `type` from `natural` to `monotone`.

> ✨ 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 chart line rendering across multiple dashboard components for improved visual consistency. Area and bar charts now use refined curve interpolation for smoother data visualization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-28 18:14:30 +00:00
MananTank 3d235e218a Dashboard: Update faucet amount for arc testnet (#8326)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview

This PR adds a new entry for the `Arc testnet` in the `faucet.ts` file, enhancing the functionality to support an additional test network alongside the existing `Sophon testnet`.

### Detailed summary

- Added a comment for `Arc testnet`.
- Introduced a new entry `5042002: 1` for the `Arc testnet`.

> ✨ 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 faucet support for the Arc testnet, allowing users connected to this network to claim test funds.
  - The claim amount on Arc is set to 1, ensuring consistent and predictable claims on that network.
  - Existing networks and the default claim behavior remain unchanged, preserving current user flows while expanding testnet coverage.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-28 17:53:36 +00:00
MananTank c08fb1356e SDK: Fix Safe wallet connection issues with WalletConnect (#8327)
<!--

## 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 addresses issues with the Safe wallet connection when using `WalletConnect`, ensuring better handling of account retrieval.

### Detailed summary

- Updated the comment for `account` retrieval to clarify that it grabs the address from mainnet if available.
- Modified the `firstAccountOn` function to return the first account from the session if no matching account is found.

> ✨ 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 Safe wallet connection issues with WalletConnect

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-28 17:19:37 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
68cca6f992 Version Packages (#8318)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-27 10:12:02 -07:00
Joaquim VergesandGitHub e0abf2b273 [SDK] Update x402 dependency and export additional types (#8319) 2025-10-27 10:02:00 -07:00
FirekeeperandGitHub abf0bbaf35 Update x402 default payment validity window (#8317) 2025-10-27 09:56:05 -07:00
MananTank 23cb3bd98c Dashboard: /pay/<id> page UI improvements, SDK: CheckoutWidget minor UI tweaks (#8310)
<!--

## 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 enhancements and minor adjustments in the `CheckoutWidget` and related components of the payment dashboard, improving styling, layout, and functionality.

### Detailed summary
- Minor UI adjustments in `CheckoutWidget`.
- Updated `formatTokenBalance.ts` to allow more flexible currency formatting.
- Added `trackingTight` prop to `Text` in `FiatValue.tsx`.
- Enhanced `PayPageWidget` with new class names for styling.
- Adjusted color values in SDK theme definitions.
- Improved layout and spacing in `WithHeader.tsx`.
- Modified text properties in `DirectPayment.tsx` for better readability.
- Created a new `PayIdPageHeader` component for improved header structure.
- Replaced complex JSX in `PayPage` with cleaner component structure, including `DotsBackgroundPattern`.

> ✨ 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**
  * Theme toggle added to Pay page headers; streamlined Pay page header and layout with decorative background.

* **Style**
  * Refined secondary/tertiary button and background colors, typography weights/letter spacing, spacing and divider styles.
  * Adjusted price and label styling, currency formatting precision, and visual spacing around branding.
  * Pay widget now uses a fallback image when no project image is provided.

* **Chores**
  * Added changelog entry documenting a patch release.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-27 16:11:49 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
dcef3f4fe7 Version Packages (#8315)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-26 01:25:18 -07:00
Joaquim VergesandGitHub bf46520a6b [SDK] Enhance compatibility with third party x402 libs (#8314) 2025-10-26 01:21:40 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
7defce7264 Version Packages (#8303)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-26 00:46:21 -07:00
Joaquim VergesandGitHub ae3a122cf1 [X402] Fix network comparison in decodePaymentRequest (#8313) 2025-10-26 00:44:27 -07:00
Jonas DanielsandGitHub 2412a6958c Handle null metadata in ERC721 and ERC1155 getNFT functions (#8312) 2025-10-24 15:22:14 -07:00
Jonas DanielsandGitHub bfd3be764c Filter out null-ish values from ERC721 and ERC1155 getNFTs() arrays (#8311) 2025-10-24 14:39:31 -07:00
Joaquim VergesandGitHub 46b9d2dd82 [Playground] Use production API URL (#8307) 2025-10-24 11:38:11 -07:00
MananTank 3e9eb49787 [MNY-289] SDK: Fix token selection UI loading state when wallet is connected to a chain that is not supported by tw bridge (#8304)
<!--

## 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 addresses an issue with the `Select Token` UI being stuck in a loading state when the wallet is connected to an unsupported chain by the `thirdweb Bridge`. It modifies the logic for selecting the default chain in various widgets.

### Detailed summary
- Updated the function from `getDefaultSelectedChain` to `findChain` for clarity.
- Improved logic to handle cases where `activeChainId` is undefined.
- Adjusted the selection of `selectedChain` to use the new `findChain` function, ensuring proper fallback options.

> ✨ 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 Select Token components from remaining in loading state when the connected wallet chain is not supported by thirdweb Bridge. This affects BuyWidget, SwapWidget, and BridgeWidget.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-23 23:39:57 +00:00
MananTank 630edadcec Dashboard: Fix horizontal overflow in /bridge page (#8302)
<!--

## 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 styling of the `bridge-page` component by adding an `overflow-hidden` class to a `div`, which likely improves the layout and prevents overflow issues.

### Detailed summary

- Added `overflow-hidden` class to the `div` in the `bridge-page` component to manage overflow behavior.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Style**
  * Updated the bridge page container to refine content display behavior and ensure consistent visual presentation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-23 23:30:50 +00:00
MananTank f757e60846 [MNY-237] SDK: Update theme colors, minor UI tweaks (#8305)
<!--

## 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 theme colors and making minor UI tweaks across various React components in the `thirdweb` package.

### Detailed summary

- Updated `weight` from `600` to `500` in multiple components' `Text`.
- Adjusted `border` styles in `Container` components.
- Modified padding and margin in several layout elements.
- Enhanced UI elements with consistent `letterSpacing` and `fontSize`.
- Added new theme stories for `darkThemeObj` and `lightThemeObj`.

> ✨ 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 Storybook theme showcase displaying color tokens and design system palette
- **Bug Fixes**
    - Improved loading skeleton UI with better scrollable layout and visual consistency
- **Style**
    - Refactored theme system with explicit color objects
    - Updated button styling with adjusted border radius and padding
    - Refined typography (font weights) across UI components
    - Adjusted layout spacing and padding throughout Bridge and wallet components
    - Increased QR code size for improved visibility
- **Chores**
    - Added changeset documentation for patch release

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-23 23:21:57 +00:00
kumaryash90 d5d593956e Update factory (#8301)
<!--

## 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 patch for the `thirdweb` package, adding a new factory address for the `create2` deployment on the Neura testnet.

### Detailed summary
- Added a new factory address for `267`: `0x4e59b44847b379578588920cA78FbF26c0B4956C` to the `FACTORIES` record in `create-2-factory.ts`.

> ✨ 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 support for Neura testnet in Create2 factory deployments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-23 17:05:22 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
9a4b1cb147 Version Packages (#8300)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-22 16:42:56 -07:00
Alec AnanianandGitHub 85fe9491cd [SDK] Fix: update React Native Send Funds screen to allow decimal amounts (#8299) 2025-10-22 15:54:19 -07:00
Joaquim VergesandGitHub 899051f788 [SDK] Add EIP-5792 support for EIP1193.toProvider() (#8298) 2025-10-22 15:54:04 -07:00
Joaquim VergesandGitHub 5828b9bd45 [Docs] Add guide for using ConnectButton with wagmi application (#8294) 2025-10-22 15:42:12 -07:00
2b68a32eaa Update airdrop tokens reference link (#8197)
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: prithvish <[email protected]>
2025-10-22 15:41:34 -07:00
Manan TankandGitHub 66ae33ee8b [MNY-243] Update /pay page to render BuyWidget (#8297) 2025-10-22 15:39:56 -07:00
Manan TankandGitHub e9788096d6 [MNY-286] SDK: Do not require connecting wallet in BuyWidget if receiverAddress is set (#8296) 2025-10-22 15:39:27 -07:00
MananTank 706d696434 [MNY-285] Dashboard: Bridge page UI updates (#8295)
<!--

## 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 `bridgeStats` object to enhance the dashboard with dynamic statistics regarding supported chains, tokens, and routes. It updates various components to utilize these statistics, improving the accuracy of displayed information.

### Detailed summary
- Added `bridgeStats` object in `data.ts` with properties for supported chains, tokens, and routes.
- Updated text in `page.tsx` files to use dynamic values from `bridgeStats`.
- Introduced `AnimatedNumbers` component to display statistics with animations.
- Replaced static statistics with dynamic `DataSquare` components in `bridge-page.tsx`.

> 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 -->
2025-10-22 21:57:02 +00:00
MananTank c36cfbb5c7 [MNY-257] Dashboard: Add query params in bridge page to configure token selection (#8293)
<!--

## 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 `Page` component in `apps/dashboard/src/app/bridge/page.tsx` by introducing asynchronous handling of search parameters and improving the logic for processing input and output chains and currencies.

### Detailed summary

- Added a `SearchParams` type to define search parameters.
- Updated `Page` to accept `searchParams` as a `Promise`.
- Implemented `onlyAddress` and `onlyNumber` functions for validation.
- Introduced a `parse` function to extract values from search parameters.
- Updated the `BridgePageUI` props for `buyTab` and `swapTab` to handle dynamic token data.

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

<!-- end pr-codex -->
2025-10-22 21:57:01 +00:00
MananTank 93a9ba9121 [PRO-72] Portal: Update hero subtext (#8292)
<!--

## 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 content of a paragraph in the `page.tsx` file to reflect a new description of the platform, enhancing its messaging and presentation.

### Detailed summary

- Changed the text within a `<p>` tag:
    - From: "Development framework for building onchain apps, games, and agents."
    - To: "Platform for building the next generation of internet products."
- Adjusted the class name from `max-w-md` to `max-w-lg` and added `text-pretty`.

> ✨ 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 hero section messaging to better reflect the platform's positioning and capabilities.
  * Enhanced text layout and formatting for improved readability on the homepage.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-22 21:53:45 +00:00
MananTank 6a7b556d75 [BLD-389] Dashboard: Project Wallet section UI improvements and refactor (#8291)
<!--

## 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 improving the handling of project wallets within the application. It removes unused components, simplifies props, and enhances the user interface for wallet management.

### Detailed summary

- Deleted unused files: `ProjectWalletSetup.client.tsx` and `ProjectWalletControls.client.tsx`.
- Updated `ProjectFTUX` to remove `managementAccessToken` prop.
- Simplified `CreateSolanaWallet` and `CreateServerWallet` components by removing unnecessary props.
- Enhanced styling in the `ProjectFTUX` story and the wallet-related components.
- Improved state management for wallet selection and transactions.
- Refactored `ProjectWalletSection` to streamline wallet creation and management processes.

> ✨ 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 comprehensive project wallet management interface with send, receive, view transactions, and change wallet actions through an interactive dashboard.

* **Refactor**
  * Reorganized wallet functionality into a dedicated module for improved code organization.
  * Simplified component interfaces by removing redundant authentication parameters throughout the dashboard.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-22 16:32:58 +00:00
122 changed files with 6145 additions and 3990 deletions
+9 -2
View File
@@ -188,9 +188,16 @@ jobs:
- name: Build Packages
run: pnpm build
- name: Report bundle size
- name: Report bundle size (thirdweb)
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
package_manager: pnpm
directory: packages/thirdweb
directory: packages/thirdweb
- name: Report bundle size (nexus)
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
package_manager: pnpm
directory: packages/nexus
+1
View File
@@ -2,6 +2,7 @@
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@marsidev/react-turnstile": "^1.1.0",
"@number-flow/react": "^0.5.10",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-avatar": "^1.1.10",
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

@@ -95,7 +95,7 @@ function SkeletonBarChart(props: {
fill={`url(#${fillAreaSkeletonId})`}
radius={8}
stroke="hsl(var(--muted-foreground))"
type="natural"
type="monotone"
/>
</AreaChart>
)}
@@ -172,7 +172,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
key={key}
stackId={props.variant !== "stacked" ? undefined : "a"}
stroke={`var(--color-${key})`}
type="natural"
type="monotone"
/>
),
)}
@@ -51,17 +51,12 @@ export function WalletAddressUI(
};
},
) {
// default back to zero address if no address provided
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
const address = props.address || ZERO_ADDRESS;
const [shortenedAddress, _lessShortenedAddress] = useMemo(() => {
return [
props.shortenAddress !== false
? `${address.slice(0, 6)}...${address.slice(-4)}`
: address,
`${address.slice(0, 14)}...${address.slice(-12)}`,
];
}, [address, props.shortenAddress]);
const shortenedAddress =
props.shortenAddress !== false
? `${address.slice(0, 6)}...${address.slice(-4)}`
: address;
if (!isAddress(address)) {
return (
+2
View File
@@ -5,6 +5,8 @@ const customClaimAmounts: Record<number, number> = {
631571: 0.1,
// Sophon testnet
531050104: 5,
// Arc testnet
5042002: 1,
};
const defaultClaimAmount = 0.01;
@@ -21,8 +21,8 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
primaryButtonText: "hsl(var(--inverted-foreground))",
primaryText: "hsl(var(--foreground))",
scrollbarBg: "hsl(var(--muted))",
secondaryButtonBg: "hsl(var(--secondary))",
secondaryButtonHoverBg: "hsl(var(--secondary)/80%)",
secondaryButtonBg: "hsl(var(--secondary)/70%)",
secondaryButtonHoverBg: "hsl(var(--secondary))",
secondaryButtonText: "hsl(var(--secondary-foreground))",
secondaryIconColor: "hsl(var(--secondary-foreground))",
secondaryIconHoverBg: "hsl(var(--accent))",
@@ -33,7 +33,7 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
separatorLine: "hsl(var(--border))",
skeletonBg: "hsl(var(--secondary-foreground)/15%)",
success: "hsl(var(--success-text))",
tertiaryBg: "hsl(var(--muted)/30%)",
tertiaryBg: "hsl(var(--muted)/50%)",
tooltipBg: "hsl(var(--popover))",
tooltipText: "hsl(var(--popover-foreground))",
},
@@ -3,6 +3,7 @@ import type { Metadata } from "next";
import { unstable_cache } from "next/cache";
import { Bridge } from "thirdweb";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { bridgeStats } from "../../../bridge/data";
import { PageHeader } from "./components/header";
import { TokenPage } from "./components/token-page";
@@ -36,7 +37,8 @@ export default async function Page() {
</h1>
<p className="text-muted-foreground">
85+ chains, 4500+ tokens and 9+ million routes supported
{bridgeStats.supportedChains} chains, {bridgeStats.supportedTokens}{" "}
tokens and {bridgeStats.supportedRoutes} routes supported
</p>
</div>
</div>
@@ -4,9 +4,14 @@ import { ProjectFTUX } from "./ProjectFTUX";
const meta = {
component: ProjectFTUX,
parameters: {
nextjs: {
appDirectory: true,
},
},
decorators: [
(Story) => (
<div className="container py-8 pb-20">
<div className="container py-8 pb-20 max-w-7xl">
<Story />
</div>
),
@@ -31,6 +36,5 @@ export const Default: Story = {
],
},
teamSlug: "bar",
managementAccessToken: undefined,
},
};
@@ -15,33 +15,12 @@ import { UnrealIcon } from "@/icons/brand-icons/UnrealIcon";
import { ContractIcon } from "@/icons/ContractIcon";
import { InsightIcon } from "@/icons/InsightIcon";
import { PayIcon } from "@/icons/PayIcon";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
import { getProjectWalletLabel } from "@/lib/project-wallet";
import {
getProjectWallet,
type ProjectWalletSummary,
} from "@/lib/server/project-wallet";
import { ClientIDSection } from "./ClientIDSection";
import { ProjectWalletControls } from "./ProjectWalletControls.client";
import { ProjectWalletSetup } from "./ProjectWalletSetup.client";
import { SecretKeySection } from "./SecretKeySection";
export async function ProjectFTUX(props: {
project: Project;
teamSlug: string;
wallet?: ProjectWalletSummary | undefined;
managementAccessToken: string | undefined;
}) {
const projectWallet = props.wallet ?? (await getProjectWallet(props.project));
export function ProjectFTUX(props: { project: Project; teamSlug: string }) {
return (
<div className="flex flex-col gap-10">
<ProjectWalletSection
project={props.project}
teamSlug={props.teamSlug}
wallet={projectWallet}
managementAccessToken={props.managementAccessToken}
/>
<IntegrateAPIKeySection project={props.project} />
<ProductsSection
projectSlug={props.project.slug}
@@ -53,64 +32,6 @@ export async function ProjectFTUX(props: {
);
}
export function ProjectWalletSection(props: {
project: Project;
teamSlug: string;
wallet: ProjectWalletSummary | undefined;
managementAccessToken: string | undefined;
}) {
const defaultLabel = getProjectWalletLabel(props.project.name);
const walletAddress = props.wallet?.address;
const label = props.wallet?.label ?? defaultLabel;
return (
<section>
<div className="rounded-lg border border-border bg-card p-4">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<div className="rounded-full border border-border bg-background p-2">
<WalletProductIcon className="size-5 text-muted-foreground" />
</div>
<div>
<p className="font-semibold text-lg tracking-tight">
Project Wallet
</p>
<p className="text-muted-foreground text-sm">
Use it for deployments, payments, and API integrations.
</p>
</div>
</div>
{walletAddress ? (
<>
<ProjectWalletControls
label={label}
project={props.project}
walletAddress={walletAddress}
/>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
<Link
className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
href={`/team/${props.teamSlug}/${props.project.slug}/transactions`}
>
View Transactions
<ChevronRightIcon className="size-4" />
</Link>
</div>
</>
) : (
<ProjectWalletSetup
managementAccessToken={props.managementAccessToken}
project={props.project}
teamSlug={props.teamSlug}
/>
)}
</div>
</div>
</section>
);
}
// Integrate API key section ------------------------------------------------------------
function IntegrateAPIKeySection({ project }: { project: Project }) {
@@ -1,747 +0,0 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
EllipsisVerticalIcon,
RefreshCcwIcon,
SendIcon,
ShuffleIcon,
WalletIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { toWei } from "thirdweb";
import { useWalletBalance } from "thirdweb/react";
import { isAddress } from "thirdweb/utils";
import { z } from "zod";
import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets";
import { sendProjectWalletTokens } from "@/actions/project-wallet/send-tokens";
import type { Project } from "@/api/project/projects";
import { FundWalletModal } from "@/components/blocks/fund-wallets-modal";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/Spinner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
import { cn } from "@/lib/utils";
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
type ProjectWalletControlsProps = {
walletAddress: string;
label: string;
project: Pick<
Project,
"id" | "publishableKey" | "teamId" | "services" | "name"
>;
defaultChainId?: number;
};
export function ProjectWalletControls(props: ProjectWalletControlsProps) {
const { walletAddress, label, project, defaultChainId } = props;
const [isSendOpen, setIsSendOpen] = useState(false);
const [isReceiveOpen, setIsReceiveOpen] = useState(false);
const [selectedChainId, setSelectedChainId] = useState(defaultChainId ?? 1);
const [isChangeWalletOpen, setIsChangeWalletOpen] = useState(false);
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
undefined,
);
const client = useMemo(() => getClientThirdwebClient(), []);
const chain = useV5DashboardChain(selectedChainId);
const queryClient = useQueryClient();
const router = useDashboardRouter();
const engineCloudService = useMemo(
() => project.services?.find((service) => service.name === "engineCloud"),
[project.services],
);
const managementAccessToken =
engineCloudService?.managementAccessToken ?? undefined;
const isManagedVault = !!engineCloudService?.encryptedAdminKey;
const serverWalletsQuery = useQuery({
enabled: !!managementAccessToken,
queryFn: async () => {
if (!managementAccessToken) {
return [] as ProjectWalletSummary[];
}
return listProjectServerWallets({
managementAccessToken,
projectId: project.id,
});
},
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
staleTime: 60_000,
});
const serverWallets = serverWalletsQuery.data ?? [];
const currentWalletAddressLower = walletAddress.toLowerCase();
const otherWallets = useMemo(() => {
return serverWallets.filter((wallet) => {
return wallet.address.toLowerCase() !== currentWalletAddressLower;
});
}, [serverWallets, currentWalletAddressLower]);
const canChangeWallet = otherWallets.length > 0;
const handleOpenChangeWallet = () => {
const currentWallet = serverWallets.find(
(wallet) => wallet.address.toLowerCase() === currentWalletAddressLower,
);
const nextWalletId =
otherWallets[0]?.id ?? currentWallet?.id ?? serverWallets[0]?.id;
setSelectedWalletId(nextWalletId);
setIsChangeWalletOpen(true);
};
const handleCloseChangeWallet = () => {
setIsChangeWalletOpen(false);
setSelectedWalletId(undefined);
};
const selectedWallet = useMemo(() => {
if (!selectedWalletId) {
return undefined;
}
return serverWallets.find((wallet) => wallet.id === selectedWalletId);
}, [selectedWalletId, serverWallets]);
const isSelectionDifferent = Boolean(
selectedWallet &&
selectedWallet.address.toLowerCase() !== currentWalletAddressLower,
);
const changeWalletMutation = useMutation({
mutationFn: async (wallet: ProjectWalletSummary) => {
await updateDefaultProjectWallet({
project: project as Project,
projectWalletAddress: wallet.address,
});
},
onError: (error) => {
const message =
error instanceof Error
? error.message
: "Failed to update project wallet";
toast.error(message);
},
onSuccess: async (_, wallet) => {
const descriptionLabel = wallet.label ?? wallet.address;
toast.success("Project wallet updated", {
description: `Now pointing to ${descriptionLabel}`,
});
await queryClient.invalidateQueries({
queryKey: [
"project",
project.id,
"server-wallets",
managementAccessToken,
],
});
handleCloseChangeWallet();
router.refresh();
},
});
const balanceQuery = useWalletBalance({
address: walletAddress,
chain,
client,
});
const balanceDisplay = balanceQuery.data
? `${balanceQuery.data.displayValue} ${balanceQuery.data.symbol}`
: undefined;
return (
<div className="space-y-5">
<div className="rounded-lg border border-border bg-background p-3">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3 lg:flex-row">
<div className="flex-1">
<p className="text-muted-foreground text-xs">Address</p>
<WalletAddress address={walletAddress} client={client} />
</div>
<div className="flex-1">
<p className="text-muted-foreground text-xs">Label</p>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<p className="font-mono text-sm break-all py-2 sm:py-0">
{label}
</p>
{canChangeWallet && (
<Button
className="h-8 gap-1 sm:ml-4"
onClick={handleOpenChangeWallet}
size="sm"
disabled={serverWalletsQuery.isLoading}
variant="outline"
>
<ShuffleIcon className="size-3.5" />
Change default
</Button>
)}
</div>
</div>
</div>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex-1 flex-col">
<p className="text-xs text-muted-foreground pb-2">Balance</p>
<div className="flex items-center gap-2">
<WalletIcon className="size-4 text-muted-foreground" />
{balanceQuery.isLoading ? (
<Spinner className="size-4" />
) : balanceDisplay ? (
<span className="text-md tracking-tight">
{balanceDisplay}
</span>
) : (
<span className="text-muted-foreground text-sm">N/A</span>
)}
<Button
aria-label="Refresh balance"
className="h-8 w-8 p-0"
disabled={balanceQuery.isFetching}
onClick={() => balanceQuery.refetch()}
size="icon"
variant="outline"
>
<RefreshCcwIcon
className={cn(
"size-4",
balanceQuery.isFetching && "animate-spin",
)}
/>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Open wallet actions"
className="h-9 w-9"
size="icon"
variant="outline"
>
<EllipsisVerticalIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 rounded-xl">
<DropdownMenuItem
className="flex items-center gap-2"
onSelect={() => setIsSendOpen(true)}
>
<SendIcon className="size-4 text-muted-foreground" />
Send funds
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onSelect={() => setIsReceiveOpen(true)}
>
<WalletIcon className="size-4 text-muted-foreground" />
Receive funds
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex-col flex-1">
<p className="text-xs text-muted-foreground pb-2">Network</p>
<SingleNetworkSelector
chainId={selectedChainId}
className="max-w-xs rounded-lg"
client={client}
disableDeprecated
disableChainId
onChange={setSelectedChainId}
placeholder="Select network"
/>
</div>
</div>
</div>
</div>
<SendProjectWalletModal
chainId={selectedChainId}
client={client}
isManagedVault={isManagedVault}
label={label}
onClose={() => setIsSendOpen(false)}
onSuccess={() => balanceQuery.refetch()}
open={isSendOpen}
publishableKey={project.publishableKey}
teamId={project.teamId}
walletAddress={walletAddress}
/>
<FundWalletModal
checkoutWidgetTitle={`Fund ${label}`}
client={client}
defaultChainId={selectedChainId}
description="Use your card or crypto to deposit into this server wallet"
onOpenChange={setIsReceiveOpen}
open={isReceiveOpen}
recipientAddress={walletAddress}
title="Fund project wallet"
/>
<Dialog
onOpenChange={(nextOpen) => {
if (!nextOpen) {
handleCloseChangeWallet();
}
}}
open={isChangeWalletOpen}
>
<DialogContent className="gap-0 p-0">
<DialogHeader className="p-4 lg:p-6">
<DialogTitle>Change default wallet</DialogTitle>
<DialogDescription>
Choose another server wallet to use as the default for this
project.
</DialogDescription>
</DialogHeader>
<div className="px-4 pb-8 lg:px-6">
{serverWalletsQuery.isLoading ? (
<div className="flex justify-center py-6">
<Spinner className="size-5" />
</div>
) : serverWallets.length <= 1 ? (
<p className="text-sm text-muted-foreground">
You need at least two server wallets to pick a different
default.
</p>
) : (
<div className="flex flex-col gap-3">
<Select
onValueChange={(value) => setSelectedWalletId(value)}
value={selectedWalletId ?? ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server wallet">
{selectedWallet ? (
<div className="flex items-center gap-2">
<WalletAddress
address={selectedWallet.address}
client={client}
/>
<div className="flex items-center gap-2">
<span className="text-sm text-foreground">
{selectedWallet.label ?? "Unnamed server wallet"}
</span>
{selectedWallet.address.toLowerCase() ===
currentWalletAddressLower && (
<Badge variant="success" className="text-xs">
current
</Badge>
)}
</div>
</div>
) : (
"Select server wallet"
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{serverWallets.map((wallet) => {
const isCurrent =
wallet.address.toLowerCase() ===
currentWalletAddressLower;
return (
<SelectItem key={wallet.id} value={wallet.id}>
<div className="flex items-center gap-2">
<WalletAddress
address={wallet.address}
client={client}
/>
<div className="flex items-center gap-2">
<span className="text-sm text-foreground">
{wallet.label ?? "Unnamed server wallet"}
</span>
{isCurrent && (
<Badge variant="success" className="text-xs">
current
</Badge>
)}
</div>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
)}
</div>
<DialogFooter className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
<Button
onClick={handleCloseChangeWallet}
type="button"
variant="outline"
disabled={changeWalletMutation.isPending}
>
Cancel
</Button>
<Button
disabled={
!selectedWallet ||
!isSelectionDifferent ||
changeWalletMutation.isPending
}
onClick={() => {
if (selectedWallet) {
changeWalletMutation.mutate(selectedWallet);
}
}}
type="button"
>
{changeWalletMutation.isPending && (
<Spinner className="mr-2 size-4" />
)}
Save default
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
const createSendFormSchema = (secretKeyLabel: string) =>
z.object({
chainId: z.number({
required_error: "Select a network",
}),
toAddress: z
.string()
.trim()
.min(1, "Destination address is required")
.refine((value) => Boolean(isAddress(value)), {
message: "Enter a valid wallet address",
}),
amount: z.string().trim().min(1, "Amount is required"),
secretKey: z.string().trim().min(1, `${secretKeyLabel} is required`),
vaultAccessToken: z.string().trim(),
});
const SECRET_KEY_LABEL = "Project secret key";
type SendFormValues = z.infer<ReturnType<typeof createSendFormSchema>>;
function SendProjectWalletModal(props: {
open: boolean;
onClose: () => void;
onSuccess: () => void;
walletAddress: string;
publishableKey: string;
teamId: string;
chainId: number;
label: string;
client: ReturnType<typeof getClientThirdwebClient>;
isManagedVault: boolean;
}) {
const {
open,
onClose,
onSuccess,
walletAddress,
publishableKey,
teamId,
chainId,
label,
client,
isManagedVault,
} = props;
const secretKeyLabel = SECRET_KEY_LABEL;
const secretKeyPlaceholder = "Enter your project secret key";
const secretKeyHelper =
"Your project secret key was generated when you created your project. If you lost it, regenerate one from Project settings.";
const vaultAccessTokenHelper =
"Vault access tokens are optional credentials with server wallet permissions. Manage them in Vault settings.";
const formSchema = useMemo(() => createSendFormSchema(SECRET_KEY_LABEL), []);
const form = useForm<SendFormValues>({
defaultValues: {
amount: "0",
chainId,
secretKey: "",
vaultAccessToken: "",
toAddress: "",
},
mode: "onChange",
resolver: zodResolver(formSchema),
});
const selectedChain = useV5DashboardChain(form.watch("chainId"));
// eslint-disable-next-line no-restricted-syntax -- form submission chainId must track selector state
useEffect(() => {
form.setValue("chainId", chainId);
}, [chainId, form]);
// eslint-disable-next-line no-restricted-syntax -- reset cached inputs when modal closes to avoid leaking state
useEffect(() => {
if (!open) {
const currentValues = form.getValues();
form.reset({
amount: "0",
chainId,
secretKey: currentValues.secretKey ?? "",
vaultAccessToken: currentValues.vaultAccessToken ?? "",
toAddress: "",
});
}
}, [open, chainId, form]);
const sendMutation = useMutation({
mutationFn: async (values: SendFormValues) => {
const quantityWei = toWei(values.amount).toString();
const secretKeyValue = values.secretKey.trim();
const vaultAccessTokenValue = values.vaultAccessToken.trim();
const result = await sendProjectWalletTokens({
chainId: values.chainId,
publishableKey,
quantityWei,
recipientAddress: values.toAddress,
teamId,
walletAddress,
secretKey: secretKeyValue,
...(vaultAccessTokenValue
? { vaultAccessToken: vaultAccessTokenValue }
: {}),
});
if (!result.ok) {
const errorMessage =
typeof result.error === "string"
? result.error
: "Failed to send funds";
throw new Error(errorMessage);
}
return result.transactionIds;
},
onError: (error) => {
toast.error(error.message);
},
onSuccess: (transactionIds) => {
toast.success("Transfer submitted", {
description:
transactionIds && transactionIds.length > 0
? `Transaction ID: ${transactionIds[0]}`
: undefined,
});
onSuccess();
onClose();
},
});
return (
<Dialog
onOpenChange={(nextOpen) => {
if (!nextOpen) {
onClose();
}
}}
open={open}
>
<DialogContent className="gap-0 p-0">
<DialogHeader className="p-4 lg:p-6">
<DialogTitle>Send from {label}</DialogTitle>
<DialogDescription>
Execute a one-off transfer using your server wallet.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) => {
void sendMutation.mutateAsync(values);
})}
>
<div className="space-y-4 px-4 pb-8 lg:px-6">
<FormField
control={form.control}
name="chainId"
render={({ field }) => (
<FormItem>
<FormLabel>Network</FormLabel>
<FormControl>
<SingleNetworkSelector
chainId={field.value}
className="bg-card"
client={client}
disableDeprecated
onChange={(nextChainId) => {
field.onChange(nextChainId);
}}
placeholder="Select network"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="toAddress"
render={({ field }) => (
<FormItem>
<FormLabel>Send to</FormLabel>
<FormControl>
<Input
autoComplete="off"
placeholder="0x..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<Input
inputMode="decimal"
min="0"
step="any"
{...field}
/>
</FormControl>
<FormDescription>
Sending native token
{selectedChain?.nativeCurrency?.symbol
? ` (${selectedChain.nativeCurrency.symbol})`
: ""}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretKey"
render={({ field }) => (
<FormItem>
<FormLabel>{secretKeyLabel}</FormLabel>
<FormControl>
<Input
autoComplete="off"
autoCorrect="off"
placeholder={secretKeyPlaceholder}
spellCheck={false}
type="password"
{...field}
/>
</FormControl>
<FormDescription>{secretKeyHelper}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{!isManagedVault && (
<FormField
control={form.control}
name="vaultAccessToken"
render={({ field }) => (
<FormItem>
<FormLabel>
Vault access token
<span className="text-muted-foreground">
{" "}
(optional)
</span>
</FormLabel>
<FormControl>
<Input
autoComplete="off"
autoCorrect="off"
placeholder="Enter a vault access token (optional)"
spellCheck={false}
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{vaultAccessTokenHelper}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
<div className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
<Button onClick={onClose} type="button" variant="outline">
Cancel
</Button>
<Button
className="gap-2"
disabled={!form.formState.isValid || sendMutation.isPending}
type="submit"
>
{sendMutation.isPending && <Spinner className="size-4" />}
Submit transfer
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
@@ -1,215 +0,0 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CircleAlertIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets";
import type { Project } from "@/api/project/projects";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
import CreateServerWallet from "../../transactions/server-wallets/components/create-server-wallet.client";
export function ProjectWalletSetup(props: {
project: Project;
teamSlug: string;
managementAccessToken: string | undefined;
}) {
const { project, teamSlug, managementAccessToken } = props;
const router = useDashboardRouter();
const queryClient = useQueryClient();
const client = useMemo(() => getClientThirdwebClient(), []);
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
undefined,
);
const serverWalletsQuery = useQuery({
enabled: Boolean(managementAccessToken),
queryFn: async () => {
if (!managementAccessToken) {
return [] as ProjectWalletSummary[];
}
return listProjectServerWallets({
managementAccessToken,
projectId: project.id,
});
},
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
staleTime: 60_000,
});
const serverWallets = serverWalletsQuery.data ?? [];
const effectiveSelectedWalletId = useMemo(() => {
if (
selectedWalletId &&
serverWallets.some((wallet) => wallet.id === selectedWalletId)
) {
return selectedWalletId;
}
return serverWallets[0]?.id;
}, [selectedWalletId, serverWallets]);
const selectedWallet = useMemo(() => {
if (!effectiveSelectedWalletId) {
return undefined;
}
return serverWallets.find(
(wallet) => wallet.id === effectiveSelectedWalletId,
);
}, [effectiveSelectedWalletId, serverWallets]);
const setDefaultWalletMutation = useMutation({
mutationFn: async (wallet: ProjectWalletSummary) => {
await updateDefaultProjectWallet({
project,
projectWalletAddress: wallet.address,
});
},
onError: (error) => {
const message =
error instanceof Error
? error.message
: "Failed to update project wallet";
toast.error(message);
},
onSuccess: async (_, wallet) => {
const descriptionLabel = wallet.label ?? wallet.address;
toast.success("Project wallet updated", {
description: `Now pointing to ${descriptionLabel}`,
});
await queryClient.invalidateQueries({
queryKey: [
"project",
project.id,
"server-wallets",
managementAccessToken,
],
});
router.refresh();
},
});
const canSelectExisting =
Boolean(managementAccessToken) && serverWallets.length > 0;
return (
<Alert variant="info">
<CircleAlertIcon className="size-5" />
<AlertTitle>No project wallet set</AlertTitle>
<AlertDescription>
Set a project wallet to use for dashboard and API integrations.
</AlertDescription>
<div className="mt-4 space-y-5">
{serverWalletsQuery.isLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Spinner className="size-4" />
<span>Loading existing server wallets…</span>
</div>
) : canSelectExisting ? (
<div className="space-y-4">
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">
Choose an existing server wallet
</p>
<p className="text-sm text-muted-foreground">
These wallets were already created for this project. Pick one to
set as the default.
</p>
<Select
onValueChange={(value) => setSelectedWalletId(value)}
value={effectiveSelectedWalletId ?? ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server wallet">
{selectedWallet ? (
<div className="flex items-center gap-2">
<WalletAddress
address={selectedWallet.address}
client={client}
/>
<span className="text-muted-foreground text-sm">
{selectedWallet.label ?? "Unnamed server wallet"}
</span>
</div>
) : (
"Select server wallet"
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{serverWallets.map((wallet) => (
<SelectItem key={wallet.id} value={wallet.id}>
<div className="flex items-center gap-2">
<WalletAddress
address={wallet.address}
client={client}
/>
<span className="text-muted-foreground text-sm">
{wallet.label ?? "Unnamed server wallet"}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button
disabled={!selectedWallet || setDefaultWalletMutation.isPending}
onClick={() => {
if (selectedWallet) {
setDefaultWalletMutation.mutate(selectedWallet);
}
}}
type="button"
>
{setDefaultWalletMutation.isPending && (
<Spinner className="mr-2 size-4" />
)}
Set as project wallet
</Button>
<CreateServerWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
setAsProjectWallet
/>
</div>
</div>
) : (
<div className="space-y-3 text-sm text-muted-foreground">
<p>
Create a server wallet to start using transactions and other
project features.
</p>
<CreateServerWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
setAsProjectWallet
/>
</div>
)}
</div>
</Alert>
);
}
@@ -0,0 +1,736 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
ArrowLeftRightIcon,
EllipsisVerticalIcon,
RefreshCcwIcon,
SendIcon,
ShuffleIcon,
WalletIcon,
} from "lucide-react";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { type ThirdwebClient, toWei } from "thirdweb";
import { useWalletBalance } from "thirdweb/react";
import { isAddress } from "thirdweb/utils";
import { z } from "zod";
import { sendProjectWalletTokens } from "@/actions/project-wallet/send-tokens";
import type { Project } from "@/api/project/projects";
import { FundWalletModal } from "@/components/blocks/fund-wallets-modal";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/Spinner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import type { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
import { cn } from "@/lib/utils";
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
type GetProjectServerWallets = (params: {
managementAccessToken: string;
projectId: string;
}) => Promise<ProjectWalletSummary[]>;
type ProjectWalletControlsProps = {
projectWallet: ProjectWalletSummary;
project: Project;
defaultChainId?: number;
teamSlug: string;
getProjectServerWallets: GetProjectServerWallets;
client: ThirdwebClient;
};
export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
const { projectWallet, project, defaultChainId } = props;
const [isSendOpen, setIsSendOpen] = useState(false);
const [isReceiveOpen, setIsReceiveOpen] = useState(false);
const [selectedChainId, setSelectedChainId] = useState(defaultChainId ?? 1);
const [isChangeWalletOpen, setIsChangeWalletOpen] = useState(false);
const chain = useV5DashboardChain(selectedChainId);
const engineCloudService = useMemo(
() => project.services?.find((service) => service.name === "engineCloud"),
[project.services],
);
const managementAccessToken =
engineCloudService?.managementAccessToken ?? undefined;
const isManagedVault = !!engineCloudService?.encryptedAdminKey;
const serverWalletsQuery = useQuery({
enabled: !!managementAccessToken,
queryFn: async () => {
if (!managementAccessToken) {
return [] as ProjectWalletSummary[];
}
return props.getProjectServerWallets({
managementAccessToken,
projectId: project.id,
});
},
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
staleTime: 60_000,
});
const balanceQuery = useWalletBalance({
address: projectWallet.address,
chain,
client: props.client,
});
const canChangeWallet =
serverWalletsQuery.data && serverWalletsQuery.data.length > 1;
const router = useDashboardRouter();
return (
<div>
<div className="rounded-xl border border-border bg-card relative">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Open wallet actions"
className="p-2 h-auto w-auto absolute right-4 top-4 z-10"
variant="ghost"
>
<EllipsisVerticalIcon className="size-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64 rounded-xl">
<DropdownMenuItem
className="flex items-center gap-2"
onSelect={() => setIsSendOpen(true)}
>
<SendIcon className="size-4 text-muted-foreground" />
Send funds
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onSelect={() => setIsReceiveOpen(true)}
>
<WalletIcon className="size-4 text-muted-foreground" />
Receive funds
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onSelect={() =>
router.push(
`/team/${props.teamSlug}/${props.project.slug}/transactions`,
)
}
>
<ArrowLeftRightIcon className="size-4 text-muted-foreground" />
View transactions
</DropdownMenuItem>
{canChangeWallet && (
<DropdownMenuItem
className="flex items-center gap-2"
onSelect={() => setIsChangeWalletOpen(true)}
>
<ShuffleIcon className="size-4 text-muted-foreground" />
Change project wallet
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<div className="p-4 relative flex flex-col gap-5">
<div>
<p className="text-sm text-foreground mb-1">Wallet Address</p>
<CopyTextButton
textToShow={projectWallet.address}
textToCopy={projectWallet.address}
tooltip="Copy wallet address"
copyIconPosition="right"
variant="ghost"
className="text-muted-foreground -translate-x-1.5 py-0 h-auto"
/>
</div>
<div>
<p className="text-sm text-foreground mb-1">Wallet Label</p>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm break-all py-2 sm:py-0 text-muted-foreground">
{projectWallet.label || "N/A"}
</p>
</div>
</div>
</div>
<div className="p-4 border-t border-dashed flex justify-between items-center">
<div>
<p className="text-sm text-foreground mb-1">Balance</p>
<div className="flex items-center gap-1">
{balanceQuery.isFetching || !balanceQuery.data ? (
<Skeleton className="h-5 w-[100px]" />
) : (
<span className="text-sm text-muted-foreground">
{balanceQuery.data.displayValue} {balanceQuery.data.symbol}
</span>
)}
<Button
aria-label="Refresh balance"
className="p-1 h-auto w-auto"
disabled={balanceQuery.isFetching}
onClick={() => balanceQuery.refetch()}
variant="ghost"
>
<RefreshCcwIcon
className={cn(
"size-3 text-muted-foreground",
balanceQuery.isFetching && "animate-spin",
)}
/>
</Button>
</div>
</div>
<SingleNetworkSelector
chainId={selectedChainId}
className="w-fit rounded-full bg-background hover:bg-accent/50"
client={props.client}
disableDeprecated
disableChainId
onChange={setSelectedChainId}
placeholder="Select network"
popoverContentClassName="!w-[320px] rounded-xl overflow-hidden"
align="end"
/>
</div>
</div>
<SendProjectWalletModal
chainId={selectedChainId}
client={props.client}
isManagedVault={isManagedVault}
label={projectWallet.label || ""}
onClose={() => setIsSendOpen(false)}
onSuccess={() => balanceQuery.refetch()}
open={isSendOpen}
publishableKey={project.publishableKey}
teamId={project.teamId}
walletAddress={projectWallet.address}
/>
<FundWalletModal
checkoutWidgetTitle="Fund Project Wallet"
client={props.client}
defaultChainId={selectedChainId}
description="Use your card or crypto to deposit into this server wallet"
onOpenChange={setIsReceiveOpen}
open={isReceiveOpen}
recipientAddress={projectWallet.address}
title="Fund project wallet"
/>
<Dialog onOpenChange={setIsChangeWalletOpen} open={isChangeWalletOpen}>
<DialogContent className="gap-0 p-0 overflow-hidden">
<ChangeProjectWalletDialogContent
isOpen={isChangeWalletOpen}
setIsOpen={setIsChangeWalletOpen}
serverWallets={{
data: serverWalletsQuery.data ?? [],
isPending: serverWalletsQuery.isLoading,
}}
projectWallet={projectWallet}
client={props.client}
project={project}
managementAccessToken={managementAccessToken}
/>
</DialogContent>
</Dialog>
</div>
);
}
function ChangeProjectWalletDialogContent(props: {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
serverWallets: {
data: ProjectWalletSummary[];
isPending: boolean;
};
projectWallet: ProjectWalletSummary;
managementAccessToken: string | undefined;
project: Project;
client: ThirdwebClient;
}) {
const queryClient = useQueryClient();
const router = useDashboardRouter();
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
props.projectWallet.id,
);
const serverWallets = props.serverWallets.data;
const currentWalletAddressLower = props.projectWallet.address.toLowerCase();
const selectedWallet = useMemo(() => {
if (!selectedWalletId) {
return undefined;
}
return serverWallets.find((wallet) => wallet.id === selectedWalletId);
}, [selectedWalletId, serverWallets]);
const isSelectionDifferent = Boolean(
selectedWallet &&
selectedWallet.address.toLowerCase() !== currentWalletAddressLower,
);
const changeWalletMutation = useMutation({
mutationFn: async (wallet: ProjectWalletSummary) => {
await updateDefaultProjectWallet({
project: props.project,
projectWalletAddress: wallet.address,
});
},
onError: (error) => {
const message =
error instanceof Error
? error.message
: "Failed to update project wallet";
toast.error(message);
},
onSuccess: async () => {
toast.success("Project wallet updated");
await queryClient.invalidateQueries({
queryKey: [
"project",
props.project.id,
"server-wallets",
props.managementAccessToken,
],
});
router.refresh();
},
});
return (
<div>
<DialogHeader className="p-4 lg:p-6">
<DialogTitle>Change Project Wallet</DialogTitle>
<DialogDescription>
Choose a server wallet to use as project wallet
</DialogDescription>
</DialogHeader>
<div className="px-4 pb-8 lg:px-6">
{props.serverWallets.isPending ? (
<div className="flex justify-center py-6">
<Spinner className="size-5" />
</div>
) : (
<div className="flex flex-col gap-3">
<Select
onValueChange={(value) => {
setSelectedWalletId(value);
}}
value={selectedWalletId ?? ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server wallet">
{selectedWallet ? (
<div className="flex items-center gap-2">
<WalletAddress
address={selectedWallet.address}
client={props.client}
/>
<div className="flex items-center gap-2">
<span className="text-sm text-foreground">
{selectedWallet.label ?? "Unnamed server wallet"}
</span>
{selectedWallet.address.toLowerCase() ===
currentWalletAddressLower && (
<Badge variant="success" className="text-xs">
current
</Badge>
)}
</div>
</div>
) : (
"Select server wallet"
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{serverWallets.map((wallet) => {
const isCurrent =
wallet.address.toLowerCase() === currentWalletAddressLower;
return (
<SelectItem key={wallet.id} value={wallet.id}>
<div className="flex items-center gap-2">
<WalletAddress
address={wallet.address}
client={props.client}
/>
<div className="flex items-center gap-2">
<span className="text-sm text-foreground">
{wallet.label ?? "Unnamed server wallet"}
</span>
{isCurrent && (
<Badge variant="success" className="text-xs">
current
</Badge>
)}
</div>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
)}
</div>
<div className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
<Button
onClick={() => props.setIsOpen(false)}
type="button"
variant="outline"
disabled={changeWalletMutation.isPending}
>
Cancel
</Button>
<Button
disabled={
!selectedWallet ||
!isSelectionDifferent ||
changeWalletMutation.isPending
}
onClick={() => {
if (selectedWallet) {
changeWalletMutation.mutate(selectedWallet);
}
}}
type="button"
>
{changeWalletMutation.isPending && (
<Spinner className="mr-2 size-4" />
)}
Set as project wallet
</Button>
</div>
</div>
);
}
const createSendFormSchema = (secretKeyLabel: string) =>
z.object({
chainId: z.number({
required_error: "Select a network",
}),
toAddress: z
.string()
.trim()
.min(1, "Destination address is required")
.refine((value) => Boolean(isAddress(value)), {
message: "Enter a valid wallet address",
}),
amount: z.string().trim().min(1, "Amount is required"),
secretKey: z.string().trim().min(1, `${secretKeyLabel} is required`),
vaultAccessToken: z.string().trim(),
});
const SECRET_KEY_LABEL = "Project secret key";
type SendFormValues = z.infer<ReturnType<typeof createSendFormSchema>>;
type SendProjectWalletModalProps = {
open: boolean;
onClose: () => void;
onSuccess: () => void;
walletAddress: string;
publishableKey: string;
teamId: string;
chainId: number;
label: string;
client: ReturnType<typeof getClientThirdwebClient>;
isManagedVault: boolean;
};
const secretKeyLabel = SECRET_KEY_LABEL;
const secretKeyPlaceholder = "Enter your project secret key";
const secretKeyHelper =
"Your project secret key was generated when you created your project. If you lost it, regenerate one from Project settings.";
const vaultAccessTokenHelper =
"Vault access tokens are optional credentials with server wallet permissions. Manage them in Vault settings.";
function SendProjectWalletModal(props: SendProjectWalletModalProps) {
return (
<Dialog
onOpenChange={(nextOpen) => {
if (!nextOpen) {
props.onClose();
}
}}
open={props.open}
>
<DialogContent className="gap-0 p-0 overflow-hidden">
<SendProjectWalletModalContent {...props} />
</DialogContent>
</Dialog>
);
}
function SendProjectWalletModalContent(props: SendProjectWalletModalProps) {
const {
onClose,
onSuccess,
walletAddress,
publishableKey,
teamId,
chainId,
label,
client,
isManagedVault,
} = props;
const formSchema = useMemo(() => createSendFormSchema(SECRET_KEY_LABEL), []);
const form = useForm<SendFormValues>({
defaultValues: {
amount: "0",
chainId,
secretKey: "",
vaultAccessToken: "",
toAddress: "",
},
mode: "onChange",
resolver: zodResolver(formSchema),
});
const selectedChain = useV5DashboardChain(form.watch("chainId"));
const sendMutation = useMutation({
mutationFn: async (values: SendFormValues) => {
const quantityWei = toWei(values.amount).toString();
const secretKeyValue = values.secretKey.trim();
const vaultAccessTokenValue = values.vaultAccessToken.trim();
const result = await sendProjectWalletTokens({
chainId: values.chainId,
publishableKey,
quantityWei,
recipientAddress: values.toAddress,
teamId,
walletAddress,
secretKey: secretKeyValue,
...(vaultAccessTokenValue
? { vaultAccessToken: vaultAccessTokenValue }
: {}),
});
if (!result.ok) {
const errorMessage =
typeof result.error === "string"
? result.error
: "Failed to send funds";
throw new Error(errorMessage);
}
return result.transactionIds;
},
onError: (error) => {
toast.error(error.message);
},
onSuccess: (transactionIds) => {
toast.success("Transfer submitted", {
description:
transactionIds && transactionIds.length > 0
? `Transaction ID: ${transactionIds[0]}`
: undefined,
});
onSuccess();
onClose();
},
});
return (
<div className="gap-0 p-0">
<DialogHeader className="p-4 lg:p-6">
<DialogTitle>Send from {label}</DialogTitle>
<DialogDescription>
Execute a one-off transfer using your server wallet.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) => {
void sendMutation.mutateAsync(values);
})}
>
<div className="space-y-4 px-4 pb-8 lg:px-6">
<FormField
control={form.control}
name="chainId"
render={({ field }) => (
<FormItem>
<FormLabel>Network</FormLabel>
<FormControl>
<SingleNetworkSelector
chainId={field.value}
className="bg-card"
client={client}
disableDeprecated
onChange={(nextChainId) => {
field.onChange(nextChainId);
}}
placeholder="Select network"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="toAddress"
render={({ field }) => (
<FormItem>
<FormLabel>Send to</FormLabel>
<FormControl>
<Input autoComplete="off" placeholder="0x..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<Input inputMode="decimal" min="0" step="any" {...field} />
</FormControl>
<FormDescription>
Sending native token
{selectedChain?.nativeCurrency?.symbol
? ` (${selectedChain.nativeCurrency.symbol})`
: ""}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretKey"
render={({ field }) => (
<FormItem>
<FormLabel>{secretKeyLabel}</FormLabel>
<FormControl>
<Input
autoComplete="off"
autoCorrect="off"
placeholder={secretKeyPlaceholder}
spellCheck={false}
type="password"
{...field}
/>
</FormControl>
<FormDescription>{secretKeyHelper}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{!isManagedVault && (
<FormField
control={form.control}
name="vaultAccessToken"
render={({ field }) => (
<FormItem>
<FormLabel>
Vault access token
<span className="text-muted-foreground"> (optional)</span>
</FormLabel>
<FormControl>
<Input
autoComplete="off"
autoCorrect="off"
placeholder="Enter a vault access token (optional)"
spellCheck={false}
type="password"
{...field}
/>
</FormControl>
<FormDescription>{vaultAccessTokenHelper}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
<div className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
<Button onClick={onClose} type="button" variant="outline">
Cancel
</Button>
<Button
className="gap-2"
disabled={!form.formState.isValid || sendMutation.isPending}
type="submit"
>
{sendMutation.isPending && <Spinner className="size-4" />}
Submit transfer
</Button>
</div>
</form>
</Form>
</div>
);
}
@@ -0,0 +1,126 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
import { projectStub } from "@/storybook/stubs";
import { storybookThirdwebClient } from "@/storybook/utils";
import { ProjectWalletSectionUI } from "./project-wallet";
const meta = {
component: ProjectWalletSectionUI,
title: "Project/ProjectWalletSection",
parameters: {
nextjs: {
appDirectory: true,
},
},
decorators: [
(Story) => (
<div className="container py-8 pb-20 max-w-7xl">
<Story />
</div>
),
],
} satisfies Meta<typeof ProjectWalletSectionUI>;
export default meta;
type Story = StoryObj<typeof meta>;
const projectWithManagedAccessToken = projectStub("foo", "bar");
projectWithManagedAccessToken.services = [
{
name: "engineCloud",
actions: [],
managementAccessToken: "managed-access-token",
},
];
const projectWithoutManagedAccessToken = projectStub("foo", "bar");
const projectWallet1: ProjectWalletSummary = {
id: "server-wallet-id",
address: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
label: "Project Wallet 1",
};
const projectWallet2: ProjectWalletSummary = {
id: "server-wallet-id-2",
address: "0x83Dd93fA5D8343094f850f90B3fb90088C1bB425",
label: "Project Wallet 2",
};
export const NoProjectWalletSetNoManagedAccessToken: Story = {
args: {
project: projectWithoutManagedAccessToken,
client: storybookThirdwebClient,
teamSlug: "bar",
projectWallet: undefined,
getProjectServerWallets: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return [];
},
},
};
export const NoProjectWalletSetWithManagedAccessToken: Story = {
args: {
project: projectWithManagedAccessToken,
client: storybookThirdwebClient,
teamSlug: "bar",
projectWallet: undefined,
getProjectServerWallets: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return [];
},
},
};
export const NoProjectWalletSetWithManagedAccessTokenAndServerWallets: Story = {
args: {
project: projectWithManagedAccessToken,
teamSlug: "bar",
client: storybookThirdwebClient,
projectWallet: undefined,
getProjectServerWallets: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return [projectWallet1, projectWallet2];
},
},
};
export const NoProjectWalletSetLoading: Story = {
args: {
project: projectWithManagedAccessToken,
teamSlug: "bar",
client: storybookThirdwebClient,
projectWallet: undefined,
getProjectServerWallets: async () => {
await new Promise((resolve) => setTimeout(resolve, 100000));
return [projectWallet1, projectWallet2];
},
},
};
export const ProjectWalletSetMultipleServerWallets: Story = {
args: {
project: projectWithManagedAccessToken,
teamSlug: "bar",
client: storybookThirdwebClient,
projectWallet: projectWallet1,
getProjectServerWallets: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return [projectWallet1, projectWallet2];
},
},
};
export const ProjectWalletSetSingleServerWallet: Story = {
args: {
project: projectWithManagedAccessToken,
teamSlug: "bar",
projectWallet: projectWallet1,
client: storybookThirdwebClient,
getProjectServerWallets: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return [projectWallet1];
},
},
};
@@ -0,0 +1,331 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { UnderlineLink } from "@workspace/ui/components/UnderlineLink";
import { ChevronDownIcon, XIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets";
import type { Project } from "@/api/project/projects";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Spinner } from "@/components/ui/Spinner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
import { CreateServerWallet } from "../../transactions/server-wallets/components/create-server-wallet.client";
import { ProjectWalletDetailsSection } from "./project-wallet-details";
type GetProjectServerWallets = (params: {
managementAccessToken: string;
projectId: string;
}) => Promise<ProjectWalletSummary[]>;
function CreateProjectWalletSection(props: {
project: Project;
teamSlug: string;
getProjectServerWallets: GetProjectServerWallets;
}) {
const { project, teamSlug } = props;
const router = useDashboardRouter();
const queryClient = useQueryClient();
const client = useMemo(() => getClientThirdwebClient(), []);
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
undefined,
);
const [isSelectDialogOpen, setIsSelectDialogOpen] = useState(false);
const managementAccessToken =
props.project.services?.find((service) => service.name === "engineCloud")
?.managementAccessToken ?? undefined;
const serverWalletsQuery = useQuery({
enabled: Boolean(managementAccessToken),
queryFn: async () => {
if (!managementAccessToken) {
return [] as ProjectWalletSummary[];
}
return props.getProjectServerWallets({
managementAccessToken,
projectId: project.id,
});
},
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
refetchOnWindowFocus: false,
});
const serverWallets = serverWalletsQuery.data ?? [];
const effectiveSelectedWalletId = useMemo(() => {
if (
selectedWalletId &&
serverWallets.some((wallet) => wallet.id === selectedWalletId)
) {
return selectedWalletId;
}
return serverWallets[0]?.id;
}, [selectedWalletId, serverWallets]);
const selectedWallet = useMemo(() => {
if (!effectiveSelectedWalletId) {
return undefined;
}
return serverWallets.find(
(wallet) => wallet.id === effectiveSelectedWalletId,
);
}, [effectiveSelectedWalletId, serverWallets]);
const setDefaultWalletMutation = useMutation({
mutationFn: async (wallet: ProjectWalletSummary) => {
await updateDefaultProjectWallet({
project,
projectWalletAddress: wallet.address,
});
},
onError: (error) => {
const message =
error instanceof Error
? error.message
: "Failed to update project wallet";
toast.error(message);
},
onSuccess: async (_, wallet) => {
const descriptionLabel = wallet.label ?? wallet.address;
toast.success("Project wallet updated", {
description: `Now pointing to ${descriptionLabel}`,
});
await queryClient.invalidateQueries({
queryKey: [
"project",
project.id,
"server-wallets",
managementAccessToken,
],
});
setIsSelectDialogOpen(false);
router.refresh();
},
});
const canSelectExisting =
Boolean(managementAccessToken) && serverWallets.length > 0;
return (
<div className="rounded-xl border p-4 bg-card">
<div className="rounded-full p-2 border bg-background inline-flex mb-4">
<XIcon className="size-3.5 text-muted-foreground" />
</div>
<h2 className="text-sm font-medium text-foreground mb-0.5">
No Project Wallet set
</h2>
<p className="text-sm text-muted-foreground">
Set a project wallet to set the default sender in thirdweb API.
</p>
<div className="mt-4 space-y-5 text-foreground">
{serverWalletsQuery.isLoading ? (
<Skeleton className="h-[36px] max-w-lg rounded-full" />
) : canSelectExisting ? (
<div className="flex items-center gap-3">
<Button
className="bg-background rounded-full gap-2"
variant="outline"
size="sm"
onClick={() => {
setSelectedWalletId(effectiveSelectedWalletId);
setIsSelectDialogOpen(true);
}}
type="button"
>
<ChevronDownIcon className="size-4 text-muted-foreground" />
Select a server wallet
</Button>
<CreateServerWallet
project={project}
teamSlug={teamSlug}
setAsProjectWallet
button={{ size: "sm", iconClassName: "size-3.5" }}
/>
<Dialog
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setIsSelectDialogOpen(false);
}
}}
open={isSelectDialogOpen}
>
<DialogContent className="gap-0 p-0 overflow-hidden">
<DialogHeader className="p-4 lg:p-6">
<DialogTitle>Select project wallet</DialogTitle>
<DialogDescription>
Choose a server wallet to use as the default for this
project.
</DialogDescription>
</DialogHeader>
<div className="px-4 pb-8 lg:px-6">
{serverWalletsQuery.isLoading ? (
<div className="flex justify-center py-6">
<Spinner className="size-5" />
</div>
) : serverWallets.length === 0 ? (
<p className="text-sm text-muted-foreground">
No server wallets found.
</p>
) : (
<div className="flex flex-col gap-3">
<Select
onValueChange={(value) => setSelectedWalletId(value)}
value={selectedWalletId ?? ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server wallet" />
</SelectTrigger>
<SelectContent>
{serverWallets.map((wallet) => (
<SelectItem key={wallet.id} value={wallet.id}>
<div className="flex items-center gap-3">
<WalletAddress
address={wallet.address}
client={client}
className="h-auto py-1"
/>
<span className="text-sm text-muted-foreground">
{wallet.label}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
<div className="flex justify-end gap-3 border-t bg-card p-4 lg:p-6">
<Button
onClick={() => {
setIsSelectDialogOpen(false);
}}
type="button"
variant="outline"
disabled={setDefaultWalletMutation.isPending}
>
Cancel
</Button>
<Button
disabled={
!selectedWallet || setDefaultWalletMutation.isPending
}
onClick={() => {
if (selectedWallet) {
setDefaultWalletMutation.mutate(selectedWallet);
}
}}
type="button"
>
{setDefaultWalletMutation.isPending && (
<Spinner className="mr-2 size-4" />
)}
Set as project wallet
</Button>
</div>
</DialogContent>
</Dialog>
</div>
) : (
<div className="space-y-3 text-sm text-muted-foreground">
<CreateServerWallet
project={project}
teamSlug={teamSlug}
setAsProjectWallet
button={{ size: "sm", iconClassName: "size-3.5" }}
/>
</div>
)}
</div>
</div>
);
}
export function ProjectWalletSectionUI(props: {
project: Project;
teamSlug: string;
projectWallet: ProjectWalletSummary | undefined;
getProjectServerWallets: GetProjectServerWallets;
client: ThirdwebClient;
}) {
return (
<div>
<div className="mb-3">
<h2 className="mb-1 font-semibold text-xl tracking-tight">
Project Wallet
</h2>
<p className="text-muted-foreground text-sm">
The server wallet to be used as the default sender in{" "}
<UnderlineLink
href="https://portal.thirdweb.com/reference"
target="_blank"
>
thirdweb API
</UnderlineLink>
</p>
</div>
{props.projectWallet ? (
<ProjectWalletDetailsSection
project={props.project}
projectWallet={props.projectWallet}
client={props.client}
teamSlug={props.teamSlug}
getProjectServerWallets={props.getProjectServerWallets}
/>
) : (
<CreateProjectWalletSection
project={props.project}
teamSlug={props.teamSlug}
getProjectServerWallets={props.getProjectServerWallets}
/>
)}
</div>
);
}
export function ProjectWalletSection(props: {
project: Project;
teamSlug: string;
projectWallet: ProjectWalletSummary | undefined;
client: ThirdwebClient;
}) {
return (
<ProjectWalletSectionUI
project={props.project}
teamSlug={props.teamSlug}
client={props.client}
projectWallet={props.projectWallet}
getProjectServerWallets={listProjectServerWallets}
/>
);
}
@@ -59,7 +59,6 @@ export function ErrorRate({ datapoints }: { datapoints: ResultItem[] }) {
key={tag}
stackId="a"
stroke={`var(--color-${tag})`}
type="natural"
/>,
);
}
@@ -39,10 +39,8 @@ import type {
import { loginRedirect } from "@/utils/redirects";
import { PieChartCard } from "../../../components/Analytics/PieChartCard";
import { EngineCloudChartCardAsync } from "./components/EngineCloudChartCard";
import {
ProjectFTUX,
ProjectWalletSection,
} from "./components/ProjectFTUX/ProjectFTUX";
import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX";
import { ProjectWalletSection } from "./components/project-wallet/project-wallet";
import { RpcMethodBarChartCardAsync } from "./components/RpcMethodBarChartCard";
import { TransactionsChartCardAsync } from "./components/Transactions";
import { ProjectHighlightsCard } from "./overview/highlights-card";
@@ -112,9 +110,6 @@ export default async function ProjectOverviewPage(props: PageProps) {
});
const projectWallet = await getProjectWallet(project);
const managementAccessToken =
project.services?.find((service) => service.name === "engineCloud")
?.managementAccessToken ?? undefined;
return (
<ResponsiveSearchParamsProvider value={searchParams}>
@@ -135,14 +130,17 @@ export default async function ProjectOverviewPage(props: PageProps) {
actions: null,
}}
>
<ProjectWalletSection
project={project}
teamSlug={params.team_slug}
projectWallet={projectWallet}
client={client}
/>
<div className="h-10" />
{isActive ? (
<div className="flex flex-col gap-4 md:gap-6">
<ProjectWalletSection
project={project}
teamSlug={params.team_slug}
wallet={projectWallet}
managementAccessToken={managementAccessToken}
/>
<ResponsiveTimeFilters defaultRange={defaultRange} />
<ProjectAnalytics
authToken={authToken}
@@ -155,12 +153,7 @@ export default async function ProjectOverviewPage(props: PageProps) {
/>
</div>
) : (
<ProjectFTUX
project={project}
teamSlug={params.team_slug}
wallet={projectWallet}
managementAccessToken={managementAccessToken}
/>
<ProjectFTUX project={project} teamSlug={params.team_slug} />
)}
</ProjectPage>
</ResponsiveSearchParamsProvider>
@@ -3,14 +3,13 @@ import { useMemo } from "react";
import type { ThirdwebClient } from "thirdweb";
import type { Project } from "@/api/project/projects";
import { type Step, StepsCard } from "@/components/blocks/StepsCard";
import CreateServerWallet from "../server-wallets/components/create-server-wallet.client";
import { CreateServerWallet } from "../server-wallets/components/create-server-wallet.client";
import type { Wallet } from "../server-wallets/wallet-table/types";
import type { SolanaWallet } from "../solana-wallets/wallet-table/types";
import { SendTestSolanaTransaction } from "./send-test-solana-tx.client";
import { SendTestTransaction } from "./send-test-tx.client";
interface Props {
managementAccessToken: string | undefined;
project: Project;
wallets: Wallet[];
solanaWallets: SolanaWallet[];
@@ -29,11 +28,12 @@ export const EngineChecklist: React.FC<Props> = (props) => {
const steps: Step[] = [];
steps.push({
children: (
<CreateServerWalletStep
managementAccessToken={props.managementAccessToken}
project={props.project}
teamSlug={props.teamSlug}
/>
<div className="mt-4 flex flex-row gap-4">
<CreateServerWallet
project={props.project}
teamSlug={props.teamSlug}
/>
</div>
),
completed: props.wallets.length > 0 || props.hasTransactions,
description:
@@ -61,7 +61,6 @@ export const EngineChecklist: React.FC<Props> = (props) => {
});
return steps;
}, [
props.managementAccessToken,
props.project,
props.wallets,
props.hasTransactions,
@@ -113,19 +112,3 @@ export const EngineChecklist: React.FC<Props> = (props) => {
/>
);
};
function CreateServerWalletStep(props: {
project: Project;
teamSlug: string;
managementAccessToken: string | undefined;
}) {
return (
<div className="mt-4 flex flex-row gap-4">
<CreateServerWallet
managementAccessToken={props.managementAccessToken}
project={props.project}
teamSlug={props.teamSlug}
/>
</div>
);
}
@@ -40,6 +40,13 @@ import {
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import {
@@ -57,8 +64,9 @@ import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import { fetchSolanaBalance } from "../lib/getSolanaBalance";
import { updateDefaultProjectWallet } from "../lib/vault.client";
import CreateServerWallet from "../server-wallets/components/create-server-wallet.client";
import { CreateServerWallet } from "../server-wallets/components/create-server-wallet.client";
import type { Wallet as EVMWallet } from "../server-wallets/wallet-table/types";
import { CreateSolanaWallet } from "../solana-wallets/components/create-solana-wallet.client";
import { UpgradeSolanaPermissions } from "../solana-wallets/components/upgrade-solana-permissions.client";
@@ -77,9 +85,9 @@ interface ServerWalletsTableProps {
solanaTotalPages: number;
project: Project;
teamSlug: string;
managementAccessToken: string | undefined;
client: ThirdwebClient;
solanaPermissionError?: boolean;
authToken: string;
}
export function ServerWalletsTable(props: ServerWalletsTableProps) {
@@ -88,7 +96,6 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
solanaWallets,
project,
teamSlug,
managementAccessToken,
evmTotalRecords,
evmCurrentPage,
evmTotalPages,
@@ -97,10 +104,14 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
solanaTotalPages,
client,
solanaPermissionError,
authToken,
} = props;
const [activeChain, setActiveChain] = useState<WalletChain>("evm");
const [selectedChainId, setSelectedChainId] = useState<number>(1);
const [selectedSolanaChain, setSelectedSolanaChain] = useState<
"solana:mainnet" | "solana:devnet"
>("solana:mainnet");
const [showSmartAccount, setShowSmartAccount] = useState(false);
const queryClient = useQueryClient();
@@ -134,11 +145,7 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
<div className="flex flex-row gap-2.5">
{activeChain === "evm" && (
<>
<CreateServerWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
/>
<CreateServerWallet project={project} teamSlug={teamSlug} />
<SingleNetworkSelector
chainId={selectedChainId}
onChange={setSelectedChainId}
@@ -151,12 +158,33 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
</>
)}
{activeChain === "solana" && (
<CreateSolanaWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
disabled={solanaPermissionError}
/>
<>
<CreateSolanaWallet
project={project}
teamSlug={teamSlug}
disabled={solanaPermissionError}
/>
<Select
value={selectedSolanaChain}
onValueChange={(value) =>
setSelectedSolanaChain(
value as "solana:mainnet" | "solana:devnet",
)
}
>
<SelectTrigger className="w-fit min-w-[180px] rounded-full bg-background hover:bg-accent/50">
<SelectValue placeholder="Select network" />
</SelectTrigger>
<SelectContent className="rounded-xl">
<SelectItem value="solana:mainnet" className="rounded-lg">
Solana Mainnet
</SelectItem>
<SelectItem value="solana:devnet" className="rounded-lg">
Solana Devnet
</SelectItem>
</SelectContent>
</Select>
</>
)}
</div>
@@ -285,6 +313,8 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
project={project}
teamSlug={teamSlug}
client={client}
authToken={authToken}
chainId={selectedSolanaChain}
/>
))}
</TableBody>
@@ -514,11 +544,15 @@ function SolanaWalletRow({
project,
teamSlug,
client,
authToken,
chainId,
}: {
wallet: SolanaWallet;
project: Project;
teamSlug: string;
client: ThirdwebClient;
authToken: string;
chainId: "solana:mainnet" | "solana:devnet";
}) {
const engineService = project.services.find(
(s) => s.name === "engineCloud",
@@ -554,7 +588,12 @@ function SolanaWalletRow({
</TableCell>
<TableCell>
<SolanaWalletBalance publicKey={wallet.publicKey} />
<SolanaWalletBalance
publicKey={wallet.publicKey}
authToken={authToken}
clientId={project.publishableKey}
chainId={chainId}
/>
</TableCell>
<TableCell>
@@ -746,16 +785,27 @@ function WalletBalance({
);
}
function SolanaWalletBalance({ publicKey }: { publicKey: string }) {
function SolanaWalletBalance({
publicKey,
authToken,
clientId,
chainId,
}: {
publicKey: string;
authToken: string;
clientId: string;
chainId: "solana:mainnet" | "solana:devnet";
}) {
const balance = useQuery({
queryFn: async () => {
// TODO: Implement actual Solana balance fetching
return {
displayValue: "0",
symbol: "SOL",
};
return await fetchSolanaBalance({
publicKey,
authToken,
clientId,
chainId,
});
},
queryKey: ["solanaWalletBalance", publicKey],
queryKey: ["solanaWalletBalance", publicKey, chainId],
});
if (balance.isFetching) {
@@ -0,0 +1,60 @@
import { configure, getSolanaWalletBalance } from "@thirdweb-dev/api";
import { THIRDWEB_API_HOST } from "@/constants/urls";
// Configure the API client to use the correct base URL
configure({
override: {
baseUrl: THIRDWEB_API_HOST,
},
});
export async function fetchSolanaBalance({
publicKey,
authToken,
clientId,
chainId = "solana:mainnet",
}: {
publicKey: string;
authToken: string;
clientId: string;
chainId?: "solana:mainnet" | "solana:devnet";
}): Promise<{
displayValue: string;
symbol: string;
value: string;
decimals: number;
} | null> {
try {
const response = await getSolanaWalletBalance({
path: {
address: publicKey,
},
query: {
chainId,
},
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": clientId,
},
});
if (response.error || !response.data) {
console.error(
"Error fetching Solana balance:",
response.error || "No data returned",
);
return null;
}
return {
displayValue: response.data.result.displayValue,
symbol: "SOL",
value: response.data.result.value,
decimals: response.data.result.decimals,
};
} catch (error) {
console.error("Error fetching Solana balance:", error);
return null;
}
}
@@ -156,7 +156,6 @@ export default async function TransactionsAnalyticsPage(props: {
isManagedVault={isManagedVault}
client={client}
hasTransactions={hasTransactions}
managementAccessToken={managementAccessToken ?? undefined}
project={project}
teamSlug={params.team_slug}
testTxWithWallet={searchParams.testTxWithWallet as string | undefined}
@@ -207,7 +206,6 @@ export default async function TransactionsAnalyticsPage(props: {
evmTotalPages={Math.ceil(eoas.data.totalRecords / pageSize)}
evmTotalRecords={eoas.data.totalRecords}
evmWallets={eoas.data.items as Wallet[]}
managementAccessToken={managementAccessToken ?? undefined}
project={project}
solanaCurrentPage={solanCurrentPage}
solanaTotalPages={Math.ceil(
@@ -217,6 +215,7 @@ export default async function TransactionsAnalyticsPage(props: {
solanaWallets={solanaAccounts.data.items}
teamSlug={params.team_slug}
solanaPermissionError={isSolanaPermissionError}
authToken={authToken}
/>
)}
</div>
@@ -15,18 +15,26 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/Spinner";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import { createProjectServerWallet } from "../../lib/vault.client";
export default function CreateServerWallet(props: {
export function CreateServerWallet(props: {
project: Project;
teamSlug: string;
managementAccessToken: string | undefined;
setAsProjectWallet?: boolean;
button?: {
size?: "default" | "sm" | "lg";
iconClassName?: string;
};
}) {
const router = useDashboardRouter();
const [label, setLabel] = useState("");
const [modalOpen, setModalOpen] = useState(false);
const managementAccessToken =
props.project.services?.find((service) => service.name === "engineCloud")
?.managementAccessToken ?? undefined;
const createEoaMutation = useMutation({
mutationFn: async ({
managementAccessToken,
@@ -53,12 +61,12 @@ export default function CreateServerWallet(props: {
});
const handleCreateServerWallet = async () => {
if (!props.managementAccessToken) {
if (!managementAccessToken) {
router.push(`/team/${props.teamSlug}/${props.project.slug}/vault`);
} else {
await createEoaMutation.mutateAsync({
label,
managementAccessToken: props.managementAccessToken,
managementAccessToken: managementAccessToken,
});
}
};
@@ -68,21 +76,32 @@ export default function CreateServerWallet(props: {
return (
<>
<Button
className="gap-1.5 rounded-full bg-background"
className="gap-1.5 rounded-full bg-background text-foreground"
variant="outline"
size={props.button?.size}
onClick={() =>
props.managementAccessToken
managementAccessToken
? setModalOpen(true)
: router.push(`/team/${props.teamSlug}/${props.project.slug}/vault`)
}
>
{isLoading ? (
<Spinner className="size-4" />
<Spinner
className={cn(
"size-4 text-muted-foreground",
props.button?.iconClassName,
)}
/>
) : (
<PlusIcon className="size-4 text-muted-foreground" />
<PlusIcon
className={cn(
"size-4 text-muted-foreground",
props.button?.iconClassName,
)}
/>
)}
{props.managementAccessToken
{managementAccessToken
? isLoading
? "Creating..."
: "Create Server Wallet"
@@ -20,7 +20,6 @@ import { useDashboardRouter } from "@/lib/DashboardRouter";
import { createSolanaAccount } from "../lib/vault.client";
export function CreateSolanaWallet(props: {
managementAccessToken: string | undefined;
project: Project;
teamSlug: string;
disabled?: boolean;
@@ -29,14 +28,18 @@ export function CreateSolanaWallet(props: {
const [label, setLabel] = useState("");
const router = useDashboardRouter();
const managementAccessToken =
props.project.services?.find((service) => service.name === "engineCloud")
?.managementAccessToken ?? undefined;
const createMutation = useMutation({
mutationFn: async () => {
if (!props.managementAccessToken) {
if (!managementAccessToken) {
throw new Error("No management access token");
}
const result = await createSolanaAccount({
managementAccessToken: props.managementAccessToken,
managementAccessToken: managementAccessToken,
label: label.trim(),
projectId: props.project.id,
teamId: props.project.teamId,
@@ -67,8 +70,8 @@ export function CreateSolanaWallet(props: {
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="gap-2 rounded-full bg-foreground hover:bg-foreground/90"
variant="outline"
className="gap-1.5 rounded-full bg-background text-foreground"
disabled={props.disabled}
>
<PlusIcon className="size-4" />
@@ -193,7 +193,6 @@ export default async function Page(props: {
evmTotalPages={Math.ceil(eoas.data.totalRecords / pageSize)}
evmTotalRecords={eoas.data.totalRecords}
evmWallets={eoas.data.items as Wallet[]}
managementAccessToken={managementAccessToken ?? undefined}
project={project}
solanaCurrentPage={solanCurrentPage}
solanaTotalPages={Math.ceil(
@@ -203,6 +202,7 @@ export default async function Page(props: {
solanaWallets={solanaAccounts.data.items}
teamSlug={params.team_slug}
solanaPermissionError={isSolanaPermissionError || false}
authToken={authToken}
/>
)}
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -1,6 +1,13 @@
import type { Format } from "@number-flow/react";
import { cn } from "@workspace/ui/lib/utils";
import Image, { type StaticImageData } from "next/image";
import type { BuyAndSwapEmbedProps } from "@/components/blocks/BuyAndSwapEmbed";
import { FaqAccordion } from "@/components/blocks/faq-section";
import ChainsImage from "../assets/chains.png";
import RoutesImage from "../assets/routes.png";
import TokensImage from "../assets/tokens.png";
import { bridgeStats } from "../data";
import { AnimatedNumbers } from "./client/animated-numbers";
import { UniversalBridgeEmbed } from "./client/UniversalBridgeEmbed";
import { BridgePageHeader } from "./header";
@@ -13,7 +20,7 @@ export function BridgePageUI(props: {
<div className="grow flex flex-col">
<BridgePageHeader />
<div className="flex grow items-center justify-center px-4 relative pt-12 pb-20 lg:py-28 min-h-[calc(100dvh-60px)]">
<div className="flex grow items-center justify-center px-4 relative pt-12 pb-20 lg:py-28 min-h-[calc(100dvh-60px)] overflow-hidden">
<DotsBackgroundPattern />
<UniversalBridgeEmbed buyTab={props.buyTab} swapTab={props.swapTab} />
</div>
@@ -35,24 +42,58 @@ function HeadingSection(props: { title: React.ReactNode }) {
<div className="mb-3 lg:mb-6">{props.title}</div>
<p className="text-muted-foreground text-sm text-pretty text-center lg:text-lg mb-6 lg:mb-8">
Seamlessly move your assets across 85+ chains with the best rates and
fastest execution
Seamlessly move your assets across {bridgeStats.supportedChains} chains
with the best rates and fastest execution
</p>
<div className="flex flex-col lg:flex-row gap-3 lg:gap-2 items-center justify-center">
<DataPill>85+ Chains Supported</DataPill>
<DataPill>4500+ Tokens Supported</DataPill>
<DataPill>9+ Million Routes Available</DataPill>
<div className="flex gap-3 items-center lg:justify-center flex-wrap">
<DataSquare data={90} label="Chains Supported" src={ChainsImage} />
<DataSquare
data={6700}
label="Tokens Supported"
src={TokensImage}
format={{ notation: "compact" }}
/>
<DataSquare
data={14000000}
format={{ notation: "compact" }}
label="Routes Available"
src={RoutesImage}
/>
</div>
</div>
);
}
function DataPill(props: { children: React.ReactNode }) {
function DataSquare(props: {
data: number;
label: string;
src: StaticImageData;
format?: Format;
imageClassName?: string;
}) {
return (
<p className="bg-card flex items-center text-xs lg:text-sm gap-1.5 text-foreground border rounded-full px-8 lg:px-3 py-1.5 hover:text-foreground transition-colors duration-300">
{props.children}
</p>
<div className="py-2 lg:py-0 size-full lg:size-[220px] rounded-xl border hover:bg-card bg-card/50 relative shrink-0 overflow-hidden">
<div className="p-4">
<div className="flex items-center gap-1 text-3xl lg:text-5xl font-medium tracking-tight font-mono mb-1 h-[45px] lg:h-[56px]">
<AnimatedNumbers
value={props.data}
format={props.format}
suffix="+"
/>
</div>
<p className="text-sm text-foreground">{props.label}</p>
</div>
<Image
draggable={false}
src={props.src}
className={cn(
"object-cover h-full lg:h-auto absolute right-0 top-0 bottom-0 left-1/2 lg:left-0 lg:top-auto invert dark:invert-0 opacity-50 lg:opacity-100",
props.imageClassName,
)}
alt=""
/>
</div>
);
}
@@ -15,6 +15,7 @@ export const bridgeWallets = [
createWallet("me.rainbow"),
createWallet("io.rabby"),
createWallet("io.zerion.wallet"),
createWallet("com.okex.wallet"),
];
export function UniversalBridgeEmbed(props: {
@@ -0,0 +1,41 @@
"use client";
import NumberFlow, { type Format } from "@number-flow/react";
import { useLayoutEffect, useRef, useState } from "react";
export function AnimatedNumbers(props: {
value: number;
format?: Format;
suffix?: string;
}) {
const ref = useRef<HTMLDivElement>(null);
const [value, setValue] = useState(props.value);
useLayoutEffect(() => {
if (ref.current) {
// when the div becomes visible, set the value to the props value
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setValue(props.value);
} else {
setValue(0);
}
});
});
observer.observe(ref.current);
return () => observer.disconnect();
}
}, [props.value]);
return (
<div ref={ref}>
<NumberFlow
willChange
value={value}
format={props.format}
suffix={props.suffix}
/>
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
export const bridgeStats = {
supportedChains: "90+",
supportedTokens: "6700+",
supportedRoutes: "14M+",
};
@@ -5,6 +5,7 @@ import type { ChainMetadata } from "thirdweb/chains";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { fetchChain } from "@/utils/fetchChain";
import { BridgePageUI } from "../../components/bridge-page";
import { bridgeStats } from "../../data";
import { generateTokenPairSlugs, getTokenPairData } from "./slug-map";
type Params = {
@@ -35,8 +36,7 @@ export async function generateMetadata(
}
const title = getTitle(tokenMetadata);
const description =
"Bridge and swap 4500+ tokens across 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality";
const description = `Bridge and swap ${bridgeStats.supportedTokens} tokens across ${bridgeStats.supportedChains} chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality`;
const result: Metadata = {
title,
+56 -6
View File
@@ -1,9 +1,10 @@
import type { Metadata } from "next";
import { isAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb";
import { BridgePageUI } from "./components/bridge-page";
import { bridgeStats } from "./data";
const title = "thirdweb Bridge: Buy, Bridge & Swap Crypto on 85+ Chains";
const description =
"Bridge and swap 4500+ tokens across 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality";
const title = `thirdweb Bridge: Buy, Bridge & Swap Crypto on ${bridgeStats.supportedChains} Chains`;
const description = `Bridge and swap ${bridgeStats.supportedTokens} tokens across ${bridgeStats.supportedChains} chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing with near-instant finality`;
export const metadata: Metadata = {
description,
@@ -14,11 +15,50 @@ export const metadata: Metadata = {
title,
};
export default function Page() {
type SearchParams = {
[key: string]: string | string[] | undefined;
};
export default async function Page(props: {
searchParams: Promise<SearchParams>;
}) {
const searchParams = await props.searchParams;
const onlyAddress = (v: string) => (isAddress(v) ? v : undefined);
const onlyNumber = (v: string) =>
Number.isNaN(Number(v)) ? undefined : Number(v);
// output is buy, input is sell
const sellChain = parse(searchParams.inputChain, onlyNumber);
const sellCurrency = parse(searchParams.inputCurrency, onlyAddress);
const buyChain = parse(searchParams.outputChain, onlyNumber);
const buyCurrency = parse(searchParams.outputCurrency, onlyAddress);
return (
<BridgePageUI
buyTab={undefined}
swapTab={undefined}
buyTab={{
buyToken: buyChain
? {
chainId: buyChain,
tokenAddress: buyCurrency || NATIVE_TOKEN_ADDRESS,
}
: undefined,
}}
swapTab={{
sellToken: sellChain
? {
chainId: sellChain,
tokenAddress: sellCurrency || NATIVE_TOKEN_ADDRESS,
}
: undefined,
buyToken: buyChain
? {
chainId: buyChain,
tokenAddress: buyCurrency || NATIVE_TOKEN_ADDRESS,
}
: undefined,
}}
title={
<h1 className="text-3xl md:text-6xl font-semibold tracking-tighter text-balance text-center">
Bridge and Swap tokens <br className="max-sm:hidden" /> across any
@@ -28,3 +68,13 @@ export default function Page() {
/>
);
}
function parse<T>(
value: string | string[] | undefined,
fn: (value: string) => T | undefined,
): T | undefined {
if (typeof value === "string") {
return fn(value);
}
return undefined;
}
@@ -49,6 +49,7 @@ const loginOptions = [
createWallet("io.rabby"),
createWallet("me.rainbow"),
createWallet("io.zerion.wallet"),
createWallet("com.okex.wallet"),
];
const inAppWalletLoginOptions = [
@@ -0,0 +1,72 @@
"use client";
import { Img } from "@workspace/ui/components/img";
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 { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { payAppThirdwebClient } from "../constants";
export function PayIdPageHeader(props: {
projectName: string;
projectIcon: string | undefined;
}) {
return (
<div className="border-b border-border/70">
<header className="container flex max-w-7xl justify-between py-4">
<div className="flex items-center gap-3">
{props.projectIcon && (
<Img
src={
resolveSchemeWithErrorHandler({
uri: props.projectIcon,
client: payAppThirdwebClient,
}) || ""
}
alt=""
className="rounded-full size-6 object-cover"
/>
)}
<h2 className="text-xl font-semibold tracking-tight">
{props.projectName}
</h2>
</div>
<div className="flex items-center gap-3 lg:gap-5">
<ToggleThemeButton />
</div>
</header>
</div>
);
}
function ToggleThemeButton(props: { className?: string }) {
const { setTheme, theme } = useTheme();
return (
<ClientOnly
ssr={<Skeleton className="size-[36px] rounded-full border bg-accent" />}
>
<Button
aria-label="Toggle theme"
className={cn(
"h-auto w-auto rounded-full p-2 text-muted-foreground hover:text-foreground",
props.className,
)}
onClick={() => {
setTheme(theme === "dark" ? "light" : "dark");
}}
variant="ghost"
>
{theme === "light" ? (
<SunIcon className="size-5 " />
) : (
<MoonIcon className="size-5 " />
)}
</Button>
</ClientOnly>
);
}
+40 -146
View File
@@ -1,21 +1,16 @@
import { ShieldCheckIcon } from "lucide-react";
import { cn } from "@workspace/ui/lib/utils";
import type { Metadata } from "next";
import { ThemeProvider } from "next-themes";
import { Bridge, defineChain, toTokens } from "thirdweb";
import { getChainMetadata } from "thirdweb/chains";
import { shortenAddress } from "thirdweb/utils";
import { Bridge } from "thirdweb";
import { getPaymentLink } from "@/api/universal-bridge/links";
import { Badge } from "@/components/ui/badge";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import {
API_SERVER_SECRET,
DASHBOARD_THIRDWEB_SECRET_KEY,
} from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { resolveEns } from "@/lib/ens";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { PayPageWidget } from "../components/client/PayPageWidget.client";
import { payAppThirdwebClient } from "../constants";
import { PayIdPageHeader } from "./header";
const title = "thirdweb Pay";
const description = "Fast, secure, and simple payments.";
@@ -54,26 +49,10 @@ export default async function PayPage({
tokenAddress: paymentLink.destinationToken.address,
});
const chainPromise = getChainMetadata(
// eslint-disable-next-line no-restricted-syntax
defineChain(Number(paymentLink.destinationToken.chainId)),
);
const recipientPromise = resolveEns(
paymentLink.receiver,
getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
);
const [tokens, projectMetadata, chain, recipientEnsOrAddress] =
await Promise.all([
tokensPromise,
projectMetadataPromise,
chainPromise,
recipientPromise,
]);
const [tokens, projectMetadata] = await Promise.all([
tokensPromise,
projectMetadataPromise,
]);
const token = tokens[0];
if (!token) {
@@ -81,122 +60,20 @@ export default async function PayPage({
}
return (
<ThemeProvider
forcedTheme={theme === "light" ? "light" : "dark"}
attribute="class"
disableTransitionOnChange
enableSystem={false}
>
<div className="flex z-10 flex-col lg:flex-row h-full w-full">
<header className="min-w-full lg:min-w-[500px] border-b lg:border-r lg:h-full bg-card flex flex-col gap-4 items-start p-4 lg:p-8">
<div>
<div className="flex flex-row items-center justify-start gap-4">
{projectMetadata.image && (
<img
src={
resolveSchemeWithErrorHandler({
uri: projectMetadata.image,
client: payAppThirdwebClient,
}) || ""
}
alt={projectMetadata.name}
width={25}
height={25}
className="rounded-full overflow-hidden"
/>
)}
<h2 className="text-xl font-bold">{projectMetadata.name}</h2>
</div>
{projectMetadata.description && (
<p className="mt-2 text-sm text-muted-foreground">
{projectMetadata.description}
</p>
)}
</div>
<div className="relative flex flex-col min-h-dvh w-full">
<ThemeProvider
defaultTheme={theme === "light" ? "light" : "dark"}
attribute="class"
disableTransitionOnChange
enableSystem={false}
>
<PayIdPageHeader
projectIcon={projectMetadata.image || undefined}
projectName={projectMetadata.name}
/>
<div className="hidden lg:block my-4 w-full">
{paymentLink.amount && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">Details</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
<div className="flex flex-row items-center gap-2">
{token.iconUri && (
<img
src={resolveSchemeWithErrorHandler({
uri: token.iconUri,
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
})}
alt={token.name}
width={25}
height={25}
className="size-5 rounded-full overflow-hidden"
/>
)}
{toTokens(BigInt(paymentLink.amount), token.decimals)}{" "}
{token.symbol}
</div>
{token.prices.USD && (
<span>
$
{(
Number(token.prices.USD) *
Number(
toTokens(BigInt(paymentLink.amount), token.decimals),
)
).toFixed(2)}
</span>
)}
</div>
</div>
)}
{chain && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">Network</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
<div className="flex flex-row items-center gap-2">
{chain.icon?.url && (
<img
src={resolveSchemeWithErrorHandler({
uri: chain.icon.url,
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
})}
alt={chain.name}
width={chain.icon.width}
height={chain.icon.height}
className="size-5 rounded-full overflow-hidden"
/>
)}
{chain.name}
</div>
</div>
</div>
)}
{recipientEnsOrAddress.ensName ||
(recipientEnsOrAddress.address && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">Seller</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
{recipientEnsOrAddress.ensName ??
shortenAddress(recipientEnsOrAddress.address)}
</div>
</div>
))}
</div>
<div className="mt-auto hidden lg:block">
<Badge className="flex items-center gap-1.5 bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800">
<ShieldCheckIcon className="size-3" />
Secured by thirdweb
</Badge>
</div>
</header>
<main className="flex justify-center p-12 w-full items-center">
<main className="flex justify-center py-12 w-full items-center grow overflow-hidden relative">
<DotsBackgroundPattern />
<PayPageWidget
amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
chainId={Number(paymentLink.destinationToken.chainId)}
@@ -210,8 +87,8 @@ export default async function PayPage({
token={token}
/>
</main>
</div>
</ThemeProvider>
</ThemeProvider>
</div>
);
}
@@ -228,7 +105,24 @@ async function getProjectMetadata(clientId: string) {
}
const { data } = (await response.json()) as {
data: { name: string; image: string | null; description: string | null };
data: { name: string; image: string | null };
};
return data;
}
function DotsBackgroundPattern(props: { className?: string }) {
return (
<div
className={cn(
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/15 hidden lg:block",
props.className,
)}
style={{
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
backgroundSize: "24px 24px",
maskImage:
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
}}
/>
);
}
@@ -1,4 +1,5 @@
"use client";
import { cn } from "@workspace/ui/lib/utils";
import { payAppThirdwebClient } from "app/pay/constants";
import { useTheme } from "next-themes";
import { createThirdwebClient, NATIVE_TOKEN_ADDRESS, toTokens } from "thirdweb";
@@ -63,7 +64,12 @@ export function PayPageWidget({
client={
clientId ? createThirdwebClient({ clientId }) : payAppThirdwebClient
}
image={image}
className={cn(
"shadow-xl",
!image &&
"[&_.tw-header-image]:invert dark:[&_.tw-header-image]:invert-0",
)}
image={image || "/assets/pay/general-pay.png"}
name={name}
onSuccess={() => {
reportPaymentLinkBuySuccessful();
@@ -1,399 +0,0 @@
"use client";
import { payAppThirdwebClient } from "app/pay/constants";
import { ChevronDownIcon, CreditCardIcon } from "lucide-react";
import { useCallback, useId, useMemo, useState } from "react";
import { toast } from "sonner";
import {
createThirdwebClient,
defineChain,
getContract,
type ThirdwebClient,
toUnits,
} from "thirdweb";
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
import { resolveScheme, upload } from "thirdweb/storage";
import { FileInput } from "@/components/blocks/FileInput";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { TokenSelector } from "@/components/blocks/TokenSelector";
import { Button } from "@/components/ui/button";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { resolveEns } from "@/lib/ens";
import { cn } from "@/lib/utils";
export function PaymentLinkForm() {
const [chainId, setChainId] = useState<number>();
const [recipientAddress, setRecipientAddress] = useState("");
// TODO - clean this up later
const [tokenAddressWithChain, setTokenAddressWithChain] = useState("");
const [amount, setAmount] = useState("");
const [title, setTitle] = useState("");
const [image, setImage] = useState<File | null>(null);
const [imageUri, setImageUri] = useState<string>("");
const [uploadingImage, setUploadingImage] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false);
const [paymentUrl, setPaymentUrl] = useState<string>("");
const isFormComplete = useMemo(() => {
return chainId && recipientAddress && tokenAddressWithChain && amount;
}, [chainId, recipientAddress, tokenAddressWithChain, amount]);
const handleImageUpload = useCallback(async (file: File) => {
try {
setImage(file);
setUploadingImage(true);
const uploadClient = createThirdwebClient({
clientId: "7ae789153cf9ecde8f35649f2d8a4333",
});
const uri = await upload({
client: uploadClient,
files: [file],
});
// eslint-disable-next-line no-restricted-syntax
const resolvedUrl = resolveScheme({
client: uploadClient,
uri,
});
setImageUri(resolvedUrl);
toast.success("Image uploaded successfully");
} catch (error) {
console.error("Error uploading image:", error);
toast.error("Failed to upload image");
setImage(null);
} finally {
setUploadingImage(false);
}
}, []);
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
if (
!chainId ||
!recipientAddress ||
!tokenAddressWithChain ||
!amount
) {
throw new Error("All fields are required");
}
const inputs = await parseInputs(
payAppThirdwebClient,
chainId,
tokenAddressWithChain,
recipientAddress,
amount,
);
// Build payment URL
const params = new URLSearchParams({
amount: inputs.amount.toString(),
chainId: inputs.chainId.toString(),
clientId: payAppThirdwebClient.clientId,
recipientAddress: inputs.recipientAddress,
tokenAddress: inputs.tokenAddress,
});
// Add title as name parameter if provided
if (title) {
params.set("name", title);
}
// Add image URI if available
if (imageUri) {
params.set("image", imageUri);
}
const url = `${window.location.origin}/pay?${params.toString()}`;
setPaymentUrl(url);
toast.success("Payment created!");
} catch (err) {
toast.error(err instanceof Error ? err.message : "An error occurred");
} finally {
setIsLoading(false);
}
},
[amount, chainId, imageUri, recipientAddress, title, tokenAddressWithChain],
);
const handlePreview = useCallback(async () => {
if (!chainId || !recipientAddress || !tokenAddressWithChain || !amount) {
toast.error("Please fill in all fields first");
return;
}
try {
const inputs = await parseInputs(
payAppThirdwebClient,
chainId,
tokenAddressWithChain,
recipientAddress,
amount,
);
const params = new URLSearchParams({
amount: inputs.amount.toString(),
chainId: inputs.chainId.toString(),
clientId: payAppThirdwebClient.clientId,
recipientAddress: inputs.recipientAddress,
tokenAddress: inputs.tokenAddress,
});
// Add title as name parameter if provided
if (title) {
params.set("name", title);
}
// Add image URI if available
if (imageUri) {
params.set("image", imageUri);
}
window.open(`/pay?${params.toString()}`, "_blank");
} catch (err) {
toast.error(err instanceof Error ? err.message : "An error occurred");
}
}, [
amount,
chainId,
imageUri,
recipientAddress,
title,
tokenAddressWithChain,
]);
const [selectedChainId, selectedTokenAddress] = tokenAddressWithChain
? tokenAddressWithChain.split(":")
: [];
const recipientId = useId();
const amountId = useId();
const titleId = useId();
return (
<Card className="mx-auto w-full max-w-[500px]">
<CardHeader>
<div className="flex flex-col items-center gap-2 sm:flex-row sm:gap-2">
<div className="rounded-lg border border-muted p-1.5 sm:p-2">
<CreditCardIcon className="size-5 sm:size-6" />
</div>
<CardTitle className="text-center sm:text-left">
Create a Payment
</CardTitle>
</div>
</CardHeader>
<CardContent>
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label className="font-medium text-sm">Network</Label>
<SingleNetworkSelector
chainId={chainId}
className="w-full"
client={payAppThirdwebClient}
disableTestnets
onChange={setChainId}
/>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Token</Label>
<TokenSelector
addNativeTokenIfMissing={false}
chainId={chainId ?? undefined}
className="w-full"
client={payAppThirdwebClient}
disabled={!chainId}
enabled={!!chainId}
onChange={(value) => {
setTokenAddressWithChain(`${value.chainId}:${value.address}`);
}}
selectedToken={
selectedChainId && selectedTokenAddress
? {
address: selectedTokenAddress,
chainId: Number(selectedChainId),
}
: undefined
}
showCheck={false}
/>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm" htmlFor={recipientId}>
Recipient Address
</Label>
<Input
className="w-full"
id={recipientId}
onChange={(e) => setRecipientAddress(e.target.value)}
placeholder="Address or ENS"
required
value={recipientAddress}
/>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm" htmlFor={amountId}>
Amount
</Label>
<Input
className="w-full"
id={amountId}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
required
step="any"
type="number"
value={amount}
/>
</div>
<div className="space-y-2">
<Button
className={cn(
"flex w-full items-center justify-between px-0 text-muted-foreground hover:bg-transparent",
)}
onClick={() => setShowAdvanced(!showAdvanced)}
type="button"
variant="ghost"
>
<span>Advanced Options</span>
<ChevronDownIcon
className={`size-4 transition-transform duration-200 ease-in-out ${
showAdvanced ? "rotate-180" : ""
}`}
/>
</Button>
<div
className={cn(
"grid transition-all duration-200 ease-in-out",
showAdvanced
? "grid-rows-[1fr] opacity-100"
: "grid-rows-[0fr] opacity-0",
)}
>
<div className={cn(showAdvanced ? "" : "overflow-hidden")}>
<div className="space-y-3">
<div className="space-y-2">
<Label className="font-medium text-sm" htmlFor={titleId}>
Title
</Label>
<Input
className="w-full"
id={titleId}
onChange={(e) => setTitle(e.target.value)}
placeholder="Payment for..."
value={title}
/>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Image</Label>
<div className="w-full">
<FileInput
accept={{ "image/*": [] }}
className="!rounded-md aspect-square h-24 w-full"
client={payAppThirdwebClient}
helperText="image"
isDisabled={uploadingImage}
isDisabledText="Uploading..."
selectOrUpload="Upload"
setValue={handleImageUpload}
value={image || imageUri}
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="flex flex-col space-y-4">
{paymentUrl && (
<CopyTextButton
className="w-full justify-between truncate bg-background px-3 py-2"
copyIconPosition="right"
textToCopy={paymentUrl}
textToShow={paymentUrl}
tooltip="Copy Payment Link"
/>
)}
<div className="flex gap-2">
<Button
className="flex-1"
disabled={isLoading || !isFormComplete}
onClick={handlePreview}
type="button"
variant="outline"
>
Preview
</Button>
<Button
className="flex-1"
disabled={isLoading || !isFormComplete}
type="submit"
>
{isLoading ? "Creating..." : "Create"}
</Button>
</div>
</div>
</form>
</CardContent>
</Card>
);
}
async function parseInputs(
client: ThirdwebClient,
chainId: number,
tokenAddressWithChain: string,
recipientAddressOrEns: string,
decimalAmount: string,
) {
const [_chainId, tokenAddress] = tokenAddressWithChain.split(":");
if (Number(_chainId) !== chainId) {
throw new Error("Chain ID does not match token chain");
}
if (!tokenAddress) {
throw new Error("Missing token address");
}
const ensPromise = resolveEns(recipientAddressOrEns, client);
const currencyPromise = getCurrencyMetadata({
contract: getContract({
address: tokenAddress,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(chainId),
client,
}),
});
const [ens, currencyMetadata] = await Promise.all([
ensPromise,
currencyPromise,
]);
if (!ens.address) {
throw new Error("Invalid recipient address");
}
const amountInWei = toUnits(decimalAmount, currencyMetadata.decimals);
return {
amount: amountInWei,
chainId,
recipientAddress: ens.address,
tokenAddress,
};
}
@@ -1,11 +0,0 @@
export type PayParams = {
chainId: string;
recipientAddress: string;
tokenAddress: string;
amount: string;
name?: string;
image?: string;
theme?: "light" | "dark";
redirectUri?: string;
clientId?: string;
};
@@ -0,0 +1,61 @@
"use client";
import { MoonIcon, SunIcon } from "lucide-react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { ClientOnly } from "@/components/blocks/client-only";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { ThirdwebMiniLogo } from "../../(app)/components/ThirdwebMiniLogo";
export function PayLandingHeader(props: { containerClassName?: string }) {
return (
<div className="border-b border-border/70">
<header
className={cn(
"container flex max-w-7xl justify-between py-4",
props.containerClassName,
)}
>
<div className="flex items-center gap-6">
<Link className="flex items-center gap-2" href="/home">
<ThirdwebMiniLogo className="h-5" />
<span className="font-bold text-2xl tracking-tight">thirdweb</span>
</Link>
</div>
<div className="flex items-center gap-3 lg:gap-5">
<ToggleThemeButton />
</div>
</header>
</div>
);
}
function ToggleThemeButton(props: { className?: string }) {
const { setTheme, theme } = useTheme();
return (
<ClientOnly
ssr={<Skeleton className="size-[36px] rounded-full border bg-accent" />}
>
<Button
aria-label="Toggle theme"
className={cn(
"h-auto w-auto rounded-full p-2 text-muted-foreground hover:text-foreground",
props.className,
)}
onClick={() => {
setTheme(theme === "dark" ? "light" : "dark");
}}
variant="ghost"
>
{theme === "light" ? (
<SunIcon className="size-5 " />
) : (
<MoonIcon className="size-5 " />
)}
</Button>
</ClientOnly>
);
}
@@ -0,0 +1,27 @@
"use client";
import { useTheme } from "next-themes";
import { BuyWidget, type BuyWidgetProps } from "thirdweb/react";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getSDKTheme } from "@/utils/sdk-component-theme";
import { payLandingWallets } from "./wallets";
const client = getClientThirdwebClient();
export function StyledBuyWidget(
props: Omit<BuyWidgetProps, "client" | "theme">,
) {
const { theme } = useTheme();
return (
<BuyWidget
{...props}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
client={client}
className="shadow-xl"
connectOptions={{
wallets: payLandingWallets,
}}
/>
);
}
@@ -0,0 +1,13 @@
import { createWallet } from "thirdweb/wallets";
import { appMetadata } from "@/constants/connect";
export const payLandingWallets = [
createWallet("io.metamask"),
createWallet("com.coinbase.wallet", {
appMetadata,
}),
createWallet("me.rainbow"),
createWallet("io.rabby"),
createWallet("io.zerion.wallet"),
createWallet("com.okex.wallet"),
];
+2 -6
View File
@@ -18,15 +18,11 @@ export default async function PayLayout({
<html lang="en" suppressHydrationWarning>
<body
className={cn(
"h-screen w-screen bg-background font-sans antialiased",
"min-h-dvh w-screen bg-background font-sans antialiased flex flex-col",
fontSans.variable,
)}
>
<PayProviders>
<div className="relative mx-auto flex h-full w-full items-center justify-center overflow-x-hidden overflow-y-scroll">
{children}
</div>
</PayProviders>
<PayProviders>{children}</PayProviders>
</body>
</html>
);
+59 -88
View File
@@ -1,12 +1,9 @@
import { cn } from "@workspace/ui/lib/utils";
import type { Metadata } from "next";
import { ThemeProvider } from "next-themes";
import { createThirdwebClient, defineChain, getContract } from "thirdweb";
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
import { checksumAddress } from "thirdweb/utils";
import { PaymentLinkForm } from "./components/client/PaymentLinkForm.client";
import { PayPageWidget } from "./components/client/PayPageWidget.client";
import type { PayParams } from "./components/types";
import { payAppThirdwebClient } from "./constants";
import { defineChain, isAddress } from "thirdweb";
import { PayLandingHeader } from "./landing/header";
import { StyledBuyWidget } from "./landing/styled-buy-widget";
const title = "thirdweb Payments";
const description = "Fast, secure, and simple payments.";
@@ -20,97 +17,71 @@ export const metadata: Metadata = {
title,
};
export default async function PayPage({
searchParams,
}: {
searchParams: Promise<PayParams>;
type SearchParams = {
[key: string]: string | string[] | undefined;
};
export default async function PayPage(props: {
searchParams: Promise<SearchParams>;
}) {
const params = await searchParams;
const searchParams = await props.searchParams;
// If no query parameters are provided, show the form
if (
!params.chainId &&
!params.recipientAddress &&
!params.tokenAddress &&
!params.amount
) {
return (
<ThemeProvider
forcedTheme={params.theme === "light" ? "light" : "dark"}
attribute="class"
disableTransitionOnChange
enableSystem={false}
>
<PaymentLinkForm />
</ThemeProvider>
);
}
const onlyAddress = (v: string) => (isAddress(v) ? v : undefined);
const onlyNumber = (v: string) =>
Number.isNaN(Number(v)) ? undefined : Number(v);
// Validate query parameters
if (Array.isArray(params.chainId)) {
throw new Error("A single chainId parameter is required.");
}
if (Array.isArray(params.recipientAddress)) {
throw new Error("A single recipientAddress parameter is required.");
}
if (Array.isArray(params.tokenAddress)) {
throw new Error("A single tokenAddress parameter is required.");
}
if (Array.isArray(params.amount)) {
throw new Error("A single amount parameter is required.");
}
if (Array.isArray(params.clientId)) {
throw new Error("A single clientId parameter is required.");
}
if (Array.isArray(params.redirectUri)) {
throw new Error("A single redirectUri parameter is required.");
}
// Use any provided clientId or use the dashboard client
const client =
params.clientId && !Array.isArray(params.clientId)
? createThirdwebClient({ clientId: params.clientId })
: payAppThirdwebClient;
const tokenContract = getContract({
address: params.tokenAddress,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(Number(params.chainId)),
client: payAppThirdwebClient,
});
const {
symbol,
decimals,
name: tokenName,
} = await getCurrencyMetadata({
contract: tokenContract,
});
const token = {
address: checksumAddress(params.tokenAddress),
chainId: Number(params.chainId),
decimals,
name: tokenName,
symbol,
};
const receiver = parse(searchParams.receiver, onlyAddress);
const token = parse(searchParams.token, onlyAddress);
const chain = parse(searchParams.chain, onlyNumber);
const amount = parse(searchParams.amount, onlyNumber);
return (
<ThemeProvider
forcedTheme={params.theme === "light" ? "light" : "dark"}
attribute="class"
disableTransitionOnChange
enableSystem={false}
>
<PayPageWidget
amount={BigInt(params.amount)}
chainId={Number(params.chainId)}
clientId={client.clientId}
image={params.image}
name={params.name}
purchaseData={undefined}
recipientAddress={params.recipientAddress}
redirectUri={params.redirectUri}
token={token}
/>
<div className="flex flex-col grow">
<PayLandingHeader />
<div className="flex-1 flex items-center justify-center py-20 relative overflow-hidden">
<DotsBackgroundPattern />
<StyledBuyWidget
// eslint-disable-next-line no-restricted-syntax
chain={chain ? defineChain(chain) : undefined}
tokenAddress={token}
receiverAddress={receiver}
amount={amount ? amount.toString() : undefined}
/>
</div>
</div>
</ThemeProvider>
);
}
function parse<T>(
value: string | string[] | undefined,
fn: (value: string) => T | undefined,
): T | undefined {
if (typeof value === "string") {
return fn(value);
}
return undefined;
}
function DotsBackgroundPattern(props: { className?: string }) {
return (
<div
className={cn(
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/20 hidden lg:block",
props.className,
)}
style={{
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
backgroundSize: "24px 24px",
maskImage:
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
}}
/>
);
}
@@ -18,8 +18,7 @@ export async function GET(request: NextRequest) {
// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string;
const ENGINE_VAULT_ACCESS_TOKEN = process.env
.ENGINE_VAULT_ACCESS_TOKEN as string;
// const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
const API_URL = "http://localhost:3030";
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
const twFacilitator = facilitator({
baseUrl: `${API_URL}/v1/payments/x402`,
@@ -42,7 +41,7 @@ export async function GET(request: NextRequest) {
const amount = queryParams.get("amount") || "0.01";
const payTo = queryParams.get("payTo") ?? undefined;
const tokenAddress = queryParams.get("tokenAddress") || token.address;
const tokenAddress = queryParams.get("tokenAddress");
const decimals = queryParams.get("decimals") || token.decimals.toString();
const waitUntil =
(queryParams.get("waitUntil") as "simulated" | "submitted" | "confirmed") ||
@@ -54,13 +53,15 @@ export async function GET(request: NextRequest) {
paymentData,
network: defineChain(Number(chainId)),
payTo,
price: {
amount: toUnits(amount, parseInt(decimals)).toString(),
asset: {
address: tokenAddress as `0x${string}`,
decimals: decimals ? parseInt(decimals) : token.decimals,
},
},
price: tokenAddress
? {
amount: toUnits(amount, parseInt(decimals)).toString(),
asset: {
address: tokenAddress as `0x${string}`,
decimals: decimals ? parseInt(decimals) : token.decimals,
},
}
: amount,
routeConfig: {
description: "Access to paid content",
},
@@ -26,7 +26,7 @@ export default function Page() {
<PageLayout
icon={PlaneIcon}
description={description}
docsLink="https://thirdweb-engine.apidocumentation.com/reference#tag/erc20/POST/contract/{chain}/{contractAddress}/erc20/mint-batch-to?utm_source=playground"
docsLink="https://engine.thirdweb.com/reference"
title={title}
>
<EngineAirdropPreview />
@@ -26,7 +26,7 @@ export default function Page() {
<PageLayout
icon={StampIcon}
description={description}
docsLink="https://thirdweb-engine.apidocumentation.com/reference#tag/erc1155/POST/contract/{chain}/{contractAddress}/erc1155/mint-to?utm_source=playground"
docsLink="https://engine.thirdweb.com/reference"
title={title}
>
<EngineMintPreview />
+2 -2
View File
@@ -48,8 +48,8 @@ function Hero() {
<h1 className="mb-3 font-bold text-4xl tracking-tighter lg:text-6xl">
thirdweb Documentation
</h1>
<p className="mb-8 max-w-md text-lg text-muted-foreground leading-normal">
Development framework for building onchain apps, games, and agents.
<p className="mb-8 max-w-lg text-lg text-muted-foreground leading-normal text-pretty">
Platform for building the next generation of internet products
</p>
<div className="flex gap-3">
<ChatButton />
+42 -3
View File
@@ -63,10 +63,48 @@ export const config = createConfig({
});
```
Then in your application, you can use the connector to trigger the in-app wallet connection.
### Using the ConnectButton or ConnectEmbed with a wagmi application
You can use the thirdweb react connection components and hooks like ConnectButton, ConnectEmbed, useConnectModal, etc, but still keep using wagmi for the rest of the application.
To do so, you will need to connect the `inAppWalletConnector` connector from the `@thirdweb-dev/wagmi-adapter` package with the connected thirdweb wallet.
You can do this at any time after connecting the thirdweb wallet, a convenient place to do this is in the `onConnect` callback of the ConnectButton or ConnectEmbed.
```tsx
const { connectors, connect } = useConnect(); // from wagmi
<ConnectButton
client={client}
onConnect={(wallet) => {
// connect the wagmi connector with the connected thirdweb wallet
const twConnector = connectors.find(
(c) => c.id === "in-app-wallet",
);
if (twConnector) {
const options = {
wallet, // pass the connected wallet
} satisfies ConnectionOptions; // for type safety
connect({
connector: twConnector,
chainId: chain.id,
...options,
});
}
}}
/>
```
Make sure your app is wrapped in a `<ThirdwebProvider>` as well as a `<WagmiProvider>` since both contexts will be used here. From there, the wallet state will be in sync between the 2 libraries.
**Note:** the ConnectButton and ConnectEmbed handle reconnecting automatically on page reload. If not using those components (ie. useConnectModal or useConnect hooks directly), you will need to handle reconnecting by explicitely calling `useAutoConnect()`, and doing the same wagmi connection from the `onConnect` callback.
### Using the connector directly (headless mode)
You can also use the `inAppWalletConnector` connector directly in your application to connect to the thirdweb wallet without needing a `<ThirdwebProvider>` at all.
```ts
const { connect, connectors } = useConnect();
const { connect, connectors } = useConnect(); // from wagmi
const onClick = () => {
// grab the connector
@@ -79,9 +117,10 @@ const onClick = () => {
};
```
### Converting a wagmi wallet client to a thirdweb wallet
You can use the thirdweb SDK within a wagmi application by setting the wagmi connected account as the thirdweb 'active wallet'. After that, you can use all of the react components and hooks normally, including `BuyWidget`, `TransactionButton`, etc.
You can also use the thirdweb SDK within a wagmi application by setting the wagmi connected account as the thirdweb 'active wallet'. After that, you can use all of the react components and hooks normally, including `BuyWidget`, `TransactionButton`, etc.
```ts
// Assumes you've wrapped your application in a `<ThirdwebProvider>`
+48
View File
@@ -1,5 +1,53 @@
# wagmi-inapp
## 0.0.19
### Patch Changes
- Updated dependencies [[`6c318f8`](https://github.com/thirdweb-dev/js/commit/6c318f83d63aa8acce46da666e43638e410f799c), [`759b93b`](https://github.com/thirdweb-dev/js/commit/759b93beff7ab10a3a2fa3b9dd25ca628efb9267), [`8e357b3`](https://github.com/thirdweb-dev/js/commit/8e357b3cb39dcc7a8595a83224098a9c44585389)]:
- thirdweb@5.110.6
- @thirdweb-dev/wagmi-adapter@0.2.174
## 0.0.18
### Patch Changes
- Updated dependencies [[`c08fb13`](https://github.com/thirdweb-dev/js/commit/c08fb1356e77be9eeb0569905fcb2ab76a516dc3), [`bcbb3f5`](https://github.com/thirdweb-dev/js/commit/bcbb3f540a8eab2b02f9261b779bd2aad3464598), [`e0c8312`](https://github.com/thirdweb-dev/js/commit/e0c83121c201eb2dbdfa6a6b1bbb637f5ad2a21d)]:
- thirdweb@5.110.5
- @thirdweb-dev/wagmi-adapter@0.2.173
## 0.0.17
### Patch Changes
- Updated dependencies [[`23cb3bd`](https://github.com/thirdweb-dev/js/commit/23cb3bd98c5390e64f45da5a222042cdc76bd7f4), [`abf0bba`](https://github.com/thirdweb-dev/js/commit/abf0bbaf359c8de5b0ed0d481e3791c4b7cbf92b), [`e0abf2b`](https://github.com/thirdweb-dev/js/commit/e0abf2b2738661fda3c69551d78231cfac4b0767)]:
- thirdweb@5.110.4
- @thirdweb-dev/wagmi-adapter@0.2.172
## 0.0.16
### Patch Changes
- Updated dependencies [[`bf46520`](https://github.com/thirdweb-dev/js/commit/bf46520a6b183c4beb873de5e63853cd3a23e685)]:
- thirdweb@5.110.3
- @thirdweb-dev/wagmi-adapter@0.2.171
## 0.0.15
### Patch Changes
- Updated dependencies [[`3e9eb49`](https://github.com/thirdweb-dev/js/commit/3e9eb49787e00c09c3286aeb4ad73826a2856e65), [`bfd3be7`](https://github.com/thirdweb-dev/js/commit/bfd3be764c461c420f1f8520fe4d5d2cc83e42f7), [`ae3a122`](https://github.com/thirdweb-dev/js/commit/ae3a122cf1628f0fc997dd240d4b23842ac30f66), [`d5d5939`](https://github.com/thirdweb-dev/js/commit/d5d593956e12b2a15393ed32d1d742bc43575bdd), [`f757e60`](https://github.com/thirdweb-dev/js/commit/f757e6084648b6b6e5879abeab30954753a1020d)]:
- thirdweb@5.110.2
- @thirdweb-dev/wagmi-adapter@0.2.170
## 0.0.14
### Patch Changes
- Updated dependencies [[`85fe949`](https://github.com/thirdweb-dev/js/commit/85fe9491cd6d2270847e9941ddb7df76aefd8b31), [`e978809`](https://github.com/thirdweb-dev/js/commit/e9788096d665afef7c2a933cea5d6f522fc67273), [`899051f`](https://github.com/thirdweb-dev/js/commit/899051f788b0c2521a022846782892a8c1405a66)]:
- thirdweb@5.110.1
- @thirdweb-dev/wagmi-adapter@0.2.169
## 0.0.13
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wagmi-inapp",
"private": true,
"version": "0.0.13",
"version": "0.0.19",
"type": "module",
"scripts": {
"dev": "vite",
+43 -2
View File
@@ -2,11 +2,13 @@ import type { ConnectionOptions } from "@thirdweb-dev/wagmi-adapter";
import { ConnectButton } from "thirdweb/react";
import {
useAccount,
useCallsStatus,
useConnect,
useDisconnect,
useSendCalls,
useSendTransaction,
} from "wagmi";
import { chain, client } from "./wagmi.js";
import { chain, client, thirdwebChainForWallet, wallet } from "./wagmi.js";
function App() {
const account = useAccount();
@@ -20,6 +22,17 @@ function App() {
error: sendTxError,
data: sendTxData,
} = useSendTransaction();
const { sendCalls, data, isPending: isPendingSendCalls } = useSendCalls();
const {
data: callStatus,
isLoading: isLoadingCallStatus,
error: callStatusError,
} = useCallsStatus({
id: data?.id || "",
query: {
enabled: !!data?.id,
},
});
return (
<>
<div>
@@ -44,6 +57,8 @@ function App() {
<h2>Connect</h2>
<ConnectButton
client={client}
chain={thirdwebChainForWallet}
wallets={[wallet]}
onConnect={(wallet) => {
// auto connect to wagmi on tw connect
const twConnector = connectors.find(
@@ -95,7 +110,26 @@ function App() {
>
Send Tx
</button>
<div>{isPending ? "Sending..." : ""}</div>
<button
onClick={() =>
sendCalls({
calls: [
{
to: "0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
value: 0n,
},
],
})
}
type="button"
>
Send Calls
</button>
<div>
{isPending || isPendingSendCalls || isLoadingCallStatus
? "Sending..."
: ""}
</div>
<div>
{isSuccess
? `Success: ${sendTxData}`
@@ -103,6 +137,13 @@ function App() {
? sendTxError?.message
: ""}
</div>
<div>
{callStatus
? `Success: ${JSON.stringify(callStatus, null, 2)}`
: callStatusError
? callStatusError?.message
: ""}
</div>
</div>
)}
</>
+10 -2
View File
@@ -1,5 +1,6 @@
import { inAppWalletConnector } from "@thirdweb-dev/wagmi-adapter";
import { createThirdwebClient, defineChain as thirdwebChain } from "thirdweb";
import { inAppWallet } from "thirdweb/wallets/in-app";
import { createConfig, http } from "wagmi";
import { baseSepolia } from "wagmi/chains";
@@ -15,6 +16,13 @@ export const client = createThirdwebClient({
clientId,
});
export const wallet = inAppWallet({
executionMode: {
mode: "EIP7702",
sponsorGas: true,
},
});
export const chain = baseSepolia;
export const thirdwebChainForWallet = thirdwebChain(baseSepolia.id);
@@ -23,8 +31,8 @@ export const config = createConfig({
connectors: [
inAppWalletConnector({
client,
smartAccount: {
chain: thirdwebChain(chain.id),
executionMode: {
mode: "EIP7702",
sponsorGas: true,
},
}),
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/api
## 0.1.2
### Patch Changes
- [#8338](https://github.com/thirdweb-dev/js/pull/8338) [`90b7791`](https://github.com/thirdweb-dev/js/commit/90b779107d3cb726adeca6a3f885129c12599c6a) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - added solana token balances endpoint
## 0.1.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/api",
"version": "0.1.1",
"version": "0.1.2",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
+28
View File
@@ -66,6 +66,9 @@ import type {
GetSolanaTransactionData,
GetSolanaTransactionErrors,
GetSolanaTransactionResponses,
GetSolanaWalletBalanceData,
GetSolanaWalletBalanceErrors,
GetSolanaWalletBalanceResponses,
GetTokenOwnersData,
GetTokenOwnersErrors,
GetTokenOwnersResponses,
@@ -1590,6 +1593,31 @@ export const createSolanaWallet = <ThrowOnError extends boolean = false>(
});
};
/**
* Get Solana Wallet Balance
* Get the SOL or SPL token balance for a Solana wallet on a specific Solana network.
*
* **Authentication**: Pass `x-client-id` for frontend usage from allowlisted origins or `x-secret-key` for backend usage.
*/
export const getSolanaWalletBalance = <ThrowOnError extends boolean = false>(
options: Options<GetSolanaWalletBalanceData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetSolanaWalletBalanceResponses,
GetSolanaWalletBalanceErrors,
ThrowOnError
>({
security: [
{
name: "x-client-id",
type: "apiKey",
},
],
url: "/v1/solana/wallets/{address}/balance",
...options,
});
};
/**
* Sign Solana Message
* Sign an arbitrary message with a Solana wallet. Supports both text and hexadecimal message formats with automatic format detection.
+92 -83
View File
@@ -2135,6 +2135,10 @@ export type GetWalletTransactionsData = {
address: string;
};
query: {
/**
* Chain ID(s) to request transaction data for. You can specify multiple chain IDs by repeating the parameter, up to a maximum of 50. Example: ?chainId=1&chainId=137
*/
chainId: Array<number>;
/**
* Filter by block timestamp (Unix timestamp) greater than or equal to this value
*/
@@ -2171,10 +2175,6 @@ export type GetWalletTransactionsData = {
* Sort order: 'asc' for ascending, 'desc' for descending
*/
sortOrder?: "asc" | "desc";
/**
* Chain ID(s) to request transaction data for. You can specify multiple chain IDs by repeating the parameter, up to a maximum of 50. Example: ?chainId=1&chainId=137
*/
chainId: Array<number>;
};
url: "/v1/wallets/{address}/transactions";
};
@@ -3234,11 +3234,11 @@ export type WriteContractErrors = {
/**
* Payment ID
*/
id: string;
id?: string;
/**
* Bridge quote for completing the payment
*/
quote: {
quote?: {
/**
* Block number when quote was generated
*/
@@ -4314,11 +4314,11 @@ export type SendTransactionsErrors = {
/**
* Payment ID
*/
id: string;
id?: string;
/**
* Bridge quote for completing the payment
*/
quote: {
quote?: {
/**
* Block number when quote was generated
*/
@@ -4725,11 +4725,11 @@ export type PaymentsPurchaseErrors = {
/**
* Payment ID
*/
id: string;
id?: string;
/**
* Bridge quote for completing the payment
*/
quote: {
quote?: {
/**
* Block number when quote was generated
*/
@@ -4914,17 +4914,7 @@ export type VerifyX402PaymentData = {
paymentPayload: {
x402Version: number;
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
payload:
| {
signature: string;
@@ -4943,17 +4933,7 @@ export type VerifyX402PaymentData = {
};
paymentRequirements: {
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
maxAmountRequired: string;
resource: string;
description: string;
@@ -5015,7 +4995,6 @@ export type VerifyX402PaymentResponses = {
| "invalid_exact_svm_payload_transaction_instruction_not_spl_token_transfer_checked"
| "invalid_exact_svm_payload_transaction_instruction_not_token_2022_transfer_checked"
| "invalid_exact_svm_payload_transaction_not_a_transfer_instruction"
| "invalid_exact_svm_payload_transaction_cannot_derive_receiver_ata"
| "invalid_exact_svm_payload_transaction_receiver_ata_not_found"
| "invalid_exact_svm_payload_transaction_sender_ata_not_found"
| "invalid_exact_svm_payload_transaction_simulation_failed"
@@ -5049,17 +5028,7 @@ export type SettleX402PaymentData = {
paymentPayload: {
x402Version: number;
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
payload:
| {
signature: string;
@@ -5078,17 +5047,7 @@ export type SettleX402PaymentData = {
};
paymentRequirements: {
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
maxAmountRequired: string;
resource: string;
description: string;
@@ -5154,7 +5113,6 @@ export type SettleX402PaymentResponses = {
| "invalid_exact_svm_payload_transaction_instruction_not_spl_token_transfer_checked"
| "invalid_exact_svm_payload_transaction_instruction_not_token_2022_transfer_checked"
| "invalid_exact_svm_payload_transaction_not_a_transfer_instruction"
| "invalid_exact_svm_payload_transaction_cannot_derive_receiver_ata"
| "invalid_exact_svm_payload_transaction_receiver_ata_not_found"
| "invalid_exact_svm_payload_transaction_sender_ata_not_found"
| "invalid_exact_svm_payload_transaction_simulation_failed"
@@ -5183,7 +5141,10 @@ export type SettleX402PaymentResponses = {
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet";
| "sei-testnet"
| "polygon"
| "polygon-amoy"
| "peaq";
errorMessage?: string;
};
};
@@ -5226,17 +5187,7 @@ export type SupportedX402PaymentsResponses = {
kinds: Array<{
x402Version: 1;
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
extra?: {
defaultAsset?: {
/**
@@ -5340,11 +5291,11 @@ export type FetchWithPaymentErrors = {
/**
* Payment ID
*/
id: string;
id?: string;
/**
* Bridge quote for completing the payment
*/
quote: {
quote?: {
/**
* Block number when quote was generated
*/
@@ -5552,17 +5503,7 @@ export type ListPayableServicesResponses = {
x402Version: number;
accepts: Array<{
scheme: "exact";
network:
| "base-sepolia"
| "base"
| "avalanche-fuji"
| "avalanche"
| "iotex"
| "solana-devnet"
| "solana"
| "sei"
| "sei-testnet"
| string;
network: string;
maxAmountRequired: string;
resource: string;
description: string;
@@ -6298,11 +6239,11 @@ export type BridgeSwapErrors = {
/**
* Payment ID
*/
id: string;
id?: string;
/**
* Bridge quote for completing the payment
*/
quote: {
quote?: {
/**
* Block number when quote was generated
*/
@@ -6640,6 +6581,74 @@ export type CreateSolanaWalletResponses = {
export type CreateSolanaWalletResponse =
CreateSolanaWalletResponses[keyof CreateSolanaWalletResponses];
export type GetSolanaWalletBalanceData = {
body?: never;
path: {
/**
* Public key of the Solana wallet.
*/
address: string;
};
query: {
/**
* Solana network to query. Choose either solana:mainnet or solana:devnet.
*/
chainId: "solana:mainnet" | "solana:devnet";
/**
* SPL token mint address. Omit to retrieve native SOL balance.
*/
tokenAddress?: string;
};
url: "/v1/solana/wallets/{address}/balance";
};
export type GetSolanaWalletBalanceErrors = {
/**
* Invalid request parameters. This occurs when the wallet address or token mint is invalid, or the request mixes a token mint with multiple networks.
*/
400: unknown;
/**
* Authentication required. Include `x-client-id` or `x-secret-key` headers.
*/
401: unknown;
/**
* Internal server error. This may occur due to RPC connectivity issues or unexpected failures.
*/
500: unknown;
};
export type GetSolanaWalletBalanceResponses = {
/**
* Wallet balance retrieved successfully for the requested Solana network.
*/
200: {
/**
* Balance data for the requested Solana network.
*/
result: {
/**
* Requested Solana network.
*/
chainId: "solana:mainnet" | "solana:devnet";
/**
* Number of decimals used by the token.
*/
decimals: number;
/**
* Human-readable balance formatted using token decimals.
*/
displayValue: string;
/**
* Raw balance value expressed in base units (lamports).
*/
value: string;
};
};
};
export type GetSolanaWalletBalanceResponse =
GetSolanaWalletBalanceResponses[keyof GetSolanaWalletBalanceResponses];
export type SignSolanaMessageData = {
/**
* Request payload for signing an arbitrary Solana message.
+42
View File
@@ -1,5 +1,47 @@
# @thirdweb-dev/nebula
## 0.2.77
### Patch Changes
- Updated dependencies [[`6c318f8`](https://github.com/thirdweb-dev/js/commit/6c318f83d63aa8acce46da666e43638e410f799c), [`759b93b`](https://github.com/thirdweb-dev/js/commit/759b93beff7ab10a3a2fa3b9dd25ca628efb9267), [`8e357b3`](https://github.com/thirdweb-dev/js/commit/8e357b3cb39dcc7a8595a83224098a9c44585389)]:
- thirdweb@5.110.6
## 0.2.76
### Patch Changes
- Updated dependencies [[`c08fb13`](https://github.com/thirdweb-dev/js/commit/c08fb1356e77be9eeb0569905fcb2ab76a516dc3), [`bcbb3f5`](https://github.com/thirdweb-dev/js/commit/bcbb3f540a8eab2b02f9261b779bd2aad3464598), [`e0c8312`](https://github.com/thirdweb-dev/js/commit/e0c83121c201eb2dbdfa6a6b1bbb637f5ad2a21d)]:
- thirdweb@5.110.5
## 0.2.75
### Patch Changes
- Updated dependencies [[`23cb3bd`](https://github.com/thirdweb-dev/js/commit/23cb3bd98c5390e64f45da5a222042cdc76bd7f4), [`abf0bba`](https://github.com/thirdweb-dev/js/commit/abf0bbaf359c8de5b0ed0d481e3791c4b7cbf92b), [`e0abf2b`](https://github.com/thirdweb-dev/js/commit/e0abf2b2738661fda3c69551d78231cfac4b0767)]:
- thirdweb@5.110.4
## 0.2.74
### Patch Changes
- Updated dependencies [[`bf46520`](https://github.com/thirdweb-dev/js/commit/bf46520a6b183c4beb873de5e63853cd3a23e685)]:
- thirdweb@5.110.3
## 0.2.73
### Patch Changes
- Updated dependencies [[`3e9eb49`](https://github.com/thirdweb-dev/js/commit/3e9eb49787e00c09c3286aeb4ad73826a2856e65), [`bfd3be7`](https://github.com/thirdweb-dev/js/commit/bfd3be764c461c420f1f8520fe4d5d2cc83e42f7), [`ae3a122`](https://github.com/thirdweb-dev/js/commit/ae3a122cf1628f0fc997dd240d4b23842ac30f66), [`d5d5939`](https://github.com/thirdweb-dev/js/commit/d5d593956e12b2a15393ed32d1d742bc43575bdd), [`f757e60`](https://github.com/thirdweb-dev/js/commit/f757e6084648b6b6e5879abeab30954753a1020d)]:
- thirdweb@5.110.2
## 0.2.72
### Patch Changes
- Updated dependencies [[`85fe949`](https://github.com/thirdweb-dev/js/commit/85fe9491cd6d2270847e9941ddb7df76aefd8b31), [`e978809`](https://github.com/thirdweb-dev/js/commit/e9788096d665afef7c2a933cea5d6f522fc67273), [`899051f`](https://github.com/thirdweb-dev/js/commit/899051f788b0c2521a022846782892a8c1405a66)]:
- thirdweb@5.110.1
## 0.2.71
### Patch Changes
+1 -1
View File
@@ -57,5 +57,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.71"
"version": "0.2.77"
}
+13
View File
@@ -0,0 +1,13 @@
[
{
"import": "*",
"limit": "110 kB",
"name": "@thirdweb-dev/nexus (esm)",
"path": "./dist/esm/exports/nexus.js"
},
{
"limit": "350 kB",
"name": "@thirdweb-dev/nexus (cjs)",
"path": "./dist/cjs/exports/nexus.js"
}
]
+7
View File
@@ -0,0 +1,7 @@
# @thirdweb-dev/nexus
## 0.1.0
### Minor Changes
- [#8332](https://github.com/thirdweb-dev/js/pull/8332) [`8e357b3`](https://github.com/thirdweb-dev/js/commit/8e357b3cb39dcc7a8595a83224098a9c44585389) Thanks [@jnsdls](https://github.com/jnsdls)! - initial release
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"extends": "//",
"overrides": [
{
"assist": {
"actions": {
"source": {
"useSortedKeys": "off"
}
}
},
"includes": ["package.json"]
}
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["src/exports/**"],
"ignore": ["src/**/__generated__/**", "**/*.bench.ts"],
"ignoreBinaries": ["printf"],
"ignoreDependencies": ["tslib"],
"project": ["src/**/*.{ts,tsx}"],
"rules": {
"enumMembers": "off",
"optionalPeerDependencies": "off"
}
}
+79
View File
@@ -0,0 +1,79 @@
{
"author": "thirdweb eng <[email protected]>",
"browser": {
"crypto": false
},
"bugs": {
"url": "https://github.com/thirdweb-dev/js/issues"
},
"dependencies": {
"x402": "0.7.0",
"zod": "3.25.75"
},
"devDependencies": {
"@biomejs/biome": "2.0.6",
"@size-limit/preset-small-lib": "11.2.0",
"knip": "5.60.2",
"rimraf": "6.0.1",
"size-limit": "11.2.0",
"typescript": "5.8.3"
},
"engines": {
"node": ">=22"
},
"exports": {
".": {
"types": "./dist/types/exports/nexus.d.ts",
"import": "./dist/esm/exports/nexus.js",
"default": "./dist/cjs/exports/nexus.js"
}
},
"files": [
"dist/*",
"src/*",
"!**/*.tsbuildinfo",
"!**/*.test.ts",
"!**/*.test.tsx",
"!**/*.test.ts.snap",
"!**/*.test-d.ts",
"!**/*.bench.ts",
"!tsconfig.build.json"
],
"license": "Apache-2.0",
"main": "./dist/cjs/exports/nexus.js",
"module": "./dist/esm/exports/nexus.js",
"name": "@thirdweb-dev/nexus",
"peerDependencies": {
"typescript": ">=5.0.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
},
"scripts": {
"build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm",
"build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json",
"build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json",
"build:types": "tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
"clean": "rimraf dist",
"dev": "tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --outDir ./dist/esm --watch",
"dev:cjs": "printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json && tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false --watch",
"dev:esm": "printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json && tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm --watch",
"fix": "biome check ./src --fix",
"format": "biome format ./src --write",
"knip": "knip",
"lint": "knip && biome check ./src && tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --noEmit",
"size": "size-limit",
"typecheck": "tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --noEmit"
},
"sideEffects": false,
"type": "module",
"types": "./dist/types/exports/nexus.d.ts",
"typings": "./dist/types/exports/nexus.d.ts",
"version": "0.1.0"
}
+320
View File
@@ -0,0 +1,320 @@
import { ChainIdToNetwork, type Money, moneySchema } from "x402/types";
import { decodePayment } from "./encode.js";
import type { ThirdwebX402Facilitator } from "./facilitator.js";
import {
networkToChainId,
type RequestedPaymentPayload,
type RequestedPaymentRequirements,
} from "./schemas.js";
import {
type DefaultAsset,
type ERC20TokenAmount,
type PaymentArgs,
type PaymentRequiredResult,
type SupportedSignatureType,
x402Version,
} from "./types.js";
import { toUnits } from "./utils.js";
type GetPaymentRequirementsResult = {
status: 200;
paymentRequirements: RequestedPaymentRequirements[];
selectedPaymentRequirements: RequestedPaymentRequirements;
decodedPayment: RequestedPaymentPayload;
};
/**
* Decodes a payment request and returns the payment requirements, selected payment requirements, and decoded payment
* @param args
* @returns The payment requirements, selected payment requirements, and decoded payment
*/
export async function decodePaymentRequest(
args: PaymentArgs,
): Promise<GetPaymentRequirementsResult | PaymentRequiredResult> {
const {
price,
network,
facilitator,
payTo,
resourceUrl,
routeConfig = {},
method,
paymentData,
} = args;
const {
description,
mimeType,
maxTimeoutSeconds,
inputSchema,
outputSchema,
errorMessages,
discoverable,
} = routeConfig;
let chainId: number;
try {
chainId = networkToChainId(network);
} catch (error) {
return {
status: 402,
responseHeaders: { "Content-Type": "application/json" },
responseBody: {
x402Version,
error:
error instanceof Error
? error.message
: `Invalid network: ${network}`,
accepts: [],
},
};
}
const atomicAmountForAsset = await processPriceToAtomicAmount(
price,
chainId,
facilitator,
);
if ("error" in atomicAmountForAsset) {
return {
status: 402,
responseHeaders: { "Content-Type": "application/json" },
responseBody: {
x402Version,
error: atomicAmountForAsset.error,
accepts: [],
},
};
}
const { maxAmountRequired, asset } = atomicAmountForAsset;
const paymentRequirements: RequestedPaymentRequirements[] = [];
const mappedNetwork = ChainIdToNetwork[chainId];
paymentRequirements.push({
scheme: "exact",
network: mappedNetwork ? mappedNetwork : `eip155:${chainId}`,
maxAmountRequired,
resource: resourceUrl,
description: description ?? "",
mimeType: mimeType ?? "application/json",
payTo: facilitator.address as `0x${string}`, // always pay to the facilitator address first
maxTimeoutSeconds: maxTimeoutSeconds ?? 86400,
asset: asset.address as `0x${string}`,
outputSchema: {
input: {
type: "http",
method,
discoverable: discoverable ?? true,
...inputSchema,
},
output: outputSchema,
},
extra: {
recipientAddress: payTo, // input payTo is the final recipient address
...((asset as ERC20TokenAmount["asset"]).eip712 ?? {}),
},
});
// Check for payment header
if (!paymentData) {
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error: errorMessages?.paymentRequired || "X-PAYMENT header is required",
accepts: paymentRequirements,
},
};
}
// decode b64 payment
let decodedPayment: RequestedPaymentPayload;
try {
decodedPayment = decodePayment(paymentData);
decodedPayment.x402Version = x402Version;
} catch (error) {
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error:
errorMessages?.invalidPayment ||
(error instanceof Error ? error.message : "Invalid payment"),
accepts: paymentRequirements,
},
};
}
const selectedPaymentRequirements = paymentRequirements.find(
(value) =>
value.scheme === decodedPayment.scheme &&
networkToChainId(value.network) ===
networkToChainId(decodedPayment.network),
);
if (!selectedPaymentRequirements) {
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error:
errorMessages?.noMatchingRequirements ||
"Unable to find matching payment requirements",
accepts: paymentRequirements,
},
};
}
return {
status: 200,
paymentRequirements,
decodedPayment,
selectedPaymentRequirements,
};
}
/**
* Parses the amount from the given price
*
* @param price - The price to parse
* @param network - The network to get the default asset for
* @returns The parsed amount or an error message
*/
async function processPriceToAtomicAmount(
price: Money | ERC20TokenAmount,
chainId: number,
facilitator: ThirdwebX402Facilitator,
): Promise<
{ maxAmountRequired: string; asset: DefaultAsset } | { error: string }
> {
// Handle USDC amount (string) or token amount (ERC20TokenAmount)
let maxAmountRequired: string;
let asset: DefaultAsset;
if (typeof price === "string" || typeof price === "number") {
// USDC amount in dollars
const parsedAmount = moneySchema.safeParse(price);
if (!parsedAmount.success) {
return {
error: `Invalid price (price: ${price}). Must be in the form "$3.10", 0.10, "0.001", ${parsedAmount.error}`,
};
}
const parsedUsdAmount = parsedAmount.data;
const defaultAsset = await getDefaultAsset(chainId, facilitator);
if (!defaultAsset) {
return {
error: `Unable to get default asset on chain ${chainId}. Please specify an asset in the payment requirements.`,
};
}
asset = defaultAsset;
maxAmountRequired = toUnits(
parsedUsdAmount.toString(),
defaultAsset.decimals,
).toString();
} else {
// Token amount in atomic units
maxAmountRequired = price.amount;
const tokenExtras = await getOrDetectTokenExtras({
facilitator,
partialAsset: price.asset,
chainId,
});
if (!tokenExtras) {
return {
error: `Unable to find token information for ${price.asset.address} on chain ${chainId}. Please specify the asset decimals and eip712 information in the asset options.`,
};
}
asset = {
address: price.asset.address,
decimals: tokenExtras.decimals,
eip712: {
name: tokenExtras.name,
version: tokenExtras.version,
primaryType: tokenExtras.primaryType,
},
};
}
return {
maxAmountRequired,
asset,
};
}
async function getDefaultAsset(
chainId: number,
facilitator: ThirdwebX402Facilitator,
): Promise<DefaultAsset | undefined> {
const supportedAssets = await facilitator.supported();
const matchingAsset = supportedAssets.kinds.find(
(supported) => networkToChainId(supported.network) === chainId,
);
const assetConfig = matchingAsset?.extra?.defaultAsset as DefaultAsset;
return assetConfig;
}
async function getOrDetectTokenExtras(args: {
facilitator: ThirdwebX402Facilitator;
partialAsset: ERC20TokenAmount["asset"];
chainId: number;
}): Promise<
| {
name: string;
version: string;
decimals: number;
primaryType: SupportedSignatureType;
}
| undefined
> {
const { facilitator, partialAsset, chainId } = args;
if (
partialAsset.eip712?.name &&
partialAsset.eip712?.version &&
partialAsset.decimals !== undefined
) {
return {
name: partialAsset.eip712.name,
version: partialAsset.eip712.version,
decimals: partialAsset.decimals,
primaryType: partialAsset.eip712.primaryType,
};
}
// read from facilitator
const response = await facilitator
.supported({
chainId,
tokenAddress: partialAsset.address,
})
.catch(() => {
return {
kinds: [],
};
});
const exactScheme = response.kinds?.find((kind) => kind.scheme === "exact");
if (!exactScheme) {
return undefined;
}
const supportedAsset = exactScheme.extra?.supportedAssets?.find(
(asset) =>
asset.address.toLowerCase() === partialAsset.address.toLowerCase(),
);
if (!supportedAsset) {
return undefined;
}
return {
name: supportedAsset.eip712.name,
version: supportedAsset.eip712.version,
decimals: supportedAsset.decimals,
primaryType: supportedAsset.eip712.primaryType as SupportedSignatureType,
};
}
+81
View File
@@ -0,0 +1,81 @@
import type { ExactEvmPayload } from "x402/types";
import {
type RequestedPaymentPayload,
RequestedPaymentPayloadSchema,
} from "./schemas.js";
/**
* Encodes a payment payload into a base64 string, ensuring bigint values are properly stringified
*
* @param payment - The payment payload to encode
* @returns A base64 encoded string representation of the payment payload
*/
export function encodePayment(payment: RequestedPaymentPayload): string {
let safe: RequestedPaymentPayload;
// evm
const evmPayload = payment.payload as ExactEvmPayload;
safe = {
...payment,
payload: {
...evmPayload,
authorization: Object.fromEntries(
Object.entries(evmPayload.authorization).map(([key, value]) => [
key,
typeof value === "bigint" ? (value as bigint).toString() : value,
]),
) as ExactEvmPayload["authorization"],
},
};
return safeBase64Encode(JSON.stringify(safe));
}
/**
* Decodes a base64 encoded payment string back into a PaymentPayload object
*
* @param payment - The base64 encoded payment string to decode
* @returns The decoded and validated PaymentPayload object
*/
export function decodePayment(payment: string): RequestedPaymentPayload {
const decoded = safeBase64Decode(payment);
const parsed = JSON.parse(decoded);
const obj: RequestedPaymentPayload = {
...parsed,
payload: parsed.payload as ExactEvmPayload,
};
const validated = RequestedPaymentPayloadSchema.parse(obj);
return validated;
}
/**
* Encodes a string to base64 format
*
* @param data - The string to be encoded to base64
* @returns The base64 encoded string
*/
export function safeBase64Encode(data: string): string {
if (
typeof globalThis !== "undefined" &&
typeof globalThis.btoa === "function"
) {
return globalThis.btoa(data);
}
return Buffer.from(data).toString("base64");
}
/**
* Decodes a base64 string back to its original format
*
* @param data - The base64 encoded string to be decoded
* @returns The decoded string in UTF-8 format
*/
function safeBase64Decode(data: string): string {
if (
typeof globalThis !== "undefined" &&
typeof globalThis.atob === "function"
) {
return globalThis.atob(data);
}
return Buffer.from(data, "base64").toString("utf-8");
}
+24
View File
@@ -0,0 +1,24 @@
export type {
HTTPRequestStructure,
Money,
PaymentMiddlewareConfig,
Resource,
} from "x402/types";
export { decodePayment, encodePayment } from "../encode.js";
export {
createFacilitator,
type ThirdwebX402Facilitator,
type ThirdwebX402FacilitatorConfig,
type WaitUntil,
} from "../facilitator.js";
export { settlePayment } from "../settle-payment.js";
export type {
ERC20TokenAmount,
PaymentArgs,
PaymentRequiredResult,
SettlePaymentArgs,
SettlePaymentResult,
SupportedSignatureType,
VerifyPaymentResult,
} from "../types.js";
export { verifyPayment } from "../verify-payment.js";
+233
View File
@@ -0,0 +1,233 @@
import type { VerifyResponse } from "x402/types";
import type {
FacilitatorSettleResponse,
FacilitatorSupportedResponse,
FacilitatorVerifyResponse,
RequestedPaymentPayload,
RequestedPaymentRequirements,
} from "./schemas.js";
import { stringify } from "./utils.js";
export type WaitUntil = "simulated" | "submitted" | "confirmed";
export type ThirdwebX402FacilitatorConfig = {
walletSecret: string;
walletAddress: string;
waitUntil?: WaitUntil;
baseUrl?: string;
};
/**
* facilitator for the x402 payment protocol.
* @public
*/
export type ThirdwebX402Facilitator = {
url: `${string}://${string}`;
address: string;
createAuthHeaders: () => Promise<{
verify: Record<string, string>;
settle: Record<string, string>;
supported: Record<string, string>;
list: Record<string, string>;
}>;
verify: (
payload: RequestedPaymentPayload,
paymentRequirements: RequestedPaymentRequirements,
) => Promise<FacilitatorVerifyResponse>;
settle: (
payload: RequestedPaymentPayload,
paymentRequirements: RequestedPaymentRequirements,
waitUntil?: WaitUntil,
) => Promise<FacilitatorSettleResponse>;
supported: (filters?: {
chainId: number;
tokenAddress?: string;
}) => Promise<FacilitatorSupportedResponse>;
};
const DEFAULT_BASE_URL = "https://nexus-api.thirdweb.com";
/**
* Creates a facilitator for the x402 payment protocol.
* You can use this with `settlePayment` or with any x402 middleware to enable settling transactions with your thirdweb server wallet.
*
* @param config - The configuration for the facilitator
* @returns a x402 compatible FacilitatorConfig
*
* @example
* ```ts
* import { createFacilitator } from "@thirdweb-dev/nexus";
* import { paymentMiddleware } from 'x402-hono'
*
* const facilitator = createFacilitator({
* walletSecret: <your-wallet-secret>,
* walletAddress: <your-wallet-address>,
* });
*
* // add the facilitator to any x402 payment middleware
* const middleware = paymentMiddleware(
* facilitator.address,
* {
* "/api/paywall": {
* price: "$0.01",
* network: "base-sepolia",
* config: {
* description: "Access to paid content",
* },
* },
* },
* facilitator,
* );
* ```
*
* #### Configuration Options
*
* ```ts
* const thirdwebX402Facilitator = createFacilitator({
* walletSecret: <your-wallet-secret>,
* walletAddress: <your-wallet-address>,
* // Optional: Wait behavior for settlements
* // - "simulated": Only simulate the transaction (fastest)
* // - "submitted": Wait until transaction is submitted
* // - "confirmed": Wait for full on-chain confirmation (slowest, default)
* waitUntil: "confirmed",
* });
* ```
*
*/
export function createFacilitator(
config: ThirdwebX402FacilitatorConfig,
): ThirdwebX402Facilitator {
if (!config.walletSecret) {
throw new Error("Wallet secret is required for the x402 facilitator");
}
if (!config.walletAddress) {
throw new Error("Wallet address is required for the x402 facilitator");
}
const BASE_URL = config.baseUrl ?? DEFAULT_BASE_URL;
const AUTH_HEADERS = {
verify: {
authorization: `Bearer ${config.walletSecret}`,
},
settle: {
authorization: `Bearer ${config.walletSecret}`,
},
supported: {
authorization: `Bearer ${config.walletSecret}`,
},
list: {
authorization: `Bearer ${config.walletSecret}`,
},
} as const;
return {
url: BASE_URL as `${string}://${string}`,
address: config.walletAddress,
createAuthHeaders: async () => AUTH_HEADERS,
/**
* Verifies a payment payload with the facilitator service
*
* @param payload - The payment payload to verify
* @param paymentRequirements - The payment requirements to verify against
* @returns A promise that resolves to the verification response
*/
async verify(
payload: RequestedPaymentPayload,
paymentRequirements: RequestedPaymentRequirements,
): Promise<FacilitatorVerifyResponse> {
let headers = { "Content-Type": "application/json" };
headers = { ...headers, ...AUTH_HEADERS.verify };
const res = await fetch(new URL("/verify", BASE_URL), {
method: "POST",
headers,
body: stringify({
x402Version: payload.x402Version,
paymentPayload: payload,
paymentRequirements: paymentRequirements,
}),
});
if (res.status !== 200) {
const text = `${res.statusText} ${await res.text()}`;
throw new Error(`Failed to verify payment: ${res.status} ${text}`);
}
const data = await res.json();
return data as VerifyResponse;
},
/**
* Settles a payment with the facilitator service
*
* @param payload - The payment payload to settle
* @param paymentRequirements - The payment requirements for the settlement
* @returns A promise that resolves to the settlement response
*/
async settle(
payload: RequestedPaymentPayload,
paymentRequirements: RequestedPaymentRequirements,
waitUntil?: WaitUntil,
): Promise<FacilitatorSettleResponse> {
let headers = { "Content-Type": "application/json" };
headers = { ...headers, ...AUTH_HEADERS.settle };
const waitUntilParam = waitUntil || config.waitUntil;
const res = await fetch(new URL("/settle", BASE_URL), {
method: "POST",
headers,
body: stringify({
x402Version: payload.x402Version,
paymentPayload: payload,
paymentRequirements: paymentRequirements,
...(waitUntilParam ? { waitUntil: waitUntilParam } : {}),
}),
});
if (res.status !== 200) {
const text = `${res.statusText} ${await res.text()}`;
throw new Error(`Failed to settle payment: ${res.status} ${text}`);
}
const data = await res.json();
return data as FacilitatorSettleResponse;
},
/**
* Gets the supported payment kinds from the facilitator service.
*
* @returns A promise that resolves to the supported payment kinds
*/
async supported(filters?: {
chainId: number;
tokenAddress?: string;
}): Promise<FacilitatorSupportedResponse> {
const headers = {
"Content-Type": "application/json",
...AUTH_HEADERS.supported,
};
const supportedUrl = new URL("/supported", BASE_URL);
if (filters?.chainId) {
supportedUrl.searchParams.set("chainId", filters.chainId.toString());
}
if (filters?.tokenAddress) {
supportedUrl.searchParams.set("tokenAddress", filters.tokenAddress);
}
const res = await fetch(supportedUrl, { headers });
if (res.status !== 200) {
throw new Error(
`Failed to get supported payment kinds: ${res.statusText}`,
);
}
const data = await res.json();
return data as FacilitatorSupportedResponse;
},
} as const satisfies ThirdwebX402Facilitator;
}
+109
View File
@@ -0,0 +1,109 @@
import {
EvmNetworkToChainId,
type Network,
PaymentPayloadSchema,
PaymentRequirementsSchema,
SettleResponseSchema,
SupportedPaymentKindsResponseSchema,
VerifyResponseSchema,
} from "x402/types";
import { z } from "zod";
const FacilitatorNetworkSchema = z.string();
export type FacilitatorNetwork = z.infer<typeof FacilitatorNetworkSchema>;
export const RequestedPaymentPayloadSchema = PaymentPayloadSchema.extend({
network: FacilitatorNetworkSchema,
});
export type RequestedPaymentPayload = z.infer<
typeof RequestedPaymentPayloadSchema
>;
const RequestedPaymentRequirementsSchema = PaymentRequirementsSchema.extend({
network: FacilitatorNetworkSchema,
});
export type RequestedPaymentRequirements = z.infer<
typeof RequestedPaymentRequirementsSchema
>;
const FacilitatorSettleResponseSchema = SettleResponseSchema.extend({
network: FacilitatorNetworkSchema,
errorMessage: z.string().optional(),
});
export type FacilitatorSettleResponse = z.infer<
typeof FacilitatorSettleResponseSchema
>;
const FacilitatorVerifyResponseSchema = VerifyResponseSchema.extend({
errorMessage: z.string().optional(),
});
export type FacilitatorVerifyResponse = z.infer<
typeof FacilitatorVerifyResponseSchema
>;
export const SupportedSignatureTypeSchema = z.enum([
"TransferWithAuthorization",
"Permit",
]);
export const FacilitatorSupportedAssetSchema = z.object({
address: z.string(),
decimals: z.number(),
eip712: z.object({
name: z.string(),
version: z.string(),
primaryType: SupportedSignatureTypeSchema,
}),
});
const FacilitatorSupportedResponseSchema =
SupportedPaymentKindsResponseSchema.extend({
kinds: z.array(
z.object({
x402Version: z.literal(1),
scheme: z.literal("exact"),
network: FacilitatorNetworkSchema,
extra: z
.object({
defaultAsset: FacilitatorSupportedAssetSchema.optional(),
supportedAssets: z
.array(FacilitatorSupportedAssetSchema)
.optional(),
})
.optional(),
}),
),
}).describe("Supported payment kinds for this facilitator");
export type FacilitatorSupportedResponse = z.infer<
typeof FacilitatorSupportedResponseSchema
>;
export function networkToChainId(network: string): number {
if (network.startsWith("eip155:")) {
const chainId = parseInt(network.split(":")[1] ?? "0");
if (!Number.isNaN(chainId) && chainId > 0) {
return chainId;
} else {
throw new Error(`Invalid network: ${network}`);
}
}
// attempt to parse it as just an integer
const maybeChainId = parseInt(network);
if (!Number.isNaN(maybeChainId) && maybeChainId > 0) {
return maybeChainId;
}
const mappedChainId = EvmNetworkToChainId.get(network as Network);
if (!mappedChainId) {
throw new Error(`Invalid network: ${network}`);
}
// TODO (402): support solana networks
if (mappedChainId === 101 || mappedChainId === 103) {
throw new Error("Solana networks not supported yet.");
}
return mappedChainId;
}
+177
View File
@@ -0,0 +1,177 @@
import { decodePaymentRequest } from "./common.js";
import { safeBase64Encode } from "./encode.js";
import {
type SettlePaymentArgs,
type SettlePaymentResult,
x402Version,
} from "./types.js";
import { stringify } from "./utils.js";
/**
* Verifies and processes X402 payments for protected resources.
*
* This function implements the X402 payment protocol, verifying payment proofs
* and settling payments through a facilitator service. It handles the complete
* payment flow from validation to settlement.
*
* @param args - Configuration object containing payment verification parameters
* @returns A promise that resolves to either a successful payment result (200) or payment required error (402)
*
* @example
*
* ### Next.js API route example
*
* ```ts
* // Usage in a Next.js API route
* import { settlePayment, createFacilitator } from "@thirdweb-dev/nexus";
* import { arbitrumSepolia } from "thirdweb/chains";
*
* const facilitator = createFacilitator({
* walletSecret: <your-wallet-secret>,
* walletAddress: <your-wallet-address>,
* });
*
* export async function GET(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: "GET",
* paymentData,
* network: arbitrumSepolia, // or any other chain
* price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } }
* facilitator,
* routeConfig: {
* description: "Access to premium API content",
* mimeType: "application/json",
* maxTimeoutSeconds: 300,
* },
* });
*
* 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,
* });
* }
* }
* ```
*
* ### Express middleware example
*
* ```ts
* // Usage in Express middleware
* import express from "express";
* import { settlePayment, createFacilitator } from "@thirdweb-dev/nexus";
* import { arbitrumSepolia } from "thirdweb/chains";
*
* const facilitator = createFacilitator({
* walletSecret: <your-wallet-secret>,
* walletAddress: <your-wallet-address>,
* });
*
* const app = express();
*
* async function paymentMiddleware(req, res, next) {
* // verify and process the payment
* const result = await settlePayment({
* resourceUrl: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
* method: req.method,
* paymentData: req.headers["x-payment"],
* network: arbitrumSepolia, // or any other chain
* price: "$0.05",
* waitUntil: "submitted",
* facilitator,
* });
*
* if (result.status === 200) {
* // Set payment receipt headers and continue
* Object.entries(result.responseHeaders).forEach(([key, value]) => {
* res.setHeader(key, value);
* });
* next();
* } else {
* // Return payment required response
* res.status(result.status)
* .set(result.responseHeaders)
* .json(result.responseBody);
* }
* }
*
* app.get("/api/premium", paymentMiddleware, (req, res) => {
* res.json({ message: "This is premium content!" });
* });
* ```
*
* @public
* @beta
*/
export async function settlePayment(
args: SettlePaymentArgs,
): Promise<SettlePaymentResult> {
const { routeConfig = {}, facilitator } = args;
const { errorMessages } = routeConfig;
const decodePaymentResult = await decodePaymentRequest(args);
if (decodePaymentResult.status !== 200) {
return decodePaymentResult;
}
const { selectedPaymentRequirements, decodedPayment, paymentRequirements } =
decodePaymentResult;
try {
const settlement = await facilitator.settle(
decodedPayment,
selectedPaymentRequirements,
args.waitUntil,
);
if (settlement.success) {
return {
status: 200,
paymentReceipt: settlement,
responseHeaders: {
"Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE",
"X-PAYMENT-RESPONSE": safeBase64Encode(stringify(settlement)),
},
};
} else {
const error = settlement.errorReason || "Settlement error";
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error,
errorMessage:
errorMessages?.settlementFailed || settlement.errorMessage,
accepts: paymentRequirements,
},
};
}
} catch (error) {
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error: "Settlement error",
errorMessage:
errorMessages?.settlementFailed ||
(error instanceof Error ? error.message : undefined),
accepts: paymentRequirements,
},
};
}
}
+110
View File
@@ -0,0 +1,110 @@
import type { Money, PaymentMiddlewareConfig } from "x402/types";
import type z from "zod";
import type { ThirdwebX402Facilitator, WaitUntil } from "./facilitator.js";
import type {
FacilitatorNetwork,
FacilitatorSettleResponse,
FacilitatorSupportedAssetSchema,
RequestedPaymentPayload,
RequestedPaymentRequirements,
SupportedSignatureTypeSchema,
} from "./schemas.js";
export const x402Version = 1;
/**
* Configuration object for verifying or processing X402 payments.
*
* @public
*/
export type PaymentArgs = {
/** The URL of the resource being protected by the payment */
resourceUrl: string;
/** The HTTP method used to access the resource */
method: "GET" | "POST" | ({} & string);
/** The payment data/proof provided by the client, typically from the X-PAYMENT header */
paymentData?: string | null;
/** The blockchain network where the payment should be processed */
network: FacilitatorNetwork;
/** The price for accessing the resource - either a USD amount (e.g., "$0.10") or a specific token amount */
price: Money | ERC20TokenAmount;
/** The payment facilitator instance used to verify and settle payments */
facilitator: ThirdwebX402Facilitator;
/** Optional configuration for the payment middleware route */
routeConfig?: PaymentMiddlewareConfig;
/** Optional recipient address to receive the payment if different from your facilitator address */
payTo?: string;
};
export type SettlePaymentArgs = PaymentArgs & {
waitUntil?: WaitUntil;
};
export type PaymentRequiredResult = {
/** HTTP 402 - Payment Required, verification or processing failed or payment missing */
status: 402;
/** The error response body containing payment requirements */
responseBody: {
/** The X402 protocol version */
x402Version: number;
/** error code */
error: string;
/** Human-readable error message */
errorMessage?: string;
/** Array of acceptable payment methods and requirements */
accepts: RequestedPaymentRequirements[];
/** Optional payer address if verification partially succeeded */
payer?: string;
};
/** Response headers for the error response */
responseHeaders: Record<string, string>;
};
/**
* The result of a payment settlement operation.
*
* @public
*/
export type SettlePaymentResult =
| {
/** HTTP 200 - Payment was successfully processed */
status: 200;
/** Response headers including payment receipt information */
responseHeaders: Record<string, string>;
/** The payment receipt from the payment facilitator */
paymentReceipt: FacilitatorSettleResponse;
}
| PaymentRequiredResult;
/**
* The result of a payment verification operation.
*
* @public
*/
export type VerifyPaymentResult =
| {
/** HTTP 200 - Payment was successfully verified */
status: 200;
decodedPayment: RequestedPaymentPayload;
selectedPaymentRequirements: RequestedPaymentRequirements;
}
| PaymentRequiredResult;
export type SupportedSignatureType = z.infer<
typeof SupportedSignatureTypeSchema
>;
export type ERC20TokenAmount = {
amount: string;
asset: {
address: `0x${string}`;
decimals?: number;
eip712?: {
name: string;
version: string;
primaryType: SupportedSignatureType;
};
};
};
export type DefaultAsset = z.infer<typeof FacilitatorSupportedAssetSchema>;
+93
View File
@@ -0,0 +1,93 @@
/**
* Stringify a JSON object and convert all bigint values to string
*
* If you are getting this error: "Exception: Do not know how to serialize a BigInt",
* you probably can use this function to parse the data.
* Because bigint is not an accepted value of the JSON format.
*
* @returns An object with all bigint values converted to string
* @example
* ```ts
* import { stringify } from "thirdweb/utils";
* const obj = { tokenId: 0n };
* const str = stringify(obj); // "{"tokenId":"0"}"
* ```
* @utils
*/
export function stringify(
// biome-ignore lint/suspicious/noExplicitAny: JSON.stringify signature
value: any,
// biome-ignore lint/suspicious/noExplicitAny: JSON.stringify signature
replacer?: ((this: any, key: string, value: any) => any) | null,
space?: string | number,
) {
const res = JSON.stringify(
value,
(key, value_) => {
const value__ = typeof value_ === "bigint" ? value_.toString() : value_;
return typeof replacer === "function" ? replacer(key, value__) : value__;
},
space,
);
return res;
}
/**
* Converts a string representation of a number with decimal places to a BigInt representation.
* @param tokens - The string representation of the number, including the integer and fraction parts.
* @param decimals - The number of decimal places to include in the BigInt representation.
* @returns The BigInt representation of the number.
* @example
* ```ts
* import { toUnits } from "thirdweb/utils";
* toUnits('1', 18)
* // 1000000000000000000n
* ```
* @utils
*/
export function toUnits(tokens: string, decimals: number): bigint {
if (tokens.includes("e")) {
tokens = Number(tokens).toFixed(decimals);
}
let [integerPart, fractionPart = ""] = tokens.split(".") as [string, string];
const prefix = integerPart.startsWith("-") ? "-" : "";
if (prefix) {
integerPart = integerPart.slice(1);
}
fractionPart = fractionPart.padEnd(decimals, "0"); // Ensure fraction part is at least 'decimals' long.
if (decimals === 0) {
// Check if there's any fraction part that would necessitate rounding up the integer part.
if (fractionPart[0] && Number.parseInt(fractionPart[0]) >= 5) {
integerPart = (BigInt(integerPart) + 1n).toString();
}
fractionPart = ""; // No fraction part is needed when decimals === 0.
} else {
// When decimals > 0, handle potential rounding based on the digit right after the specified decimal places.
if (fractionPart.length > decimals) {
const roundingDigit = fractionPart[decimals];
if (roundingDigit && Number.parseInt(roundingDigit, 10) >= 5) {
// If rounding is needed, add 1 to the last included digit of the fraction part.
const roundedFraction =
BigInt(fractionPart.substring(0, decimals)) + 1n;
fractionPart = roundedFraction.toString().padStart(decimals, "0");
if (fractionPart.length > decimals) {
// If rounding the fraction results in a length increase (e.g., .999 -> 1.000), increment the integer part.
integerPart = (BigInt(integerPart) + 1n).toString();
// Adjust the fraction part if it's longer than the specified decimals due to rounding up.
fractionPart = fractionPart.substring(fractionPart.length - decimals);
}
} else {
// If no rounding is necessary, just truncate the fraction part to the specified number of decimals.
fractionPart = fractionPart.substring(0, decimals);
}
}
// If the fraction part is shorter than the specified decimals, it's already handled by padEnd() above.
}
// Combine the integer and fraction parts into the final BigInt representation.
return BigInt(`${prefix}${integerPart}${fractionPart}`);
}
+128
View File
@@ -0,0 +1,128 @@
import { decodePaymentRequest } from "./common.js";
import {
type PaymentArgs,
type VerifyPaymentResult,
x402Version,
} from "./types.js";
/**
* Verifies X402 payments for protected resources. This function only verifies the payment,
* you should use `settlePayment` to settle the payment.
*
* @param args - Configuration object containing payment verification parameters
* @returns A promise that resolves to either a successful verification result (200) or payment required error (402)
*
* @example
* ```ts
* // Usage in a Next.js API route
* import { verifyPayment, createFacilitator } from "@thirdweb-dev/nexus";
* import { arbitrumSepolia } from "thirdweb/chains";
*
* const facilitator = createFacilitator({
* walletSecret: <your-wallet-secret>,
* walletAddress: <your-wallet-address>,
* });
*
* export async function GET(request: Request) {
* const paymentData = request.headers.get("x-payment");
*
* const paymentArgs = {
* resourceUrl: "https://api.example.com/premium-content",
* method: "GET",
* paymentData,
* network: arbitrumSepolia, // or any other chain
* price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } }
* facilitator,
* routeConfig: {
* description: "Access to premium API content",
* mimeType: "application/json",
* maxTimeoutSeconds: 300,
* },
* };
*
* // verify the payment
* const result = await verifyPayment(paymentArgs);
*
* if (result.status === 200) {
* // Payment verified, but not settled yet
* // you can do the work that requires payment first
* const result = await doSomething();
* // then settle the payment
* const settleResult = await settlePayment(paymentArgs);
*
* // then return the result
* return Response.json(result);
* } else {
* // verification failed, return payment required
* return Response.json(result.responseBody, {
* status: result.status,
* headers: result.responseHeaders,
* });
* }
* }
* ```
*
* @public
* @beta
*/
export async function verifyPayment(
args: PaymentArgs,
): Promise<VerifyPaymentResult> {
const { routeConfig = {}, facilitator } = args;
const { errorMessages } = routeConfig;
const decodePaymentResult = await decodePaymentRequest(args);
if (decodePaymentResult.status !== 200) {
return decodePaymentResult;
}
const { selectedPaymentRequirements, decodedPayment, paymentRequirements } =
decodePaymentResult;
// Verify payment
try {
const verification = await facilitator.verify(
decodedPayment,
selectedPaymentRequirements,
);
if (verification.isValid) {
return {
status: 200,
decodedPayment,
selectedPaymentRequirements,
};
} else {
const error = verification.invalidReason || "Verification failed";
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error: error,
errorMessage:
errorMessages?.verificationFailed || verification.errorMessage,
accepts: paymentRequirements,
},
};
}
} catch (error) {
return {
status: 402,
responseHeaders: {
"Content-Type": "application/json",
},
responseBody: {
x402Version,
error: "Verification error",
errorMessage:
errorMessages?.verificationFailed ||
(error instanceof Error ? error.message : undefined),
accepts: paymentRequirements,
},
};
}
}
+48
View File
@@ -0,0 +1,48 @@
{
// This tsconfig file contains the shared config for the build (tsconfig.build.json) and type checking (tsconfig.json) config.
"compilerOptions": {
// Incremental builds
// NOTE: Enabling incremental builds speeds up `tsc`. Keep in mind though that it does not reliably bust the cache when the `tsconfig.json` file changes.
"allowJs": false,
"allowSyntheticDefaultImports": true,
"checkJs": false,
// Interop constraints
"esModuleInterop": false,
"exactOptionalPropertyTypes": false,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
// Incremental builds
// NOTE: Enabling incremental builds speeds up `tsc`. Keep in mind though that it does not reliably bust the cache when the `tsconfig.json` file changes.
"incremental": false,
// jsx for "/react" portion
"jsx": "react-jsx",
"lib": [
"ES2022", // By using ES2022 we get access to the `.cause` property on `Error` instances.
"DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped.
],
"module": "NodeNext",
// Language and environment
"moduleResolution": "NodeNext",
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
// Skip type checking for node modules
"skipLibCheck": true,
// Type checking
"strict": true,
"target": "ES2021",
"useDefineForClassFields": true,
"useUnknownInCatchVariables": true,
"verbatimModuleSyntax": true
},
// This tsconfig file contains the shared config for the build (tsconfig.build.json) and type checking (tsconfig.json) config.
"include": []
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"moduleResolution": "node",
"rootDir": "./src",
"sourceMap": true
},
"exclude": [
"src/**/*.test.ts",
"src/**/*.test.tsx",
"src/**/*.test-d.ts",
"src/**/*.bench.ts",
"src/**/*.macro.ts"
],
"extends": "./tsconfig.base.json",
"include": ["src"]
}
+13
View File
@@ -0,0 +1,13 @@
{
// This configuration is used for local development and type checking.
"compilerOptions": {
"baseUrl": ".",
"outDir": "./dist",
"paths": {
"~test/*": ["./test/src/*"]
}
},
"exclude": [],
"extends": "./tsconfig.base.json",
"include": ["src", "test"]
}
+1 -1
View File
@@ -6,7 +6,7 @@
"path": "./dist/esm/exports/thirdweb.js"
},
{
"limit": "375 kB",
"limit": "380 kB",
"name": "thirdweb (cjs)",
"path": "./dist/cjs/exports/thirdweb.js"
},
+60
View File
@@ -1,5 +1,65 @@
# thirdweb
## 5.110.6
### Patch Changes
- [#8334](https://github.com/thirdweb-dev/js/pull/8334) [`6c318f8`](https://github.com/thirdweb-dev/js/commit/6c318f83d63aa8acce46da666e43638e410f799c) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Update implementations
- [#8306](https://github.com/thirdweb-dev/js/pull/8306) [`759b93b`](https://github.com/thirdweb-dev/js/commit/759b93beff7ab10a3a2fa3b9dd25ca628efb9267) Thanks [@alecananian](https://github.com/alecananian)! - Moved `@storybook/react` package to be a dev dependency
- [#8332](https://github.com/thirdweb-dev/js/pull/8332) [`8e357b3`](https://github.com/thirdweb-dev/js/commit/8e357b3cb39dcc7a8595a83224098a9c44585389) Thanks [@jnsdls](https://github.com/jnsdls)! - expose some useful erc20 extensions
## 5.110.5
### Patch Changes
- [#8327](https://github.com/thirdweb-dev/js/pull/8327) [`c08fb13`](https://github.com/thirdweb-dev/js/commit/c08fb1356e77be9eeb0569905fcb2ab76a516dc3) Thanks [@MananTank](https://github.com/MananTank)! - Fix Safe wallet connection issues with WalletConnect
- [#8331](https://github.com/thirdweb-dev/js/pull/8331) [`bcbb3f5`](https://github.com/thirdweb-dev/js/commit/bcbb3f540a8eab2b02f9261b779bd2aad3464598) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Loosen network schema
- [#8325](https://github.com/thirdweb-dev/js/pull/8325) [`e0c8312`](https://github.com/thirdweb-dev/js/commit/e0c83121c201eb2dbdfa6a6b1bbb637f5ad2a21d) Thanks [@Yash094](https://github.com/Yash094)! - Add Arc testnet to chains package
## 5.110.4
### Patch Changes
- [#8310](https://github.com/thirdweb-dev/js/pull/8310) [`23cb3bd`](https://github.com/thirdweb-dev/js/commit/23cb3bd98c5390e64f45da5a222042cdc76bd7f4) Thanks [@MananTank](https://github.com/MananTank)! - Minor UI adjustments in CheckoutWidget
- [#8317](https://github.com/thirdweb-dev/js/pull/8317) [`abf0bba`](https://github.com/thirdweb-dev/js/commit/abf0bbaf359c8de5b0ed0d481e3791c4b7cbf92b) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - updates default x402 transfer with authorization timeout
- [#8319](https://github.com/thirdweb-dev/js/pull/8319) [`e0abf2b`](https://github.com/thirdweb-dev/js/commit/e0abf2b2738661fda3c69551d78231cfac4b0767) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update x402 types
## 5.110.3
### Patch Changes
- [#8314](https://github.com/thirdweb-dev/js/pull/8314) [`bf46520`](https://github.com/thirdweb-dev/js/commit/bf46520a6b183c4beb873de5e63853cd3a23e685) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Enhance compatiblity with third party x402 libs
## 5.110.2
### Patch Changes
- [#8304](https://github.com/thirdweb-dev/js/pull/8304) [`3e9eb49`](https://github.com/thirdweb-dev/js/commit/3e9eb49787e00c09c3286aeb4ad73826a2856e65) Thanks [@MananTank](https://github.com/MananTank)! - Fix Select Token UI stuck in loading state if wallet is connected to a chain that is not supported by thirdweb Bridge in BuyWidget, SwapWidget and BridgeWidget
- [#8311](https://github.com/thirdweb-dev/js/pull/8311) [`bfd3be7`](https://github.com/thirdweb-dev/js/commit/bfd3be764c461c420f1f8520fe4d5d2cc83e42f7) Thanks [@jnsdls](https://github.com/jnsdls)! - filter out null-ish values from `ERC721.getNFTs()` and `ERC1155.getNFTs()` arrays
- [#8313](https://github.com/thirdweb-dev/js/pull/8313) [`ae3a122`](https://github.com/thirdweb-dev/js/commit/ae3a122cf1628f0fc997dd240d4b23842ac30f66) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix network comparison when using diff libraries
- [#8301](https://github.com/thirdweb-dev/js/pull/8301) [`d5d5939`](https://github.com/thirdweb-dev/js/commit/d5d593956e12b2a15393ed32d1d742bc43575bdd) Thanks [@kumaryash90](https://github.com/kumaryash90)! - create2 factory for neura testnet
- [#8305](https://github.com/thirdweb-dev/js/pull/8305) [`f757e60`](https://github.com/thirdweb-dev/js/commit/f757e6084648b6b6e5879abeab30954753a1020d) Thanks [@MananTank](https://github.com/MananTank)! - Update theme colors and minor UI tweaks in React Components
## 5.110.1
### Patch Changes
- [#8299](https://github.com/thirdweb-dev/js/pull/8299) [`85fe949`](https://github.com/thirdweb-dev/js/commit/85fe9491cd6d2270847e9941ddb7df76aefd8b31) Thanks [@alecananian](https://github.com/alecananian)! - [React Native] Fixed Send Funds screen not allowing decimal amounts
- [#8296](https://github.com/thirdweb-dev/js/pull/8296) [`e978809`](https://github.com/thirdweb-dev/js/commit/e9788096d665afef7c2a933cea5d6f522fc67273) Thanks [@MananTank](https://github.com/MananTank)! - Do not require connecting wallet in `BuyWidget` if `receiverAddress` is set
- [#8298](https://github.com/thirdweb-dev/js/pull/8298) [`899051f`](https://github.com/thirdweb-dev/js/commit/899051f788b0c2521a022846782892a8c1405a66) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add EIP-5792 support for EIP1193.toProvider()
## 5.110.0
### Minor Changes
+3 -3
View File
@@ -21,7 +21,6 @@
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-tooltip": "1.2.7",
"@storybook/react": "9.0.15",
"@tanstack/react-query": "5.81.5",
"@thirdweb-dev/engine": "workspace:*",
"@thirdweb-dev/insight": "workspace:*",
@@ -40,7 +39,7 @@
"toml": "3.0.0",
"uqr": "0.1.2",
"viem": "2.37.9",
"x402": "0.6.1",
"x402": "0.7.0",
"zod": "3.25.75"
},
"devDependencies": {
@@ -57,6 +56,7 @@
"@storybook/addon-docs": "9.0.15",
"@storybook/addon-links": "9.0.15",
"@storybook/addon-onboarding": "9.0.15",
"@storybook/react": "9.0.15",
"@storybook/react-vite": "9.0.15",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -430,5 +430,5 @@
}
},
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "5.110.0"
"version": "5.110.6"
}
@@ -64,100 +64,138 @@ export function toProvider(options: ToEip1193ProviderOptions): EIP1193Provider {
// should invoke the return fn from subscribe instead
},
request: async (request) => {
if (request.method === "eth_sendTransaction") {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
switch (request.method) {
case "eth_sendTransaction": {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
}
const result = await sendTransaction({
account: account,
transaction: prepareTransaction({
...request.params[0],
chain,
client,
}),
});
return result.transactionHash;
}
const result = await sendTransaction({
account: account,
transaction: prepareTransaction({
case "eth_estimateGas": {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
}
return estimateGas({
account,
transaction: prepareTransaction({
...request.params[0],
chain,
client,
}),
});
}
case "personal_sign": {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
}
return account.signMessage({
message: {
raw: request.params[0],
},
});
}
case "eth_signTypedData_v4": {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
}
const data = JSON.parse(request.params[1]);
return account.signTypedData(data);
}
case "eth_accounts": {
const account = wallet.getAccount();
if (!account) {
return [];
}
return [account.address];
}
case "eth_requestAccounts": {
const connectedAccount = wallet.getAccount();
if (connectedAccount) {
return [connectedAccount.address];
}
const account = connectOverride
? await connectOverride(wallet)
: await wallet
.connect({
client,
})
.catch((e) => {
console.error("Error connecting wallet", e);
return null;
});
if (!account) {
throw new Error(
"Unable to connect wallet - try passing a connectOverride function",
);
}
return [account.address];
}
case "wallet_switchEthereumChain":
case "wallet_addEthereumChain": {
const data = request.params[0];
const chainIdHex = data.chainId;
if (!chainIdHex) {
throw new Error("Chain ID is required");
}
// chainId is hex most likely, convert to number
const chainId = isHex(chainIdHex)
? hexToNumber(chainIdHex)
: chainIdHex;
const chain = getCachedChain(chainId);
return wallet.switchChain(chain);
}
case "wallet_getCapabilities": {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
}
if (!account.getCapabilities) {
throw new Error("Wallet does not support EIP-5792");
}
return account.getCapabilities({ chainId: chain.id });
}
case "wallet_sendCalls": {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
}
if (!account.sendCalls) {
throw new Error("Wallet does not support EIP-5792");
}
return account.sendCalls({
...request.params[0],
chain,
client,
}),
});
return result.transactionHash;
}
if (request.method === "eth_estimateGas") {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
chain: chain,
});
}
return estimateGas({
account,
transaction: prepareTransaction({
...request.params[0],
chain,
client,
}),
});
}
if (request.method === "personal_sign") {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
case "wallet_getCallsStatus": {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
}
if (!account.getCallsStatus) {
throw new Error("Wallet does not support EIP-5792");
}
return account.getCallsStatus({
id: request.params[0],
chain: chain,
client: client,
});
}
return account.signMessage({
message: {
raw: request.params[0],
},
});
default:
return rpcClient(request);
}
if (request.method === "eth_signTypedData_v4") {
const account = wallet.getAccount();
if (!account) {
throw new Error("Account not connected");
}
const data = JSON.parse(request.params[1]);
return account.signTypedData(data);
}
if (request.method === "eth_accounts") {
const account = wallet.getAccount();
if (!account) {
return [];
}
return [account.address];
}
if (request.method === "eth_requestAccounts") {
const connectedAccount = wallet.getAccount();
if (connectedAccount) {
return [connectedAccount.address];
}
const account = connectOverride
? await connectOverride(wallet)
: await wallet
.connect({
client,
})
.catch((e) => {
console.error("Error connecting wallet", e);
return null;
});
if (!account) {
throw new Error(
"Unable to connect wallet - try passing a connectOverride function",
);
}
return [account.address];
}
if (
request.method === "wallet_switchEthereumChain" ||
request.method === "wallet_addEthereumChain"
) {
const data = request.params[0];
const chainIdHex = data.chainId;
if (!chainIdHex) {
throw new Error("Chain ID is required");
}
// chainId is hex most likely, convert to number
const chainId = isHex(chainIdHex)
? hexToNumber(chainIdHex)
: chainIdHex;
const chain = getCachedChain(chainId);
return wallet.switchChain(chain);
}
return rpcClient(request);
},
};
}
@@ -0,0 +1,17 @@
import { defineChain } from "../utils.js";
/**
* @chain
*/
export const arcTestnet = /* @__PURE__ */ defineChain({
blockExplorers: [
{
name: "Arc Testnet Explorer",
url: "https://testnet.arcscan.app",
},
],
id: 5042002,
name: "Arc Testnet",
nativeCurrency: { decimals: 6, name: "USDC", symbol: "USDC" },
testnet: true,
});
@@ -321,6 +321,7 @@ const CUSTOM_GAS_FOR_CHAIN: Record<string, CustomChain> = {
const FACTORIES: Record<string, string> = {
"88888": "0xc501b9abf5540de1dd24f66633b1ecf35ff7101f",
"267": "0x4e59b44847b379578588920cA78FbF26c0B4956C",
"420120000": COMMON_FACTORY_ADDRESS,
"420120001": COMMON_FACTORY_ADDRESS, // EIP155 is enforced, but the check fails, hence we hardcode the address here instead of computing dynamically
};
@@ -1,6 +1,6 @@
export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
232: {
MarketplaceV3: "0x9742f5ac11958cFAd151eBF0Fc31302fA409036E",
MarketplaceV3: "0xB7387cF425eC8Da1b04b8E34De86245cb32bCA6D",
},
300: {
MarketplaceV3: "0x58e0F289C7dD2025eBd0696d913ECC0fdc1CC8bc",
@@ -18,7 +18,7 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
MarketplaceV3: "0x2dA4Dd326A6482679547071be21f74685d730504",
},
37111: {
MarketplaceV3: "0x56Abb6a3f25DCcdaDa106191053b1CC54C196DEE",
MarketplaceV3: "0xF8fd1016420c2B7832714241d36Efe55D41df126",
},
50104: {
MarketplaceV3: "0x292c324920d4120F928dA6f04548442fa10B8B0f",
@@ -32,12 +32,12 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
};
export const ZKSYNC_WETH: Record<number, string> = {
232: "0xE5ecd226b3032910CEaa43ba92EE8232f8237553",
232: "0x6bDc36E20D267Ff0dd6097799f82e78907105e2F",
300: "0x0462C05457Fed440740Ff3696bDd2D0577411e34",
324: "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91",
2741: "0x3439153EB7AF838Ad19d56E1571FBD09333C2809",
11124: "0x9EDCde0257F2386Ce177C3a7FCdd97787F0D841d",
37111: "0xaA91D645D7a6C1aeaa5988e0547267B77d33fe16",
37111: "0xeee5a340Cdc9c179Db25dea45AcfD5FE8d4d3eB8",
50104: "0x2b1A859dE6A55c553520D7780Bc5805712B128F9",
555271: "0xb0b8b267d44c64BA6dD1Daf442949887c85199f6",
531050104: "0x9B7EA63EF5062661570afe548e3AD691D5652527",
+1
View File
@@ -10,6 +10,7 @@ export { anvil } from "../chains/chain-definitions/anvil.js";
export { arbitrum } from "../chains/chain-definitions/arbitrum.js";
export { arbitrumNova } from "../chains/chain-definitions/arbitrum-nova.js";
export { arbitrumSepolia } from "../chains/chain-definitions/arbitrum-sepolia.js";
export { arcTestnet } from "../chains/chain-definitions/arc-testnet.js";
export { assetChainTestnet } from "../chains/chain-definitions/assetchain-testnet.js";
export { astriaEvmDusknet } from "../chains/chain-definitions/astria-evm-dusknet.js";
export { avalanche } from "../chains/chain-definitions/avalanche.js";
@@ -1,3 +1,5 @@
export type { Abi } from "abitype";
export { formatCompilerMetadata } from "../contract/actions/compiler-metadata.js";
export { getBytecode } from "../contract/actions/get-bytecode.js";
export { getCompilerMetadata } from "../contract/actions/get-compiler-metadata.js";
@@ -17,10 +17,6 @@ export {
getActiveClaimConditionId,
isGetActiveClaimConditionIdSupported,
} from "../../extensions/erc20/__generated__/IDropERC20/read/getActiveClaimConditionId.js";
/**
* DROPS extension for ERC20
*/
// READ
export {
getClaimConditionById,
isGetClaimConditionByIdSupported,
@@ -43,6 +39,15 @@ export {
balanceOf,
} from "../../extensions/erc20/__generated__/IERC20/read/balanceOf.js";
export { totalSupply } from "../../extensions/erc20/__generated__/IERC20/read/totalSupply.js";
/**
* PERMIT extension for ERC20
*/
export { nonces } from "../../extensions/erc20/__generated__/IERC20Permit/read/nonces.js";
export { isPermitSupported } from "../../extensions/erc20/__generated__/IERC20Permit/write/permit.js";
/**
* DROPS extension for ERC20
*/
// READ
export {
type TokensMintedEventFilters,
tokensMintedEvent,
@@ -63,6 +68,7 @@ export {
type WithdrawParams,
withdraw,
} from "../../extensions/erc20/__generated__/IWETH/write/withdraw.js";
export { isTransferWithAuthorizationSupported } from "../../extensions/erc20/__generated__/USDC/write/transferWithAuthorization.js";
export {
type CanClaimParams,
type CanClaimResult,
+6 -1
View File
@@ -1,4 +1,9 @@
export type { Money, PaymentMiddlewareConfig } from "x402/types";
export type {
HTTPRequestStructure,
Money,
PaymentMiddlewareConfig,
Resource,
} from "x402/types";
export { decodePayment, encodePayment } from "../x402/encode.js";
export {
facilitator,
@@ -83,11 +83,23 @@ async function getNFTFromRPC(
client: options.contract.client,
tokenId: options.tokenId,
tokenUri,
}).catch(() => ({
id: options.tokenId,
type: "ERC1155",
uri: tokenUri,
})),
})
.then((metadata) => {
// if the metadata is null-ish, return a default metadata object
if (!metadata) {
return {
id: options.tokenId,
type: "ERC1155",
uri: tokenUri,
};
}
return metadata;
})
.catch(() => ({
id: options.tokenId,
type: "ERC1155",
uri: tokenUri,
})),
{
chainId: options.contract.chain.id,
owner: null,
@@ -139,11 +139,23 @@ async function getNFTFromRPC(
client: options.contract.client,
tokenId,
tokenUri: uri,
}).catch(() => ({
id: tokenId,
type: "ERC721",
uri,
})),
})
.then((metadata) => {
// if the metadata is null-ish, return a default metadata object
if (!metadata) {
return {
id: tokenId,
type: "ERC721",
uri,
};
}
return metadata;
})
.catch(() => ({
id: tokenId,
type: "ERC721",
uri,
})),
{
chainId: options.contract.chain.id,
owner,
@@ -1,48 +1,3 @@
type ThemeColors = {
base1: string;
base2: string;
base3: string;
base4: string;
primaryText: string;
secondaryText: string;
danger: string;
success: string;
overlay: string;
accentText: string;
accentBg: string;
textOnAccent: string;
};
const darkColors = {
accentBg: "hsl(216 100% 50%)",
accentText: "#3385FF",
base1: "hsl(230 11.63% 8.43%)",
base2: "hsl(230 11.63% 12%)",
base3: "hsl(230 11.63% 15%)",
base4: "hsl(230 11.63% 17%)",
danger: "#e5484D",
overlay: "rgba(0, 0, 0, 0.7)",
primaryText: "#eeeef0",
secondaryText: "#7c7a85",
success: "#30A46C",
textOnAccent: "#eeeef0",
} as const satisfies ThemeColors;
const lightColors = {
accentBg: "hsl(216 100% 50%)",
accentText: "#3385FF",
base1: "#fdfcfd",
base2: "#f2eff3",
base3: "#e3dfe6",
base4: "#dbd8e0",
danger: "#e5484D",
overlay: "rgba(0, 0, 0, 0.7)",
primaryText: "#211f26",
secondaryText: "#6f6d78",
success: "#30A46C",
textOnAccent: "#fdfcfd",
} as const satisfies ThemeColors;
/**
* @theme
*/
@@ -95,64 +50,77 @@ export type Theme = {
fontFamily: string;
};
/**
* @internal
*/
function createThemeObj(type: "dark" | "light", colors: ThemeColors): Theme {
return {
colors: {
accentButtonBg: colors.accentBg,
accentButtonText: colors.textOnAccent,
accentText: colors.accentText,
export const darkThemeObj: Theme = {
type: "dark",
colors: {
accentButtonBg: "hsl(221 83% 54%)",
accentButtonText: "hsl(0 0% 100%)",
accentText: "hsl(209.61deg 100% 65.31%)",
borderColor: "hsl(0 0% 15%)",
connectedButtonBg: "hsl(0 0% 3.92%)",
connectedButtonBgHover: "hsl(0 0% 11%)",
danger: "hsl(360 72% 55%)",
inputAutofillBg: "hsl(0 0% 11%)",
modalBg: "hsl(0 0% 3.92%)",
primaryButtonBg: "hsl(0 0% 100%)",
primaryButtonText: "hsl(0 0% 0%)",
primaryText: "hsl(0 0% 98%)",
scrollbarBg: "hsl(0 0% 11%)",
secondaryButtonBg: "hsl(0 0% 9%)",
modalOverlayBg: "rgba(0, 0, 0, 0.7)",
secondaryButtonHoverBg: "hsl(0 0% 9%/80%)",
secondaryButtonText: "hsl(0 0% 98%)",
secondaryIconColor: "hsl(0 0% 63%)",
secondaryIconHoverBg: "hsl(0 0% 11%)",
secondaryIconHoverColor: "hsl(0 0% 98%)",
secondaryText: "hsl(0 0% 63%)",
selectedTextBg: "hsl(0 0% 100%)",
selectedTextColor: "hsl(0 0% 0%)",
separatorLine: "hsl(0 0% 15%)",
skeletonBg: "hsl(0 0% 12%)",
success: "hsl(142 75% 50%)",
tertiaryBg: "hsl(0 0% 11%/50%)",
tooltipBg: "hsl(0 0% 11%)",
tooltipText: "hsl(0 0% 98%)",
},
fontFamily: "inherit",
};
borderColor: colors.base4,
connectedButtonBg: colors.base1,
connectedButtonBgHover: colors.base2,
danger: colors.danger,
inputAutofillBg: colors.base1,
modalBg: colors.base1,
modalOverlayBg: colors.overlay,
primaryButtonBg: colors.primaryText,
primaryButtonText: colors.base1,
primaryText: colors.primaryText,
scrollbarBg: colors.base2,
secondaryButtonBg: colors.base3,
secondaryButtonHoverBg: colors.base4,
secondaryButtonText: colors.primaryText,
secondaryIconColor: colors.secondaryText,
secondaryIconHoverBg: colors.base3,
secondaryIconHoverColor: colors.primaryText,
secondaryText: colors.secondaryText,
selectedTextBg: colors.primaryText,
selectedTextColor: colors.base1,
separatorLine: colors.base4,
skeletonBg: colors.base4,
success: colors.success,
tertiaryBg: colors.base2,
tooltipBg: colors.primaryText,
tooltipText: colors.base1,
},
fontFamily: "inherit",
type,
};
}
export const darkThemeObj = /* @__PURE__ */ createThemeObj("dark", darkColors);
export const lightThemeObj = /* @__PURE__ */ createThemeObj(
"light",
lightColors,
);
export const lightThemeObj: Theme = {
type: "light",
colors: {
accentButtonBg: "hsl(221 83% 54%)",
accentButtonText: "hsl(0 0% 100%)",
accentText: "hsl(211.23deg 100% 44.47%)",
borderColor: "hsl(0 0% 85%)",
connectedButtonBg: "hsl(0 0% 100%)",
connectedButtonBgHover: "hsl(0 0% 93%)",
danger: "hsl(360 72% 60%)",
inputAutofillBg: "hsl(0 0% 93%)",
modalBg: "hsl(0 0% 100%)",
primaryButtonBg: "hsl(0 0% 4%)",
primaryButtonText: "hsl(0 0% 100%)",
primaryText: "hsl(0 0% 4%)",
scrollbarBg: "hsl(0 0% 93%)",
secondaryButtonBg: "hsl(0 0% 93%)",
modalOverlayBg: "rgba(0, 0, 0, 0.7)",
secondaryButtonHoverBg: "hsl(0 0% 93%/80%)",
secondaryButtonText: "hsl(0 0% 4%)",
secondaryIconColor: "hsl(0 0% 40%)",
secondaryIconHoverBg: "hsl(0 0% 93%)",
secondaryIconHoverColor: "hsl(0 0% 4%)",
secondaryText: "hsl(0 0% 40%)",
selectedTextBg: "hsl(0 0% 4%)",
selectedTextColor: "hsl(0 0% 100%)",
separatorLine: "hsl(0 0% 85%)",
skeletonBg: "hsl(0 0% 85%)",
success: "hsl(142.09 70.56% 35.29%)",
tertiaryBg: "hsl(0 0% 93%/70%)",
tooltipBg: "hsl(0 0% 100%)",
tooltipText: "hsl(0 0% 4%)",
},
fontFamily: "inherit",
};
/**
* @theme
@@ -183,7 +183,7 @@ export const SendScreen = (props: SendScreenProps) => {
Amount
</ThemedText>
<ThemedInput
inputMode="numeric"
inputMode="decimal"
onChangeText={setAmount}
rightView={
<ThemedText
@@ -39,7 +39,6 @@ import { PaymentSelection } from "./payment-selection/PaymentSelection.js";
import { SuccessScreen } from "./payment-success/SuccessScreen.js";
import { QuoteLoader } from "./QuoteLoader.js";
import { StepRunner } from "./StepRunner.js";
import { useActiveWalletInfo } from "./swap-widget/hooks.js";
import type { PaymentMethod, RequiredParams } from "./types.js";
export type BuyOrOnrampPrepareResult = Extract<
@@ -432,7 +431,6 @@ function BridgeWidgetContent(
>,
) {
const [screen, setScreen] = useState<BuyWidgetScreen>({ id: "1:buy-ui" });
const activeWalletInfo = useActiveWalletInfo();
const handleError = useCallback(
(error: Error, quote: BridgePrepareResult | undefined) => {
@@ -478,7 +476,7 @@ function BridgeWidgetContent(
};
});
if (screen.id === "1:buy-ui" || !activeWalletInfo) {
if (screen.id === "1:buy-ui") {
return (
<FundWallet
theme={props.theme}
@@ -93,11 +93,11 @@ export function DirectPayment({
>
{/* Price section */}
<Container
center="y"
flex="row"
gap="3xs"
style={{
justifyContent: "space-between",
alignItems: "end",
}}
>
<FiatValue
@@ -108,7 +108,7 @@ export function DirectPayment({
size="xl"
token={paymentInfo.token}
tokenAmount={paymentInfo.amount}
weight={600}
weight={500}
/>
<Container flex="row" gap="3xs">
<Text
@@ -118,6 +118,9 @@ export function DirectPayment({
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
textTransform: "uppercase",
letterSpacing: "0.07em",
opacity: 0.7,
}}
>
One-time payment
@@ -125,11 +128,11 @@ export function DirectPayment({
</Container>
</Container>
<Spacer y="md" />
<Spacer y="md+" />
<Line />
<Line dashed />
<Spacer y="md" />
<Spacer y="md+" />
<Container
flex="row"
@@ -141,18 +144,12 @@ export function DirectPayment({
<Text color="secondaryText" size="sm">
Price
</Text>
<Text
color="primaryText"
size="sm"
style={{
fontFamily: "monospace",
}}
>
<Text color="primaryText" size="sm">
{`${paymentInfo.amount} ${paymentInfo.token.symbol}`}
</Text>
</Container>
<Spacer y="xs" />
<Spacer y="sm" />
{/* Network section */}
<Container
@@ -173,18 +170,15 @@ export function DirectPayment({
color="primaryText"
short
size="sm"
style={{
fontFamily: "monospace",
}}
/>
</Container>
</Container>
<Spacer y="md" />
<Spacer y="md+" />
<Line />
<Line dashed />
<Spacer y="lg" />
<Spacer y="md+" />
{/* Action button */}
<Container flex="column">
@@ -194,11 +188,11 @@ export function DirectPayment({
{showThirdwebBranding ? (
<div>
<Spacer y="md" />
<Spacer y="md+" />
<PoweredByThirdweb />
</div>
) : null}
<Spacer y="md" />
<Spacer y="md+" />
</Container>
</WithHeader>
);
@@ -167,6 +167,9 @@ export function FundWallet(props: FundWalletProps) {
const actionLabel = isReceiverDifferentFromActiveWallet ? "Pay" : "Buy";
const isMobile = useIsMobile();
// if no receiver address is set - wallet must be connected because the user's wallet is the receiver
const showConnectButton = !props.receiverAddress && !activeWalletInfo;
return (
<WithHeader
client={props.client}
@@ -283,7 +286,20 @@ export function FundWallet(props: FundWalletProps) {
)}
{/* Continue Button */}
{activeWalletInfo ? (
{showConnectButton ? (
<ConnectButton
client={props.client}
connectButton={{
label: props.buttonLabel || actionLabel,
style: {
width: "100%",
borderRadius: radius.full,
},
}}
theme={theme}
{...props.connectOptions}
/>
) : (
<Button
disabled={!receiver}
fullWidth
@@ -316,19 +332,6 @@ export function FundWallet(props: FundWalletProps) {
>
{props.buttonLabel || actionLabel}
</Button>
) : (
<ConnectButton
client={props.client}
connectButton={{
label: props.buttonLabel || actionLabel,
style: {
width: "100%",
borderRadius: radius.full,
},
}}
theme={theme}
{...props.connectOptions}
/>
)}
{props.showThirdwebBranding ? (
@@ -1,7 +1,7 @@
import type { ThirdwebClient } from "../../../../../client/client.js";
import { resolveScheme } from "../../../../../utils/ipfs.js";
import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
import { radius } from "../../../../core/design-system/index.js";
import { radius, spacing } from "../../../../core/design-system/index.js";
import { Container } from "../../components/basic.js";
import { Spacer } from "../../components/Spacer.js";
import { Text } from "../../components/text.js";
@@ -20,6 +20,7 @@ export function WithHeader(props: {
{/* image */}
{props.image && (
<div
className="tw-header-image"
style={{
aspectRatio: "16/9",
backgroundColor: theme.colors.tertiaryBg,
@@ -27,6 +28,7 @@ export function WithHeader(props: {
backgroundPosition: "center",
backgroundSize: "cover",
borderRadius: `${radius.md} ${radius.md} 0 0`,
marginBottom: spacing.xxs,
overflow: "hidden",
width: "100%",
}}
@@ -40,7 +42,7 @@ export function WithHeader(props: {
<>
{/* title */}
{props.title && (
<Text color="primaryText" size="lg" weight={600}>
<Text color="primaryText" size="lg" weight={500} trackingTight>
{props.title}
</Text>
)}
@@ -5,7 +5,6 @@ import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
import { checksumAddress } from "../../../../../utils/address.js";
import { formatNumber } from "../../../../../utils/formatNumber.js";
import { toTokens } from "../../../../../utils/units.js";
import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
import {
iconSize,
radius,
@@ -62,8 +61,6 @@ export function FiatProviderSelection({
currency,
country,
}: FiatProviderSelectionProps) {
const theme = useCustomTheme();
// Fetch quotes for all providers
const quoteQueries = useBuyWithFiatQuotesForProviders({
amount: toAmount || "0",
@@ -122,9 +119,9 @@ export function FiatProviderSelection({
fullWidth
onClick={() => onProviderSelected(provider.id)}
style={{
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.md,
borderRadius: radius.lg,
textAlign: "left",
padding: `${spacing.md}`,
}}
variant="secondary"
>
@@ -269,7 +269,7 @@ export function PaymentSelection({
}
return (
<Container flex="column" px="md" pb="md" pt="md+">
<Container flex="column" px="md" pt="md+">
<ModalHeader onBack={getBackHandler()} title={getStepTitle()} />
<Spacer y="lg" />
@@ -61,10 +61,8 @@ function PaymentMethodTokenRow({
onClick={() => onPaymentMethodSelected(paymentMethod)}
style={{
backgroundColor: theme.colors.tertiaryBg,
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.md,
opacity: hasEnoughBalance ? 1 : 0.5,
padding: `${spacing.sm} ${spacing.md}`,
borderRadius: radius.lg,
padding: `${spacing.md} ${spacing.md}`,
textAlign: "left",
}}
variant="secondary"
@@ -134,71 +132,74 @@ export function TokenSelection({
if (paymentMethodsLoading) {
return (
<>
<Text color="primaryText" size="md">
Loading your tokens
</Text>
<Spacer y="sm" />
<Container flex="column" gap="sm">
{/* Skeleton rows matching PaymentMethodTokenRow structure */}
{[1, 2, 3].map((i) => (
<Container
flex="column"
gap="xs"
pb="lg"
style={{
maxHeight: "400px",
overflowY: "auto",
scrollbarWidth: "none",
}}
>
{/* Skeleton rows matching PaymentMethodTokenRow structure */}
{new Array(10).fill(0).map((_, i) => (
<Container
// biome-ignore lint/suspicious/noArrayIndexKey: ok
key={i}
style={{
backgroundColor: theme.colors.tertiaryBg,
borderRadius: radius.lg,
padding: `${spacing.md} ${spacing.md}`,
}}
>
<Container
key={i}
style={{
backgroundColor: theme.colors.tertiaryBg,
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.md,
padding: `${spacing.sm} ${spacing.md}`,
}}
flex="row"
gap="md"
style={{ alignItems: "center", width: "100%" }}
>
{/* Left side: Token icon and name skeleton */}
<Container
center="y"
flex="row"
gap="md"
style={{ alignItems: "center", width: "100%" }}
gap="sm"
style={{ maxWidth: "50%" }}
>
{/* Left side: Token icon and name skeleton */}
<Container
center="y"
flex="row"
gap="sm"
style={{ maxWidth: "50%" }}
>
{/* Token icon skeleton */}
<div
style={{
backgroundColor: theme.colors.skeletonBg,
borderRadius: "50%",
height: "32px",
width: "32px",
}}
/>
<Container flex="column" gap="3xs">
{/* Token name skeleton */}
<Skeleton height="14px" width="60px" />
{/* Chain name skeleton */}
<Skeleton height="12px" width="40px" />
</Container>
{/* Token icon skeleton */}
<div
style={{
backgroundColor: theme.colors.skeletonBg,
borderRadius: "50%",
height: "32px",
width: "32px",
}}
/>
<Container flex="column" gap="3xs">
{/* Token name skeleton */}
<Skeleton height="14px" width="60px" />
{/* Chain name skeleton */}
<Skeleton height="12px" width="40px" />
</Container>
</Container>
{/* Right side: Price and balance skeleton */}
<Container
flex="column"
gap="3xs"
style={{ alignItems: "flex-end", flex: 1 }}
>
{/* Price amount skeleton */}
<Skeleton height="16px" width="80px" />
{/* Balance skeleton */}
<Container flex="row" gap="3xs">
<Skeleton height="12px" width="50px" />
<Skeleton height="12px" width="40px" />
</Container>
{/* Right side: Price and balance skeleton */}
<Container
flex="column"
gap="3xs"
style={{ alignItems: "flex-end", flex: 1 }}
>
{/* Price amount skeleton */}
<Skeleton height="16px" width="80px" />
{/* Balance skeleton */}
<Container flex="row" gap="3xs">
<Skeleton height="12px" width="50px" />
<Skeleton height="12px" width="40px" />
</Container>
</Container>
</Container>
))}
</Container>
</>
</Container>
))}
</Container>
);
}
@@ -221,35 +222,30 @@ export function TokenSelection({
}
return (
<>
<Text color="primaryText" size="md">
Your token balances
</Text>
<Spacer y="sm" />
<Container
flex="column"
gap="sm"
style={{
maxHeight: "400px",
overflowY: "auto",
scrollbarWidth: "none",
}}
>
{paymentMethods
.filter((method) => method.type === "wallet")
.map((method) => (
<PaymentMethodTokenRow
client={client}
destinationAmount={destinationAmount}
destinationToken={destinationToken}
feePayer={feePayer}
key={`${method.originToken.address}-${method.originToken.chainId}`}
onPaymentMethodSelected={onPaymentMethodSelected}
paymentMethod={method}
currency={currency}
/>
))}
</Container>
</>
<Container
flex="column"
gap="xs"
pb="lg"
style={{
maxHeight: "400px",
overflowY: "auto",
scrollbarWidth: "none",
}}
>
{paymentMethods
.filter((method) => method.type === "wallet")
.map((method) => (
<PaymentMethodTokenRow
client={client}
destinationAmount={destinationAmount}
destinationToken={destinationToken}
feePayer={feePayer}
key={`${method.originToken.address}-${method.originToken.chainId}`}
onPaymentMethodSelected={onPaymentMethodSelected}
paymentMethod={method}
currency={currency}
/>
))}
</Container>
);
}
@@ -1,5 +1,5 @@
"use client";
import { ChevronRightIcon, PlusIcon } from "@radix-ui/react-icons";
import { PlusIcon } from "@radix-ui/react-icons";
import type { ThirdwebClient } from "../../../../../client/client.js";
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
@@ -12,7 +12,6 @@ import { CreditCardIcon } from "../../ConnectWallet/icons/CreditCardIcon.js";
import { WalletRow } from "../../ConnectWallet/screens/Buy/swap/WalletRow.js";
import { Container } from "../../components/basic.js";
import { Button } from "../../components/buttons.js";
import { Spacer } from "../../components/Spacer.js";
import { Text } from "../../components/text.js";
interface WalletFiatSelectionProps {
@@ -35,56 +34,39 @@ export function WalletFiatSelection({
const theme = useCustomTheme();
return (
<>
<Container flex="column" gap="xs">
{paymentMethods.includes("crypto") && (
<>
{paymentMethods.length > 1 && (
<>
<Text color="primaryText" size="sm" weight={500}>
Pay with Crypto
</Text>
<Spacer y="sm" />
</>
)}
{/* Connected Wallets */}
{connectedWallets.length > 0 && (
<>
<Container flex="column" gap="sm">
{connectedWallets.map((wallet) => {
const account = wallet.getAccount();
if (!account?.address) {
return null;
}
return (
<Button
fullWidth
key={wallet.id}
onClick={() => onWalletSelected(wallet)}
style={{
backgroundColor: theme.colors.tertiaryBg,
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.md,
justifyContent: "space-between",
padding: `${spacing.sm} ${spacing.md}`,
}}
variant="secondary"
>
<WalletRow
address={account?.address}
client={client}
iconSize="lg"
textSize="sm"
/>
<ChevronRightIcon
style={{ height: iconSize.md, width: iconSize.md }}
/>
</Button>
);
})}
</Container>
<Spacer y="sm" />
</>
<Container flex="column" gap="sm">
{connectedWallets.map((wallet) => {
const account = wallet.getAccount();
if (!account?.address) {
return null;
}
return (
<Button
fullWidth
key={`${wallet.id}-${account.address}`}
onClick={() => onWalletSelected(wallet)}
style={{
borderRadius: radius.md,
justifyContent: "space-between",
padding: `${spacing.md} ${spacing.md}`,
}}
variant="secondary"
>
<WalletRow
address={account?.address}
client={client}
iconSize="lg"
textSize="sm"
/>
</Button>
);
})}
</Container>
)}
{/* Connect Another Wallet */}
@@ -92,11 +74,9 @@ export function WalletFiatSelection({
fullWidth
onClick={onConnectWallet}
style={{
backgroundColor: theme.colors.tertiaryBg,
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.md,
height: "auto",
padding: `${spacing.sm} ${spacing.md}`,
padding: `${spacing.md} ${spacing.md}`,
textAlign: "left",
}}
variant="secondary"
@@ -109,23 +89,22 @@ export function WalletFiatSelection({
<Container
style={{
alignItems: "center",
border: `1px dashed ${theme.colors.secondaryIconColor}`,
borderRadius: radius.sm,
border: `1px dashed ${theme.colors.secondaryText}`,
borderRadius: radius.full,
display: "flex",
height: iconSize.lg,
height: `${iconSize.lg}px`,
justifyContent: "center",
padding: spacing["4xs"],
width: iconSize.lg,
width: `${iconSize.lg}px`,
}}
>
<PlusIcon
color={theme.colors.secondaryText}
height={iconSize.md}
width={iconSize.md}
height={iconSize["sm+"]}
width={iconSize["sm+"]}
/>
</Container>
<Container flex="column" gap="3xs" style={{ flex: 1 }}>
<Text color="primaryText" size="sm" style={{ fontWeight: 600 }}>
<Text color="primaryText" size="sm">
Connect a Wallet
</Text>
<Text color="secondaryText" size="xs">
@@ -138,49 +117,37 @@ export function WalletFiatSelection({
)}
{paymentMethods.includes("card") && (
<>
<Spacer y="lg" />
<Text color="primaryText" size="sm" weight={500}>
Pay with Card
</Text>
<Spacer y="sm" />
<Button
fullWidth
onClick={onFiatSelected}
style={{
backgroundColor: theme.colors.tertiaryBg,
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.md,
height: "auto",
padding: `${spacing.sm} ${spacing.md}`,
textAlign: "left",
}}
variant="secondary"
<Button
fullWidth
onClick={onFiatSelected}
style={{
borderRadius: radius.md,
height: "auto",
padding: `${spacing.md} ${spacing.md}`,
textAlign: "left",
}}
variant="secondary"
>
<Container
flex="row"
gap="md"
style={{ alignItems: "center", width: "100%" }}
>
<Container
flex="row"
gap="md"
style={{ alignItems: "center", width: "100%" }}
>
<CreditCardIcon
color={theme.colors.secondaryIconColor}
size={iconSize.lg}
/>
<Container flex="column" gap="3xs" style={{ flex: 1 }}>
<Text color="primaryText" size="sm" style={{ fontWeight: 600 }}>
Pay with Card
</Text>
<Text color="secondaryText" size="xs">
Onramp and pay in one step
</Text>
</Container>
<CreditCardIcon
color={theme.colors.secondaryText}
size={iconSize.lg}
/>
<Container flex="column" gap="3xs" style={{ flex: 1 }}>
<Text color="primaryText" size="sm">
Pay with Card
</Text>
<Text color="secondaryText" size="xs">
Onramp and pay in one step
</Text>
</Container>
</Button>
</>
</Container>
</Button>
)}
</>
</Container>
);
}

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