Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
881c7a96f7 Version Packages (#8372)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-07 06:04:42 +07:00
FirekeeperandGitHub ad68969559 Refactor Solana sign transaction API for flexible inputs (#8371) 2025-11-07 05:56:11 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
915d955da1 Version Packages (#8370)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-06 13:58:49 -08:00
Joaquim VergesandGitHub 7da16cff2f [SDK] Make maxAmount optional in wrapFetchWithPayment (#8369) 2025-11-06 13:54:29 -08:00
Yash094 f0edc0f253 Update vault documentation text and faq (#8367)
```
[Portal] Fix: Update Vault open-source status and add FAQ

## Notes for the reviewer

This PR updates the Vault portal documentation to accurately reflect its open-source status.

*   Removed "open-source" from the main description and metadata on the Vault overview page.
*   Added a new FAQ entry under `/vault/faqs` clarifying that Vault is not yet open source.

## How to test

1.  Navigate to <https://portal.thirdweb.com/vault>
    *   Verify the main description no longer includes "open-source".
    *   (Optional) Inspect page metadata to confirm "open-source" is removed from the description.
2.  Navigate to <https://portal.thirdweb.com/vault/faqs>
    *   Verify the new FAQ "Is Vault open source?" is present with the answer "Vault is not yet open source, but we plan to release it soon."
```

---
[Slack Thread](https://thirdwebdev.slack.com/archives/C09DS2CKGP2/p1762411784588169?thread_ts=1762411784.588169&cid=C09DS2CKGP2)

<a href="https://cursor.com/background-agent?bcId=bc-ab8c2b24-6a74-4e0c-b302-d84d69ac5953"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-cursor-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-cursor-light.svg"><img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg"></picture></a>&nbsp;<a href="https://cursor.com/agents?id=bc-ab8c2b24-6a74-4e0c-b302-d84d69ac5953"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-web-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-web-light.svg"><img alt="Open in Web" src="https://cursor.com/open-in-web.svg"></picture></a>

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the description of the `Vault` service to remove references to it being open source and refining the language for clarity.

### Detailed summary
- Added a new `Details` section in `apps/portal/src/app/vault/faqs/page.mdx` about Vault's open-source status.
- Updated the `description` in `apps/portal/src/app/vault/layout.tsx` to remove "open-source."
- Revised the `description` in `apps/portal/src/app/vault/page.mdx` to remove "open-source."
- Modified the main description of `Vault` in `apps/portal/src/app/vault/page.mdx` for clarity.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

## Documentation
* Added new FAQ entry addressing open-source availability with information on future release plans
* Updated Vault service descriptions emphasizing non-custodial key management design and TEE security architecture
* Refined documentation metadata to provide clearer communication of Vault's core features, security model, and service capabilities

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-06 17:58:37 +00:00
Yash094 0a2d269782 Dashboard: Update faucet amount for injective testnet (#8368)
<!--

## 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 `Injective testnet` in the `faucet.ts` file, indicating a change in the configuration for test networks.

### Detailed summary
- Added a new entry for `Injective testnet` with a value of `1` in the configuration.
- The existing entry for `Arc testnet` remains unchanged.

> ✨ 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 Injective testnet, enabling faucet claims on this network with customized claim amount configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-06 16:07:33 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
3d996d1d23 Version Packages (#8364)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-05 16:06:25 -08:00
Joaquim VergesandGitHub 40781b8170 [SDK] Allow custom chain for SIWE authentication (#8365) 2025-11-05 15:47:54 -08:00
MananTank d907017217 [MNY-295] Fix BuyWidget autoconnect not working when receiverAddress prop is set (#8363)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing the `BuyWidget` component's autoconnect functionality when the `receiverAddress` prop is set, and it introduces a new story variant to demonstrate this behavior.

### Detailed summary
- Updated `BuyWidget` to conditionally render the `AutoConnect` component based on `connectOptions.autoConnect`.
- Introduced a new story `NoAutoConnect` in `BuyWidget.stories.tsx` to showcase the widget with `autoConnect` set to false.
- Modified `FundWallet.tsx` to ensure proper handling of the `autoConnect` prop.

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

## Release Notes

* **Bug Fixes**
  * Fixed auto-connect functionality that was not working correctly when the receiver address property is set on the buy widget.

* **New Features**
  * Added configurable auto-connect option to the buy widget, enabling users to control whether auto-connection is enabled or disabled based on their needs.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-05 21:10:50 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
cf53c2d7f1 Version Packages (#8362)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-05 12:10:38 -08:00
Joaquim VergesandGitHub 207cb9dbeb [SDK] Enhance isMobile() detection with additional mobile signals (#8361) 2025-11-05 11:48:57 -08:00
Jonas DanielsandGitHub b4d365bdc0 Move x402 route from /payments/x402 to /x402 (#8360) 2025-11-04 16:30:08 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ea4e2fe376 Version Packages (#8357)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-04 14:20:21 -08:00
Joaquim VergesandGitHub 1d54cff7b4 [SDK] Add support for extra metadata in x402 payment requests (#8358) 2025-11-04 14:13:48 -08:00
YashandGitHub fd04eefea9 Update implementations, fix deployment (#8356) 2025-11-05 00:10:12 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
4cc68310dc Version Packages (#8355)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-04 00:47:18 -08:00
Joaquim VergesandGitHub ccdce496e0 [SDK] Fix hiddenWallets prop not applying to all wallets screen (#8354) 2025-11-04 00:39:11 -08:00
Joaquim VergesandGitHub 2725f337f8 [Dashboard] Fix active users aggregation in project highlights card (#8346) 2025-11-02 18:23:41 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2c6ea3e6dd Version Packages (#8340)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-31 18:48:20 -07:00
Joaquim VergesandGitHub 219fc69d72 [SDK] Update x402 flows to use remote /accepts endpoint (#8345) 2025-10-31 18:40:39 -07:00
Joaquim VergesandGitHub 8d17685683 [SDK] Refactor payment processing to use facilitator.accepts (#8344) 2025-10-31 18:38:51 -07:00
Jonas DanielsandGitHub 8ef418c543 Move success URL handling from PayPage to StyledBuyWidget component (#8342) 2025-10-30 14:35:32 -07:00
jnsdls b88f75b494 Add successUrl support to payment page (#8341)
<!--

## 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 various configuration files, updates the `PayPage` component with URL validation, and enhances the fetch functionality with OAuth support. It also includes the addition of testing HTML and size limits for the package.

### Detailed summary
- Added `wrap` export in `nexus-fetch.ts`.
- Introduced `biome.json`, `.size-limit.json`, `tsconfig.json`, `tsconfig.build.json`, and `knip.json` for configuration.
- Enhanced `PayPage` with `onlyUrl` validation and success URL handling.
- Added testing HTML file for `nexus-fetch`.
- Updated `package.json` with author and dependencies.
- Improved fetch functionality with OAuth and error handling.

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

<!-- end pr-codex -->

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

- **New Features**
  - Payment page now supports redirecting to a provided success URL after payment completes.
  - New nexus-fetch library adds automatic OAuth token management, PKCE-backed sign-in flow, automatic token refresh, and integrated handling of proxy payment flows for web requests.

- **Chores**
  - Added build/configuration and size-limit files for the new nexus-fetch package.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-30 20:40:41 +00:00
MananTank c83f1c6f7a [MNY-298] SDK: Remove Bridge.routes, Update Docs (#8335)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview

This PR focuses on removing the `Bridge.routes` functionality and the `useBridgeRoutes` hook from the codebase, along with related references and documentation updates in various files.

### Detailed summary

- Deleted `Routes.ts`, `Routes.test.ts`, and `useBridgeRoutes.ts` files.
- Removed references to `Bridge.routes` in multiple `.mdx` files and sidebar components.
- Updated `index.ts` to stop exporting `routes`.
- Adjusted `useSendTransactionCore` to use `Bridge.tokens` instead of `Bridge.routes`.
- Removed the `PriceComparator` class and associated methods for route comparison.

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

* **Breaking Changes**
  * Removed the Bridge.routes API, which previously supported route discovery with filtering by chains and tokens, pagination, and sorting
  * Removed the useBridgeRoutes React hook

* **Documentation**
  * Removed documentation pages covering bridge route discovery and filtering functionality
  * Removed related sidebar and guide navigation links

* **Tests**
  * Removed test suites for removed APIs

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-30 14:46:34 +00:00
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
122 changed files with 5114 additions and 3494 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 -1
View File
@@ -10,7 +10,7 @@ module.exports = [
"/wallets",
"/account-abstraction",
"/payments",
"/payments/x402",
"/x402",
"/nexus",
"/auth",
"/in-app-wallets",
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

+5
View File
@@ -479,6 +479,11 @@ async function redirects() {
destination: "/tokens",
permanent: false,
},
{
source: "/payments/x402",
destination: "/x402",
permanent: false,
},
];
}
@@ -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"
/>
),
)}
+4
View File
@@ -5,6 +5,10 @@ const customClaimAmounts: Record<number, number> = {
631571: 0.1,
// Sophon testnet
531050104: 5,
// Arc testnet
5042002: 1,
// Injective testnet
1439: 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))",
},
@@ -59,7 +59,6 @@ export function ErrorRate({ datapoints }: { datapoints: ResultItem[] }) {
key={tag}
stackId="a"
stroke={`var(--color-${tag})`}
type="natural"
/>,
);
}
@@ -14,6 +14,7 @@ type AggregatedMetrics = {
export function ProjectHighlightsCard(props: {
selectedChart: string | undefined;
aggregatedUserStats: InAppWalletStats[];
userStats: InAppWalletStats[];
volumeStats: UniversalBridgeStats[];
teamSlug: string;
@@ -22,6 +23,7 @@ export function ProjectHighlightsCard(props: {
}) {
const {
selectedChart,
aggregatedUserStats,
userStats,
volumeStats,
teamSlug,
@@ -70,6 +72,12 @@ export function ProjectHighlightsCard(props: {
: "activeUsers"
}
aggregateFn={(_data, key) => {
if (key === "activeUsers") {
return aggregatedUserStats.reduce(
(acc, curr) => acc + curr.uniqueWalletsConnected,
0,
);
}
return timeSeriesData.reduce((acc, curr) => acc + curr[key], 0);
}}
chartConfig={chartConfig}
@@ -378,8 +378,18 @@ async function AsyncAppHighlightsCard(props: {
params: PageParams;
authToken: string;
}) {
const [walletUserStatsTimeSeries, universalBridgeUsage] =
const [aggregatedUserStats, walletUserStatsTimeSeries, universalBridgeUsage] =
await Promise.allSettled([
getInAppWalletUsage(
{
from: props.range.from,
period: "all",
projectId: props.project.id,
teamId: props.project.teamId,
to: props.range.to,
},
props.authToken,
),
getInAppWalletUsage(
{
from: props.range.from,
@@ -408,6 +418,11 @@ async function AsyncAppHighlightsCard(props: {
) {
return (
<ProjectHighlightsCard
aggregatedUserStats={
aggregatedUserStats.status === "fulfilled"
? aggregatedUserStats.value
: []
}
selectedChart={props.selectedChart}
selectedChartQueryParam={props.selectedChartQueryParam}
teamSlug={props.params.team_slug}
@@ -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,6 +64,7 @@ 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 type { Wallet as EVMWallet } from "../server-wallets/wallet-table/types";
@@ -79,6 +87,7 @@ interface ServerWalletsTableProps {
teamSlug: string;
client: ThirdwebClient;
solanaPermissionError?: boolean;
authToken: string;
}
export function ServerWalletsTable(props: ServerWalletsTableProps) {
@@ -95,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();
@@ -145,11 +158,33 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
</>
)}
{activeChain === "solana" && (
<CreateSolanaWallet
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>
@@ -278,6 +313,8 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
project={project}
teamSlug={teamSlug}
client={client}
authToken={authToken}
chainId={selectedSolanaChain}
/>
))}
</TableBody>
@@ -507,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",
@@ -547,7 +588,12 @@ function SolanaWalletRow({
</TableCell>
<TableCell>
<SolanaWalletBalance publicKey={wallet.publicKey} />
<SolanaWalletBalance
publicKey={wallet.publicKey}
authToken={authToken}
clientId={project.publishableKey}
chainId={chainId}
/>
</TableCell>
<TableCell>
@@ -739,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;
}
}
@@ -215,6 +215,7 @@ export default async function TransactionsAnalyticsPage(props: {
solanaWallets={solanaAccounts.data.items}
teamSlug={params.team_slug}
solanaPermissionError={isSolanaPermissionError}
authToken={authToken}
/>
)}
</div>
@@ -70,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" />
@@ -202,6 +202,7 @@ export default async function Page(props: {
solanaWallets={solanaAccounts.data.items}
teamSlug={params.team_slug}
solanaPermissionError={isSolanaPermissionError || false}
authToken={authToken}
/>
)}
</div>
@@ -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>
);
}
+46 -161
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,144 +60,33 @@ export default async function PayPage({
}
return (
<div className="relative flex h-dvh w-full items-center justify-center">
<div className="relative flex flex-col min-h-dvh w-full">
<ThemeProvider
forcedTheme={theme === "light" ? "light" : "dark"}
defaultTheme={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>
<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 py-12 w-full items-center grow">
<PayPageWidget
amount={
paymentLink.amount ? BigInt(paymentLink.amount) : undefined
}
chainId={Number(paymentLink.destinationToken.chainId)}
clientId={undefined} // Payment links don't need to use the same client ID to be executed
image={paymentLink.imageUrl}
name={paymentLink.title}
paymentLinkId={id}
purchaseData={paymentLink.purchaseData}
recipientAddress={paymentLink.receiver}
redirectUri={redirectUri}
token={token}
/>
</main>
</div>
<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)}
clientId={undefined} // Payment links don't need to use the same client ID to be executed
image={paymentLink.imageUrl}
name={paymentLink.title}
paymentLinkId={id}
purchaseData={paymentLink.purchaseData}
recipientAddress={paymentLink.receiver}
redirectUri={redirectUri}
token={token}
/>
</main>
</ThemeProvider>
</div>
);
@@ -237,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();
@@ -9,7 +9,12 @@ import { payLandingWallets } from "./wallets";
const client = getClientThirdwebClient();
export function StyledBuyWidget(
props: Omit<BuyWidgetProps, "client" | "theme">,
props: Omit<
BuyWidgetProps,
"client" | "theme" | "onSuccess" | "onError" | "onCancel"
> & {
successUrl?: string;
},
) {
const { theme } = useTheme();
@@ -22,6 +27,22 @@ export function StyledBuyWidget(
connectOptions={{
wallets: payLandingWallets,
}}
onSuccess={() => {
if (props.successUrl) {
try {
const url = new URL(props.successUrl);
url.searchParams.set("success", "true");
window.location.href = url.toString();
} catch (error) {
// Log URL construction error for debugging
console.error(
"Failed to construct redirect URL:",
props.successUrl,
error,
);
}
}
}}
/>
);
}
+30 -4
View File
@@ -21,19 +21,44 @@ type SearchParams = {
[key: string]: string | string[] | undefined;
};
const onlyAddress = (v: string) => (isAddress(v) ? v : undefined);
const onlyNumber = (v: string) =>
Number.isNaN(Number(v)) ? undefined : Number(v);
/**
* Validates and normalizes a URL string.
* Only allows http: and https: protocols with a valid hostname.
* @returns normalized URL string on success, undefined on failure
*/
const onlyUrl = (v: string): string | undefined => {
try {
const url = new URL(v);
// Only allow http or https protocols
if (url.protocol !== "http:" && url.protocol !== "https:") {
return undefined;
}
// Ensure hostname is non-empty
if (!url.hostname) {
return undefined;
}
// Return normalized URL
return url.toString();
} catch {
// Invalid URL format
return undefined;
}
};
export default async function PayPage(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);
const receiver = parse(searchParams.receiver, onlyAddress);
const token = parse(searchParams.token, onlyAddress);
const chain = parse(searchParams.chain, onlyNumber);
const amount = parse(searchParams.amount, onlyNumber);
const successUrl = parse(searchParams.successUrl, onlyUrl);
return (
<ThemeProvider
@@ -51,6 +76,7 @@ export default async function PayPage(props: {
tokenAddress={token}
receiverAddress={receiver}
amount={amount ? amount.toString() : undefined}
successUrl={successUrl}
/>
</div>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"dependencies": {
"@abstract-foundation/agw-react": "^1.6.4",
"@abstract-foundation/agw-react": "^1.10.0",
"@ai-sdk/react": "^2.0.25",
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-avatar": "^1.1.10",
@@ -41,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") ||
@@ -53,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",
},
-172
View File
@@ -1,172 +0,0 @@
import {
Callout,
OpenSourceCard,
createMetadata,
InstallTabs,
SDKCard,
Grid,
ArticleIconCard,
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@doc";
import {
ReactIcon,
TypeScriptIcon,
UnityIcon,
DotNetIcon,
UnrealEngineIcon,
} from "@/icons";
import { ExternalLink } from "lucide-react";
export const metadata = createMetadata({
image: {
title: "Find Routes",
icon: "payments",
},
title: "Find Routes",
description: "Find available routes for bridging, swapping, and paying.",
});
# Find Routes
Use thirdweb's `Bridge.routes` utility to find all routes between tokens.
### Get All Routes
Retrieve all supported routes:
```typescript
import { Bridge } from "thirdweb";
const routes = await Bridge.routes({
client,
});
```
This returns an array of route objects with information about supported token pairs:
```typescript
[
{
destinationToken: {
address: "0x12c88a3C30A7AaBC1dd7f2c08a97145F5DCcD830",
chainId: 1,
decimals: 18,
iconUri: "https://assets.coingecko.com/coins/images/37207/standard/G.jpg",
name: "G7",
symbol: "G7",
},
originToken: {
address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
chainId: 480,
decimals: 18,
iconUri: "https://assets.relay.link/icons/1/light.png",
name: "Ether",
symbol: "ETH",
}
},
// ... more routes
]
```
### Filter Routes by Origin
Get all routes starting from a specific chain and token:
```typescript
import { Bridge } from "thirdweb";
// Get all routes starting from mainnet ETH
const routes = await Bridge.routes({
originChainId: 1,
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
client,
});
```
### Filter Routes by Destination
Find routes that end at a specific destination:
```typescript
import { Bridge } from "thirdweb";
// Get all routes ending at USDC on Optimism
const routes = await Bridge.routes({
destinationChainId: 10,
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
client,
});
```
### Filter Routes by Both Origin and Destination
Find direct routes between specific token pairs:
```typescript
import { Bridge } from "thirdweb";
// Get routes from mainnet ETH to USDC on Optimism
const routes = await Bridge.routes({
originChainId: 1,
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
destinationChainId: 10,
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
client,
});
```
### Pagination
For large result sets, use pagination to retrieve routes in chunks:
```typescript
import { Bridge } from "thirdweb";
// Get the first 10 routes starting from mainnet ETH
const routes = await Bridge.routes({
originChainId: 1,
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
limit: 10,
offset: 0,
client,
});
// Get the next 10 routes
const nextRoutes = await Bridge.routes({
originChainId: 1,
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
limit: 10,
offset: 10,
client,
});
```
### Limit Route Complexity
Control the maximum number of steps in the returned routes:
```typescript
import { Bridge } from "thirdweb";
// Get only direct routes (1 step)
const directRoutes = await Bridge.routes({
originChainId: 1,
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
maxSteps: 1,
client,
});
```
## Going further
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
- [Send a Payment](/payments/send)
- [Get Token Prices](/payments/prices)
## Explore Full API References
- [Bridge.routes](/references/typescript/v5/routes)
-1
View File
@@ -60,7 +60,6 @@ You can execute this quote exactly like a buy. See the [Send a Payment](/payment
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
- [Send a Payment](/payments/send)
- [Get Routes](/payments/routes)
## Explore Full API References
-4
View File
@@ -28,10 +28,6 @@ export const sidebar: SideBar = {
href: `${bridgeSlug}/tokens`,
name: "Get Token Prices",
},
{
href: `${bridgeSlug}/routes`,
name: "Get Routes",
},
{
href: `${bridgeSlug}/bridge-widget-script`,
name: "BridgeWidget Script",
+1 -108
View File
@@ -255,23 +255,6 @@ export class SwapManager {
throw new Error("Transaction timeout - please check status manually");
}
async getAvailableRoutes(params: {
originChainId?: number;
originTokenAddress?: string;
destinationChainId?: number;
destinationTokenAddress?: string;
limit?: number;
}) {
try {
return await Bridge.routes({
...params,
client: this.client,
});
} catch (error) {
throw new Error(`Failed to get routes: ${error.message}`);
}
}
}
```
@@ -319,92 +302,6 @@ async function swapEthToMatic(amount: string, userAccount: Account) {
}
```
### Advanced Price Comparison
Compare prices across multiple routes to find the best deal:
```typescript
interface RouteComparison {
route: any;
quote: SwapQuote;
priceImpact: number;
gasEstimate: bigint;
}
export class PriceComparator {
private swapManager: SwapManager;
constructor(swapManager: SwapManager) {
this.swapManager = swapManager;
}
async compareRoutes(
fromToken: { chainId: number; address: string },
amount: bigint,
userAddress: string
): Promise<RouteComparison[]> {
// Get available routes
const routes = await this.swapManager.getAvailableRoutes({
originChainId: fromToken.chainId,
originTokenAddress: fromToken.address,
limit: 10,
});
// Get quotes for each route
const comparisons: RouteComparison[] = [];
for (const route of routes) {
try {
const quote = await this.swapManager.getQuote({
fromChainId: route.originToken.chainId,
fromTokenAddress: route.originToken.address,
toChainId: route.destinationToken.chainId,
toTokenAddress: route.destinationToken.address,
amount,
userAddress,
client: this.swapManager.client,
});
const priceImpact = this.calculatePriceImpact(quote, amount);
const gasEstimate = this.estimateGasCosts(quote);
comparisons.push({
route,
quote,
priceImpact,
gasEstimate,
});
} catch (error) {
console.warn(`Failed to get quote for route ${route.originToken.symbol} -> ${route.destinationToken.symbol}:`, error);
}
}
// Sort by best output amount
return comparisons.sort((a, b) =>
Number(b.quote.destinationAmount) - Number(a.quote.destinationAmount)
);
}
private calculatePriceImpact(quote: SwapQuote, inputAmount: bigint): number {
// Simplified price impact calculation
const ratio = Number(quote.destinationAmount) / Number(quote.originAmount);
return Math.abs(1 - ratio) * 100; // Percentage
}
private estimateGasCosts(quote: SwapQuote): bigint {
// Sum up gas estimates from all transactions
return quote.steps.reduce((total, step) => {
return total + step.transactions.reduce((stepTotal: bigint, tx: any) => {
return stepTotal + (tx.gasLimit || 200000n); // Default estimate
}, 0n);
}, 0n);
}
}
```
<Callout variant="info">
**Gas Optimization Tip:** For frequently traded pairs, consider caching route data to reduce API calls and improve UI responsiveness.
</Callout>
</TabsContent>
</Tabs>
@@ -415,20 +312,16 @@ export class PriceComparator {
- **Cross-chain swaps** - Exchange tokens across 50+ supported chains
- **Real-time quotes** - Get up-to-date pricing and execution estimates
- **Route optimization** - Find the best path for any token pair
- **Price comparison** - Compare multiple routes to maximize output
- **Status tracking** - Monitor cross-chain transaction progress
## Going Further
- [Send a Payment](/payments/send) - Learn about peer-to-peer transfers
- [Get Routes](/payments/routes) - Explore route discovery APIs
- [Token Prices](/payments/tokens) - Access real-time token pricing
- [Token Prices](/bridge/tokens) - Access real-time token pricing
- [Webhooks](/payments/webhooks) - Get notified when swaps complete
## API Reference
- [Bridge.Buy.prepare](/references/typescript/v5/buy/prepare)
- [Bridge.routes](/references/typescript/v5/routes)
- [Bridge.status](/references/typescript/v5/status)
@@ -119,7 +119,6 @@ const nextTokens = await Bridge.tokens({
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
- [Send a Payment](/payments/send)
- [Get Routes](/payments/routes)
## Explore Full API References
@@ -264,7 +264,6 @@ Once this code completes, your payment has been fully executed. Payments normall
- [Sell a Product](/payments/products)
- [Token Prices](/payments/tokens)
- [Routes](/payments/routes)
## Explore API References
-4
View File
@@ -49,10 +49,6 @@ export const sidebar: SideBar = {
href: `/bridge/tokens`,
name: "Get Token Prices",
},
{
href: `/bridge/routes`,
name: "Get Routes",
},
{
href: `${paymentsSlug}/webhooks`,
name: "Webhooks",
@@ -222,7 +222,6 @@ async function verifyWebhook(
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
- [Send a Payment](/payments/send)
- [Find Routes](/payments/routes)
- [Get Token Prices](/payments/tokens)
## Explore Full API References
+4
View File
@@ -44,3 +44,7 @@ The admin key is temporarily stored in dashboard when performing actions that re
Please note, you should never share your admin key with third parties.
</Details>
<Details summary="Is Vault open source?">
Vault is not yet open source, but we plan to release it soon.
</Details>
+1 -1
View File
@@ -12,7 +12,7 @@ export default async function Layout(props: { children: React.ReactNode }) {
export const metadata = createMetadata({
description:
"Vault is an open-source non-custodial key management service, secured with TEE architecture (AWS Nitro Enclaves) and designed for blockchain applications.",
"Vault is a non-custodial key management service secured with TEE architecture (AWS Nitro Enclaves), designed for blockchain applications.",
title: "Vault",
image: {
icon: "vault",
+2 -2
View File
@@ -5,7 +5,7 @@ import { ArrowLeftRightIcon, UserLockIcon, UsersIcon, WalletIcon } from "lucide-
export const metadata = createMetadata({
title: "thirdweb Vault",
description: "An open-source, secure key management service for storing, managing, and protecting cryptographic keys and secrets.",
description: "A secure key management service for storing, managing, and protecting cryptographic keys and secrets.",
image: {
icon: "vault",
title: "thirdweb Vault",
@@ -14,7 +14,7 @@ export const metadata = createMetadata({
# Vault
Vault is an open-source non-custodial key management service, secured with TEE architecture (AWS Nitro Enclaves) and designed for blockchain applications.
Vault is a non-custodial key management service secured with TEE architecture (AWS Nitro Enclaves), designed for blockchain applications.
## Features
+88
View File
@@ -1,5 +1,93 @@
# wagmi-inapp
## 0.0.26
### Patch Changes
- Updated dependencies []:
- thirdweb@5.111.6
- @thirdweb-dev/wagmi-adapter@0.2.181
## 0.0.25
### Patch Changes
- Updated dependencies [[`7da16cf`](https://github.com/thirdweb-dev/js/commit/7da16cff2ff3aaa80c7fe5614639d950d88b5bf2)]:
- thirdweb@5.111.5
- @thirdweb-dev/wagmi-adapter@0.2.180
## 0.0.24
### Patch Changes
- Updated dependencies [[`d907017`](https://github.com/thirdweb-dev/js/commit/d907017217a0927e7f69eab0470b1fe120b4d1a1), [`40781b8`](https://github.com/thirdweb-dev/js/commit/40781b8170a1014ef76e438ba3c8244137dba7ea)]:
- thirdweb@5.111.4
- @thirdweb-dev/wagmi-adapter@0.2.179
## 0.0.23
### Patch Changes
- Updated dependencies [[`207cb9d`](https://github.com/thirdweb-dev/js/commit/207cb9dbeb82411809bafa2a8e833b1f10414b92)]:
- thirdweb@5.111.3
- @thirdweb-dev/wagmi-adapter@0.2.178
## 0.0.22
### Patch Changes
- Updated dependencies [[`1d54cff`](https://github.com/thirdweb-dev/js/commit/1d54cff7b442b8667ddd54bd783502c367ffc83e), [`fd04eef`](https://github.com/thirdweb-dev/js/commit/fd04eefea9a2a99eee3740d6746040c9f81cd43f)]:
- thirdweb@5.111.2
- @thirdweb-dev/wagmi-adapter@0.2.177
## 0.0.21
### Patch Changes
- Updated dependencies [[`ccdce49`](https://github.com/thirdweb-dev/js/commit/ccdce496e09bc9420b0e15854328651c01f36279)]:
- thirdweb@5.111.1
- @thirdweb-dev/wagmi-adapter@0.2.176
## 0.0.20
### Patch Changes
- Updated dependencies [[`c83f1c6`](https://github.com/thirdweb-dev/js/commit/c83f1c6f7a38c8a9f424798e6e726e1d037ed042), [`219fc69`](https://github.com/thirdweb-dev/js/commit/219fc69d72b1ca861bf0228589039788008ea0c0)]:
- thirdweb@5.111.0
- @thirdweb-dev/wagmi-adapter@0.2.175
## 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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wagmi-inapp",
"private": true,
"version": "0.0.15",
"version": "0.0.26",
"type": "module",
"scripts": {
"dev": "vite",
+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.
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/engine
## 3.4.0
### Minor Changes
- [#8371](https://github.com/thirdweb-dev/js/pull/8371) [`ad68969`](https://github.com/thirdweb-dev/js/commit/ad68969559a80a5f188a647ef0e1c04f0003a0ad) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - Reworked Solana Sign Transaction API that accepts more flexible inputs
## 3.3.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/engine",
"version": "3.3.0",
"version": "3.4.0",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
+27 -27
View File
@@ -274,6 +274,33 @@ export const sendSolanaTransaction = <ThrowOnError extends boolean = false>(
});
};
/**
* Sign Solana Transaction
* Sign a Solana transaction without broadcasting it
*/
export const signSolanaTransaction = <ThrowOnError extends boolean = false>(
options: Options<SignSolanaTransactionData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).post<
SignSolanaTransactionResponses,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/sign/transaction",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
};
/**
* Cancel Transaction
* Attempt to cancel a queued transaction. Transactions that have been sent and are waiting for mine cannot be cancelled.
@@ -424,33 +451,6 @@ export const signSolanaMessage = <ThrowOnError extends boolean = false>(
});
};
/**
* Sign Solana Transaction
* Sign a serialized Solana transaction.
*/
export const signSolanaTransaction = <ThrowOnError extends boolean = false>(
options?: Options<SignSolanaTransactionData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
SignSolanaTransactionResponses,
unknown,
ThrowOnError
>({
security: [
{
name: "x-secret-key",
type: "apiKey",
},
],
url: "/v1/solana/sign-transaction",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
};
/**
* Get Transactions
* Search transactions with various filters and pagination
+90 -48
View File
@@ -556,6 +556,10 @@ export type Eip7702OwnerExecution = {
* The delegated EOA address
*/
from: AddressDef;
/**
* Optional fleet ID to route the transaction to a specific bundler fleet
*/
fleetId?: string | null;
};
/**
@@ -570,6 +574,10 @@ export type Eip7702SessionKeyExecution = {
* The account address is the address of a delegated account you want to execute the transaction on. This account has granted a session key to the `session_key_address`
*/
accountAddress: AddressDef;
/**
* Optional fleet ID to route the transaction to a specific bundler fleet
*/
fleetId?: string | null;
};
export type EmptyIdempotencySetResponse = {
@@ -1000,15 +1008,11 @@ export type RpcErrorResponse = {
/**
* Request to send a Solana transaction
*/
export type SendSolanaTransactionRequest = {
export type SendSolanaTransactionRequest = SolanaTransactionInput & {
/**
* Idempotency key for this transaction (defaults to random UUID)
*/
idempotencyKey?: string;
/**
* List of Solana instructions to execute
*/
instructions: Array<SolanaInstructionData>;
/**
* Solana execution options
*/
@@ -1188,6 +1192,30 @@ export type SignResultData = {
signedData: string;
};
/**
* Request to sign a Solana transaction
*/
export type SignSolanaTransactionRequest = SolanaTransactionInput & {
/**
* Solana execution options
*/
executionOptions: SolanaExecutionOptions;
};
/**
* Data returned from successful signing
*/
export type SignSolanaTransactionResponse = {
/**
* The signature (base58-encoded)
*/
signature: string;
/**
* The signed serialized transaction (base64-encoded)
*/
signedTransaction: string;
};
/**
* Request to sign typed data
*/
@@ -1452,6 +1480,21 @@ export type SolanaRpcResponseErrorData =
type: "NODE_UNHEALTHY";
};
/**
* Input for Solana transaction - either build from instructions or use pre-built
*/
export type SolanaTransactionInput =
| SolanaTransactionInputInstructions
| SolanaTransactionInputSerialized;
export type SolanaTransactionInputInstructions = {
instructions: Array<SolanaInstructionData>;
};
export type SolanaTransactionInputSerialized = {
transaction: string;
};
/**
* Execution Option Variants
* All supported specific execution options are contained here
@@ -1503,6 +1546,22 @@ export type SuccessResponseQueuedTransactionsResponse = {
};
};
export type SuccessResponseSignSolanaTransactionResponse = {
/**
* Data returned from successful signing
*/
result: {
/**
* The signature (base58-encoded)
*/
signature: string;
/**
* The signed serialized transaction (base64-encoded)
*/
signedTransaction: string;
};
};
export type ThirdwebError =
| {
error: ThirdwebSerializationError;
@@ -1847,6 +1906,32 @@ export type SendSolanaTransactionResponses = {
export type SendSolanaTransactionResponse =
SendSolanaTransactionResponses[keyof SendSolanaTransactionResponses];
export type SignSolanaTransactionData = {
/**
* Sign Solana transaction request
*/
body: SignSolanaTransactionRequest;
headers?: {
/**
* Vault access token
*/
"x-vault-access-token"?: string | null;
};
path?: never;
query?: never;
url: "/v1/solana/sign/transaction";
};
export type SignSolanaTransactionResponses = {
/**
* Successfully signed Solana transaction
*/
200: SuccessResponseSignSolanaTransactionResponse;
};
export type SignSolanaTransactionResponse2 =
SignSolanaTransactionResponses[keyof SignSolanaTransactionResponses];
export type CancelTransactionData = {
body?: never;
path: {
@@ -2076,49 +2161,6 @@ export type SignSolanaMessageResponses = {
export type SignSolanaMessageResponse =
SignSolanaMessageResponses[keyof SignSolanaMessageResponses];
export type SignSolanaTransactionData = {
body?: {
/**
* Base58 encoded Solana public key
*/
from: string;
/**
* Base64 encoded Solana transaction
*/
transaction: string;
};
headers?: {
/**
* Vault Access Token used to access your EOA
*/
"x-vault-access-token"?: string;
};
path?: never;
query?: never;
url: "/v1/solana/sign-transaction";
};
export type SignSolanaTransactionResponses = {
/**
* Transaction signed
*/
200: {
result: {
/**
* Base58 encoded signature
*/
signature: string;
/**
* Base58 encoded Solana public key
*/
signerPublicKey: string;
};
};
};
export type SignSolanaTransactionResponse =
SignSolanaTransactionResponses[keyof SignSolanaTransactionResponses];
export type GetTransactionsData = {
body?: never;
path?: never;
+77
View File
@@ -1,5 +1,82 @@
# @thirdweb-dev/nebula
## 0.2.84
### Patch Changes
- Updated dependencies []:
- thirdweb@5.111.6
## 0.2.83
### Patch Changes
- Updated dependencies [[`7da16cf`](https://github.com/thirdweb-dev/js/commit/7da16cff2ff3aaa80c7fe5614639d950d88b5bf2)]:
- thirdweb@5.111.5
## 0.2.82
### Patch Changes
- Updated dependencies [[`d907017`](https://github.com/thirdweb-dev/js/commit/d907017217a0927e7f69eab0470b1fe120b4d1a1), [`40781b8`](https://github.com/thirdweb-dev/js/commit/40781b8170a1014ef76e438ba3c8244137dba7ea)]:
- thirdweb@5.111.4
## 0.2.81
### Patch Changes
- Updated dependencies [[`207cb9d`](https://github.com/thirdweb-dev/js/commit/207cb9dbeb82411809bafa2a8e833b1f10414b92)]:
- thirdweb@5.111.3
## 0.2.80
### Patch Changes
- Updated dependencies [[`1d54cff`](https://github.com/thirdweb-dev/js/commit/1d54cff7b442b8667ddd54bd783502c367ffc83e), [`fd04eef`](https://github.com/thirdweb-dev/js/commit/fd04eefea9a2a99eee3740d6746040c9f81cd43f)]:
- thirdweb@5.111.2
## 0.2.79
### Patch Changes
- Updated dependencies [[`ccdce49`](https://github.com/thirdweb-dev/js/commit/ccdce496e09bc9420b0e15854328651c01f36279)]:
- thirdweb@5.111.1
## 0.2.78
### Patch Changes
- Updated dependencies [[`c83f1c6`](https://github.com/thirdweb-dev/js/commit/c83f1c6f7a38c8a9f424798e6e726e1d037ed042), [`219fc69`](https://github.com/thirdweb-dev/js/commit/219fc69d72b1ca861bf0228589039788008ea0c0)]:
- thirdweb@5.111.0
## 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
+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.73"
"version": "0.2.84"
}
+13
View File
@@ -0,0 +1,13 @@
[
{
"import": "*",
"limit": "1 kB",
"name": "@thirdweb-dev/nexus (esm)",
"path": "./dist/esm/exports/nexus.js"
},
{
"limit": "1 kB",
"name": "@thirdweb-dev/nexus (cjs)",
"path": "./dist/cjs/exports/nexus.js"
}
]
+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"
}
}
+76
View File
@@ -0,0 +1,76 @@
{
"author": "thirdweb eng <[email protected]>",
"browser": {
"crypto": false
},
"bugs": {
"url": "https://github.com/thirdweb-dev/js/issues"
},
"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-fetch.d.ts",
"import": "./dist/esm/exports/nexus-fetch.js",
"default": "./dist/cjs/exports/nexus-fetch.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-fetch.js",
"module": "./dist/esm/exports/nexus-fetch.js",
"name": "@thirdweb-dev/fetch",
"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",
"test:ui": "bun ./testing/test.html --watch"
},
"sideEffects": false,
"type": "module",
"types": "./dist/types/exports/nexus-fetch.d.ts",
"typings": "./dist/types/exports/nexus-fetch.d.ts",
"version": "0.0.0"
}
@@ -0,0 +1 @@
export { wrap } from "../fetch.js";
+459
View File
@@ -0,0 +1,459 @@
//---
// Constants
//---
const TOKEN_DATA_KEY = "tw:nexus:td";
const STATE_KEY = "tw:nexus:state";
const VERIFIER_KEY = "tw:nexus:verifier";
const NEXUS_API_BASE_URL = "https://nexus-api.thirdweb-dev.com";
const OAUTH_CLIENT_ID = "9ad56e84-5abe-4ea5-a6c2-9834b9d31245";
// Token expiry buffer: consider token expired 5 minutes before actual expiry
const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
//---
// Types
//---
type TokenData = {
accessToken: string;
refreshToken: string;
expiresAt: number;
};
type OAuthTokenResponse = {
access_token: string;
expires_in: number;
refresh_token: string;
token_type: "Bearer";
};
//---
// Token Encoding/Decoding
//---
function encodeTokenData(data: TokenData): string {
return btoa(JSON.stringify(data));
}
function decodeTokenData(encoded: string): TokenData | null {
try {
const decoded = atob(encoded);
return JSON.parse(decoded) as TokenData;
} catch {
return null;
}
}
//---
// Initialization
// Automatically wrap global fetch and complete OAuth flow if returning from authorization
//---
(() => {
if (hasWindow()) {
completeOauthFlow();
const originalFetch = globalThis.fetch;
try {
globalThis.fetch = wrap(originalFetch);
} catch (error) {
console.error("[thirdweb nexus] Error wrapping fetch", error);
globalThis.fetch = originalFetch;
}
}
})();
//---
// Main Fetch Wrapper
//---
/**
* Wraps a fetch function to automatically handle HTTP 402 (Payment Required) responses.
* When a 402 is encountered, it uses the Nexus API to process the payment and retry the request.
*
* @param fetchFn - The fetch function to wrap (typically globalThis.fetch)
* @param options - Optional configuration
* @param options.maxValue - Maximum payment value allowed in wei
* @returns A wrapped fetch function that handles 402 responses
*/
export function wrap(
fetchFn: typeof globalThis.fetch,
options?: {
maxValue?: bigint;
},
) {
return async (input: RequestInfo | URL, init?: RequestInit) => {
const response = await fetchFn(input, init);
if (response.status !== 402) {
return response;
}
const AT = await getAuthToken();
if (!AT) {
// open login page in a new tab or throw an error if opening fails
startOauthFlow();
return new Response("Payment required", { status: 402 });
}
// create the proxy request
const proxyUrl = new URL("/fetch", NEXUS_API_BASE_URL);
proxyUrl.searchParams.set(
"data",
btoa(
JSON.stringify({
// original request related information
request: {
url: new URL(
typeof input === "string"
? input
: input instanceof Request
? input.url
: input,
).toString(),
method:
(typeof input === "string"
? init?.method
: input instanceof Request
? input.method
: "GET") ?? "GET",
headers:
typeof input === "string"
? init?.headers
: input instanceof Request
? input.headers
: {},
},
// x402 options
x402Options: {
...(options?.maxValue
? { maxValue: options.maxValue.toString() }
: {}),
},
}),
),
);
let proxyRes = await fetchFn(proxyUrl, {
method: "POST",
body:
(typeof input === "string"
? init?.body
: input instanceof Request
? input.body
: init?.body) ?? undefined,
headers: {
Authorization: `Bearer ${AT}`,
},
});
// Handle 401 (unauthorized) - try to refresh token and retry
if (proxyRes.status === 401) {
const refreshedToken = await refreshAccessToken();
if (refreshedToken) {
// Retry with refreshed token
proxyRes = await fetchFn(proxyUrl, {
method: "POST",
body:
(typeof input === "string"
? init?.body
: input instanceof Request
? input.body
: init?.body) ?? undefined,
headers: {
Authorization: `Bearer ${refreshedToken}`,
},
});
} else {
// Refresh failed, start OAuth flow
startOauthFlow();
return new Response("Unauthorized", { status: 401 });
}
}
if (proxyRes.status === 402) {
// means the wallet doesn't have funds and needs to onramp, open the funding link
const data = (await proxyRes.json()) as {
result: { link: string; message: string };
};
// TODO: we can await this and then retry the call after by setting up
// window.postMessage communication and opening this in a new tab
const paymentUrl = new URL(data.result.link);
paymentUrl.searchParams.set("successUrl", window.location.href);
window.location.href = paymentUrl.toString();
return new Response("Payment required", { status: 402 });
}
return proxyRes;
};
}
//---
// Browser/Environment Utilities
//---
function hasWindow(): boolean {
return typeof window !== "undefined";
}
function getLocalStorage(): Storage | null {
try {
return hasWindow() ? window.localStorage : null;
} catch {
return null;
}
}
//---
// Token Data Management
//---
let inMemoryTokenData: TokenData | null = null;
function getTokenData(): TokenData | null {
if (inMemoryTokenData !== null) {
return inMemoryTokenData;
}
const encoded = getLocalStorage()?.getItem(TOKEN_DATA_KEY);
if (encoded) {
const data = decodeTokenData(encoded);
if (data) {
inMemoryTokenData = data;
return data;
}
}
return null;
}
function setTokenData(
accessToken: string,
refreshToken: string,
expiresIn: number,
): void {
if (!accessToken || accessToken === "undefined") {
return;
}
if (!refreshToken || refreshToken === "undefined") {
return;
}
// Calculate expiry timestamp (current time + expires_in seconds)
const expiresAt = Date.now() + expiresIn * 1000;
const tokenData: TokenData = { accessToken, refreshToken, expiresAt };
const encoded = encodeTokenData(tokenData);
getLocalStorage()?.setItem(TOKEN_DATA_KEY, encoded);
inMemoryTokenData = tokenData;
}
function deleteTokenData(): void {
getLocalStorage()?.removeItem(TOKEN_DATA_KEY);
inMemoryTokenData = null;
}
function isTokenExpired(tokenData: TokenData | null): boolean {
if (!tokenData) {
return true;
}
return Date.now() >= tokenData.expiresAt - TOKEN_EXPIRY_BUFFER_MS;
}
//---
// Token Validation & Refresh
//---
/**
* Gets a valid access token, refreshing it if necessary.
* @returns A valid access token or null if unavailable
*/
async function getAuthToken(): Promise<string | null> {
const tokenData = getTokenData();
// If we have a token but it's expired, try to refresh
if (tokenData && isTokenExpired(tokenData)) {
const refreshedToken = await refreshAccessToken();
return refreshedToken;
}
return tokenData?.accessToken ?? null;
}
/**
* Refreshes an expired access token using the refresh token.
* @returns A new access token or null if refresh fails
*/
async function refreshAccessToken(): Promise<string | null> {
const tokenData = getTokenData();
if (!tokenData) {
return null;
}
try {
const response = await fetch(`${NEXUS_API_BASE_URL}/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: tokenData.refreshToken,
client_id: OAUTH_CLIENT_ID,
}),
});
if (!response.ok) {
// Refresh failed, clear all tokens
deleteTokenData();
return null;
}
const data = (await response.json()) as OAuthTokenResponse;
// Store new tokens
setTokenData(data.access_token, data.refresh_token, data.expires_in);
return data.access_token;
} catch (error) {
console.error("[thirdweb nexus] Error refreshing access token", error);
return null;
}
}
//---
// OAuth PKCE (Proof Key for Code Exchange) Utilities
//---
function storeState(code: string): void {
getLocalStorage()?.setItem(STATE_KEY, code);
}
function getState(): string | null | undefined {
return getLocalStorage()?.getItem(STATE_KEY);
}
function deleteState(): void {
getLocalStorage()?.removeItem(STATE_KEY);
}
function storeVerifier(verifier: string): void {
getLocalStorage()?.setItem(VERIFIER_KEY, verifier);
}
function getVerifier(): string | null | undefined {
return getLocalStorage()?.getItem(VERIFIER_KEY);
}
function deleteVerifier(): void {
getLocalStorage()?.removeItem(VERIFIER_KEY);
}
/**
* Generates a cryptographically random code verifier for PKCE.
*/
function generateCodeVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
/**
* Generates a code challenge from a verifier for PKCE using SHA-256.
*/
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(new Uint8Array(hash));
}
/**
* Base64 URL-safe encoding (without padding).
*/
function base64UrlEncode(array: Uint8Array): string {
const base64 = btoa(String.fromCharCode(...array));
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
//---
// OAuth Flow
//---
/**
* Initiates the OAuth authorization flow by redirecting to the Nexus authorization endpoint.
*/
async function startOauthFlow(): Promise<void> {
const state = crypto.randomUUID();
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
const redirectUri = new URL(window.location.href);
const url = new URL("/oauth/authorize", NEXUS_API_BASE_URL);
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", OAUTH_CLIENT_ID);
url.searchParams.set("code_challenge", challenge);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("state", state);
url.searchParams.set(
"redirect_uri",
`${redirectUri.origin}${redirectUri.pathname}`,
);
storeState(state);
storeVerifier(verifier);
// just open the page directly
window.location.href = url.toString();
}
/**
* Completes the OAuth flow by exchanging the authorization code for tokens.
* Called automatically on page load if code parameter is present in URL.
*/
async function completeOauthFlow(): Promise<void> {
try {
const searchParams = new URL(window.location.href).searchParams;
const code = searchParams.get("code");
const state = searchParams.get("state");
const error = searchParams.get("error");
if (error) {
console.error("[thirdweb nexus] OAuth error:", error);
return;
}
if (!code || !state) {
// No code or state found, nothing to do
return;
}
if (state !== getState()) {
// State mismatch - possible CSRF attack
console.warn("[thirdweb nexus] OAuth state mismatch");
return;
}
const verifier = getVerifier();
if (!verifier) {
// No verifier found, nothing to do
return;
}
// Delete the state and verifier so they can't be reused
deleteState();
deleteVerifier();
const redirectUri = new URL(window.location.href);
const response = await fetch(`${NEXUS_API_BASE_URL}/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
code_verifier: verifier,
grant_type: "authorization_code",
redirect_uri: `${redirectUri.origin}${redirectUri.pathname}`,
client_id: OAUTH_CLIENT_ID,
}),
});
if (!response.ok) {
console.error(
"[thirdweb nexus] Failed to exchange authorization code for tokens",
);
return;
}
const data = (await response.json()) as OAuthTokenResponse;
// Store all token data
setTokenData(data.access_token, data.refresh_token, data.expires_in);
} catch (error) {
console.error("[thirdweb nexus] Error completing OAuth flow:", error);
}
}
+21
View File
@@ -0,0 +1,21 @@
<html>
<head>
<title>Nexus Fetch Testing</title>
<script src="../src/exports/nexus-fetch.ts"></script>
</head>
<body>
<button id="test-button">Test</button>
<script>
const testButton = document.getElementById("test-button");
testButton.addEventListener("click", () => {
fetch("https://q1qkkomp.nx.link/kindle", {
method: "POST",
body: JSON.stringify({
recipient: "0x0890C23024089675D072E984f28A93bb391a35Ab",
})
});
});
testButton.click();
</script>
</body>
</html>
+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"]
}
+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"
},
+87
View File
@@ -1,5 +1,92 @@
# thirdweb
## 5.111.6
### Patch Changes
- Updated dependencies [[`ad68969`](https://github.com/thirdweb-dev/js/commit/ad68969559a80a5f188a647ef0e1c04f0003a0ad)]:
- @thirdweb-dev/engine@3.4.0
## 5.111.5
### Patch Changes
- [#8369](https://github.com/thirdweb-dev/js/pull/8369) [`7da16cf`](https://github.com/thirdweb-dev/js/commit/7da16cff2ff3aaa80c7fe5614639d950d88b5bf2) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Make maxAmount optional in wrapFetchWithPayment and loosen schema validation for payment payloads
## 5.111.4
### Patch Changes
- [#8363](https://github.com/thirdweb-dev/js/pull/8363) [`d907017`](https://github.com/thirdweb-dev/js/commit/d907017217a0927e7f69eab0470b1fe120b4d1a1) Thanks [@MananTank](https://github.com/MananTank)! - Fix BuyWidget autoconnect not working when receiverAddress prop is set
- [#8365](https://github.com/thirdweb-dev/js/pull/8365) [`40781b8`](https://github.com/thirdweb-dev/js/commit/40781b8170a1014ef76e438ba3c8244137dba7ea) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Respect passed chain when doing SIWE for in-app/ecosystem wallet
## 5.111.3
### Patch Changes
- [#8361](https://github.com/thirdweb-dev/js/pull/8361) [`207cb9d`](https://github.com/thirdweb-dev/js/commit/207cb9dbeb82411809bafa2a8e833b1f10414b92) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add extra mobile detection for isMobile() function
## 5.111.2
### Patch Changes
- [#8358](https://github.com/thirdweb-dev/js/pull/8358) [`1d54cff`](https://github.com/thirdweb-dev/js/commit/1d54cff7b442b8667ddd54bd783502c367ffc83e) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add support for extraMetadata in x402 payment requirements
- [#8356](https://github.com/thirdweb-dev/js/pull/8356) [`fd04eef`](https://github.com/thirdweb-dev/js/commit/fd04eefea9a2a99eee3740d6746040c9f81cd43f) Thanks [@kumaryash90](https://github.com/kumaryash90)! - Update implementations, fix deployment
## 5.111.1
### Patch Changes
- [#8354](https://github.com/thirdweb-dev/js/pull/8354) [`ccdce49`](https://github.com/thirdweb-dev/js/commit/ccdce496e09bc9420b0e15854328651c01f36279) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix hiddenWallets prop not applying to all wallets screen
## 5.111.0
### Minor Changes
- [#8335](https://github.com/thirdweb-dev/js/pull/8335) [`c83f1c6`](https://github.com/thirdweb-dev/js/commit/c83f1c6f7a38c8a9f424798e6e726e1d037ed042) Thanks [@MananTank](https://github.com/MananTank)! - Remove `Bridge.routes` and `useBridgeRoutes`
### Patch Changes
- [#8345](https://github.com/thirdweb-dev/js/pull/8345) [`219fc69`](https://github.com/thirdweb-dev/js/commit/219fc69d72b1ca861bf0228589039788008ea0c0) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Update x402 flows to use remote /accepts endpoint
## 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
+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.2"
"version": "5.111.6"
}
-161
View File
@@ -1,161 +0,0 @@
import { http, passthrough } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { TEST_CLIENT } from "~test/test-clients.js";
import { routes } from "./Routes.js";
const server = setupServer(
http.get("https://bridge.thirdweb.com/v1/routes", () => {
passthrough();
}),
);
describe.runIf(process.env.TW_SECRET_KEY)("Bridge.routes", () => {
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it("should get a valid list of routes", async () => {
const allRoutes = await routes({
client: TEST_CLIENT,
});
expect(allRoutes).toBeDefined();
expect(Array.isArray(allRoutes)).toBe(true);
});
it("should filter routes by origin chain", async () => {
const filteredRoutes = await routes({
client: TEST_CLIENT,
originChainId: 1,
});
expect(filteredRoutes).toBeDefined();
expect(Array.isArray(filteredRoutes)).toBe(true);
expect(
filteredRoutes.every((route) => route.originToken.chainId === 1),
).toBe(true);
});
it("should filter routes by destination chain", async () => {
const filteredRoutes = await routes({
client: TEST_CLIENT,
destinationChainId: 1,
});
expect(filteredRoutes).toBeDefined();
expect(Array.isArray(filteredRoutes)).toBe(true);
expect(
filteredRoutes.every((route) => route.destinationToken.chainId === 1),
).toBe(true);
});
it("should filter routes by origin token", async () => {
const filteredRoutes = await routes({
client: TEST_CLIENT,
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
});
expect(filteredRoutes).toBeDefined();
expect(Array.isArray(filteredRoutes)).toBe(true);
expect(
filteredRoutes.every(
(route) =>
route.originToken.address ===
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
),
).toBe(true);
});
it("should filter routes by destination token", async () => {
const filteredRoutes = await routes({
client: TEST_CLIENT,
destinationTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
});
expect(filteredRoutes).toBeDefined();
expect(Array.isArray(filteredRoutes)).toBe(true);
expect(
filteredRoutes.every(
(route) =>
route.destinationToken.address ===
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
),
).toBe(true);
});
it("should combine filters", async () => {
const filteredRoutes = await routes({
client: TEST_CLIENT,
destinationChainId: 10,
destinationTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
originChainId: 1,
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
});
expect(filteredRoutes).toBeDefined();
expect(Array.isArray(filteredRoutes)).toBe(true);
expect(filteredRoutes.length).toBeGreaterThan(0);
expect(
filteredRoutes.every(
(route) =>
route.originToken.chainId === 1 &&
route.destinationToken.chainId === 10 &&
route.originToken.address ===
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" &&
route.destinationToken.address ===
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
),
).toBe(true);
});
it("should respect limit and offset", async () => {
const page1Routes = await routes({
client: TEST_CLIENT,
limit: 1,
offset: 1,
});
expect(page1Routes).toBeDefined();
expect(Array.isArray(page1Routes)).toBe(true);
expect(page1Routes.length).toBe(1);
const page2Routes = await routes({
client: TEST_CLIENT,
limit: 1,
offset: 2,
});
expect(page2Routes).toBeDefined();
expect(Array.isArray(page2Routes)).toBe(true);
expect(page2Routes.length).toBe(1);
expect(JSON.stringify(page1Routes)).not.toEqual(
JSON.stringify(page2Routes),
);
});
it("should surface any errors", async () => {
server.use(
http.get("https://bridge.thirdweb.com/v1/routes", () => {
return Response.json(
{
code: "InvalidRoutesRequest",
message: "The provided request is invalid.",
},
{ status: 400 },
);
}),
);
await expect(
routes({
client: TEST_CLIENT,
limit: 1000,
offset: 1000,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
"[Error: The provided request is invalid.]",
);
});
});
-228
View File
@@ -1,228 +0,0 @@
import type { Address as ox__Address, Hex as ox__Hex } from "ox";
import type { ThirdwebClient } from "../client/client.js";
import { getThirdwebBaseUrl } from "../utils/domains.js";
import { getClientFetch } from "../utils/fetch.js";
import { ApiError } from "./types/Errors.js";
import type { Route } from "./types/Route.js";
/**
* Retrieves supported Bridge routes based on the provided filters.
*
* When multiple filters are specified, a route must satisfy all filters to be included (it acts as an AND operator).
*
* @example
* ```typescript
* import { Bridge } from "thirdweb";
*
* const routes = await Bridge.routes({
* client: thirdwebClient,
* });
* ```
*
* Returned routes might look something like:
* ```typescript
* [
* {
* destinationToken: {
* address: "0x12c88a3C30A7AaBC1dd7f2c08a97145F5DCcD830",
* chainId: 1,
* decimals: 18,
* iconUri: "https://assets.coingecko.com/coins/images/37207/standard/G.jpg",
* name: "G7",
* symbol: "G7",
* },
* originToken: {
* address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
* chainId: 480,
* decimals: 18,
* iconUri: "https://assets.relay.link/icons/1/light.png",
* name: "Ether",
* symbol: "ETH",
* }
* },
* {
* destinationToken: {
* address: "0x4d224452801ACEd8B2F0aebE155379bb5D594381",
* chainId: 1,
* decimals: 18,
* iconUri: "https://coin-images.coingecko.com/coins/images/24383/large/apecoin.jpg?1696523566",
* name: "ApeCoin",
* symbol: "APE",
* },
* originToken: {
* address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
* chainId: 480,
* decimals: 18,
* iconUri: "https://assets.relay.link/icons/1/light.png",
* name: "Ether",
* symbol: "ETH",
* }
* }
* ]
* ```
*
* You can filter for specific chains or tokens:
* ```typescript
* import { Bridge } from "thirdweb";
*
* // Get all routes starting from mainnet ETH
* const routes = await Bridge.routes({
* originChainId: 1,
* originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
* client: thirdwebClient,
* });
* ```
*
* The returned routes will be limited based on the API. You can paginate through the results using the `limit` and `offset` parameters:
* ```typescript
* import { Bridge } from "thirdweb";
*
* // Get the first 10 routes starting from mainnet ETH
* const routes = await Bridge.routes({
* originChainId: 1,
* originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
* limit: 10,
* offset: 0,
* client: thirdwebClient,
* });
* ```
*
* You can sort the returned routes by `popularity`:
* ```ts
* import { Bridge } from "thirdweb";
*
* // Get the 10 most popular routes starting from mainnet ETH
* const routes = await Bridge.routes({
* originChainId: 1,
* originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
* limit: 10,
* offset: 0,
* sortBy: "popularity",
* client: thirdwebClient,
* });
* ```
*
* @param options - The options for the quote.
* @param options.client - Your thirdweb client.
* @param options.originChainId - Filter by a specific origin chain ID.
* @param options.originTokenAddress - Filter by a specific origin token address.
* @param options.destinationChainId - Filter by a specific destination chain ID.
* @param options.destinationTokenAddress - Filter by a specific destination token address.
* @param options.transactionHash - Filter by a specific transaction hash.
* @param options.maxSteps - Limit the number of steps returned.
* @param options.sortBy - Sort the routes by various categories.
* @param options.limit - Limit the number of routes returned.
* @param options.offset - Offset the number of routes returned.
*
* @returns A promise that resolves to an array of routes.
*
* @throws Will throw an error if there is an issue fetching the routes.
* @bridge
* @beta
*/
export async function routes(options: routes.Options): Promise<routes.Result> {
const {
client,
originChainId,
originTokenAddress,
destinationChainId,
destinationTokenAddress,
maxSteps,
sortBy,
limit,
offset,
includePrices,
} = options;
const clientFetch = getClientFetch(client);
const url = new URL(`${getThirdwebBaseUrl("bridge")}/v1/routes`);
if (originChainId) {
url.searchParams.set("originChainId", originChainId.toString());
}
if (originTokenAddress) {
url.searchParams.set("originTokenAddress", originTokenAddress);
}
if (destinationChainId) {
url.searchParams.set("destinationChainId", destinationChainId.toString());
}
if (destinationTokenAddress) {
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
}
if (maxSteps) {
url.searchParams.set("maxSteps", maxSteps.toString());
}
if (limit) {
url.searchParams.set("limit", limit.toString());
}
if (offset) {
url.searchParams.set("offset", offset.toString());
}
if (sortBy) {
url.searchParams.set("sortBy", sortBy);
}
if (includePrices) {
url.searchParams.set("includePrices", includePrices.toString());
}
const response = await clientFetch(url.toString());
if (!response.ok) {
const errorJson = await response.json();
throw new ApiError({
code: errorJson.code || "UNKNOWN_ERROR",
correlationId: errorJson.correlationId || undefined,
message: errorJson.message || response.statusText,
statusCode: response.status,
});
}
const { data }: { data: Route[] } = await response.json();
return data;
}
/**
* Namespace containing types for the routes function.
* @namespace routes
* @bridge
*/
export declare namespace routes {
/**
* Options for fetching available bridge routes.
* @interface Options
* @bridge
*/
type Options = {
/** Your thirdweb client */
client: ThirdwebClient;
/** The origin chain ID to filter routes by */
originChainId?: number;
/** The origin token address to filter routes by */
originTokenAddress?: ox__Address.Address;
/** The destination chain ID to filter routes by */
destinationChainId?: number;
/** The destination token address to filter routes by */
destinationTokenAddress?: ox__Address.Address;
/** Transaction hash to filter routes by */
transactionHash?: ox__Hex.Hex;
/**
* Sort routes by popularity
* @deprecated
*/
sortBy?: "popularity";
/** Maximum number of steps in the route */
maxSteps?: number;
/** Whether to include price information in the response */
includePrices?: boolean;
/** Number of results to return (pagination) */
limit?: number;
/** Number of results to skip (pagination) */
offset?: number;
};
/**
* Result returned from fetching bridge routes.
* Contains an array of available routes.
* @interface Result
* @bridge
*/
type Result = Route[];
}
-1
View File
@@ -1,7 +1,6 @@
export * as Buy from "./Buy.js";
export { chains } from "./Chains.js";
export * as Onramp from "./Onramp.js";
export { routes } from "./Routes.js";
export * as Sell from "./Sell.js";
export { status } from "./Status.js";
export { tokens } from "./Token.js";
@@ -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,
});
@@ -9,44 +9,48 @@ import { getContract } from "../contract.js";
import { deployCloneFactory } from "./utils/bootstrap.js";
describe.runIf(process.env.TW_SECRET_KEY)("deploy dynamic", () => {
it.sequential("should deploy dynamic contract with extensions", async () => {
await deployCloneFactory({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
// TODO: Fix this test
it.skip.sequential(
"should deploy dynamic contract with extensions",
async () => {
await deployCloneFactory({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
const deployed = await deployPublishedContract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
contractId: "EvolvingNFT",
contractParams: {
contractURI: "",
defaultAdmin: TEST_ACCOUNT_A.address,
name: "Evolving nft",
royaltyBps: 0n,
royaltyRecipient: TEST_ACCOUNT_A.address,
saleRecipient: TEST_ACCOUNT_A.address,
symbol: "ENFT",
trustedForwarders: [],
},
});
const deployed = await deployPublishedContract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
contractId: "EvolvingNFT",
contractParams: {
contractURI: "",
defaultAdmin: TEST_ACCOUNT_A.address,
name: "Evolving nft",
royaltyBps: 0n,
royaltyRecipient: TEST_ACCOUNT_A.address,
saleRecipient: TEST_ACCOUNT_A.address,
symbol: "ENFT",
trustedForwarders: [],
},
});
expect(deployed).toBeDefined();
expect(deployed).toBeDefined();
const contract = getContract({
address: deployed,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
const contract = getContract({
address: deployed,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
const extensions = await readContract({
contract,
method: resolveMethod("getAllExtensions"),
params: [],
});
const extensions = await readContract({
contract,
method: resolveMethod("getAllExtensions"),
params: [],
});
expect(extensions.length).toEqual(3);
});
expect(extensions.length).toEqual(3);
},
);
});
@@ -1,6 +1,6 @@
export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
232: {
MarketplaceV3: "0x9742f5ac11958cFAd151eBF0Fc31302fA409036E",
MarketplaceV3: "0x4e0C3577335961Ff800FFDA24981EB2F38D94483",
},
300: {
MarketplaceV3: "0x58e0F289C7dD2025eBd0696d913ECC0fdc1CC8bc",
@@ -18,7 +18,7 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
MarketplaceV3: "0x2dA4Dd326A6482679547071be21f74685d730504",
},
37111: {
MarketplaceV3: "0x56Abb6a3f25DCcdaDa106191053b1CC54C196DEE",
MarketplaceV3: "0x12A305cc7168fa3b7B172fE53c57b9a22716F667",
},
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",
@@ -171,7 +171,8 @@ describe.runIf(process.env.TW_SECRET_KEY)("getEvents", () => {
expect(events.length).toBe(14);
});
it("should get individual events with extension no filter using indexer", async () => {
// TODO: re-enable this test once indexer is fixed
it.skip("should get individual events with extension no filter using indexer", async () => {
const events = await getContractEvents({
contract: DOODLES_CONTRACT,
events: [transferEvent()],
@@ -182,7 +183,8 @@ describe.runIf(process.env.TW_SECRET_KEY)("getEvents", () => {
expect(events.length).toBe(38);
});
it("should get events for signature using indexer", async () => {
// TODO: re-enable this test once indexer is fixed
it.skip("should get events for signature using indexer", async () => {
const events = await getContractEvents({
contract: DOODLES_CONTRACT,
events: [
@@ -198,7 +200,8 @@ describe.runIf(process.env.TW_SECRET_KEY)("getEvents", () => {
expect(events.length).toBe(2);
});
it("should get specified events using indexer", async () => {
// TODO: re-enable this test once indexer is fixed
it.skip("should get specified events using indexer", async () => {
const events = await getContractEvents({
contract: USDT_CONTRACT,
events: [
+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,
-4
View File
@@ -74,10 +74,6 @@ export type {
} from "../react/core/hooks/transaction/useSendTransaction.js";
export { useSimulateTransaction } from "../react/core/hooks/transaction/useSimulateTransaction.js";
export type { BridgePrepareResult } from "../react/core/hooks/useBridgePrepare.js";
export {
type UseBridgeRoutesParams,
useBridgeRoutes,
} from "../react/core/hooks/useBridgeRoutes.js";
export type { CompletedStatusResult } from "../react/core/hooks/useStepExecutor.js";
export { useActiveAccount } from "../react/core/hooks/wallets/useActiveAccount.js";
// wallet hooks
+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,
@@ -11,50 +11,54 @@ import { sendTransaction } from "../../../transaction/actions/send-transaction.j
import { installPublishedExtension } from "./installPublishedExtension.js";
describe.runIf(process.env.TW_SECRET_KEY)("install extension", () => {
it.sequential("should install extension to a dynamic contract", async () => {
await deployCloneFactory({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
// TODO: Fix this test
it.skip.sequential(
"should install extension to a dynamic contract",
async () => {
await deployCloneFactory({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
const deployed = await deployPublishedContract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
contractId: "EvolvingNFT",
contractParams: {
contractURI: "",
defaultAdmin: TEST_ACCOUNT_A.address,
name: "Evolving nft",
royaltyBps: 0n,
royaltyRecipient: TEST_ACCOUNT_A.address,
saleRecipient: TEST_ACCOUNT_A.address,
symbol: "ENFT",
trustedForwarders: [],
},
});
const deployed = await deployPublishedContract({
account: TEST_ACCOUNT_A,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
contractId: "EvolvingNFT",
contractParams: {
contractURI: "",
defaultAdmin: TEST_ACCOUNT_A.address,
name: "Evolving nft",
royaltyBps: 0n,
royaltyRecipient: TEST_ACCOUNT_A.address,
saleRecipient: TEST_ACCOUNT_A.address,
symbol: "ENFT",
trustedForwarders: [],
},
});
const contract = getContract({
address: deployed,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
const contract = getContract({
address: deployed,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
});
const transaction = installPublishedExtension({
account: TEST_ACCOUNT_A,
contract,
extensionName: "DirectListingsLogic",
});
const transaction = installPublishedExtension({
account: TEST_ACCOUNT_A,
contract,
extensionName: "DirectListingsLogic",
});
await sendTransaction({ account: TEST_ACCOUNT_A, transaction });
await sendTransaction({ account: TEST_ACCOUNT_A, transaction });
const extensions = await readContract({
contract,
method: resolveMethod("getAllExtensions"),
params: [],
});
const extensions = await readContract({
contract,
method: resolveMethod("getAllExtensions"),
params: [],
});
expect(extensions.length).toEqual(4);
});
expect(extensions.length).toEqual(4);
},
);
});
@@ -11,7 +11,8 @@ import { deployPublishedContract } from "../../prebuilts/deploy-published.js";
import { uninstallExtension } from "./uninstallExtension.js";
describe.runIf(process.env.TW_SECRET_KEY)("uninstall extension", () => {
it.sequential(
// TODO: Fix this test
it.skip.sequential(
"should uninstall extension from a dynamic contract",
async () => {
await deployCloneFactory({
@@ -16,6 +16,7 @@ import {
fetchBytecodeFromCompilerMetadata,
} from "../../utils/any-evm/deploy-metadata.js";
import { encodeExtraDataWithUri } from "../../utils/any-evm/encode-extra-data-with-uri.js";
import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js";
import type { Hex } from "../../utils/encoding/hex.js";
import type { Account } from "../../wallets/interfaces/wallet.js";
import { getAllDefaultConstructorParamsForImplementation } from "./get-required-transactions.js";
@@ -213,7 +214,8 @@ export async function deployContractfromDeployMetadata(
if (
deployMetadata.routerType === "dynamic" &&
deployMetadata.defaultExtensions
deployMetadata.defaultExtensions &&
!isZkSyncChain(chain)
) {
for (const e of deployMetadata.defaultExtensions) {
await getOrDeployInfraForPublishedContract({
@@ -253,10 +253,10 @@ export function useSendTransactionCore(args: {
(nativeCost > 0n && nativeBalance.value < nativeCost);
if (shouldShowModal) {
const supportedDestinations = await Bridge.routes({
const tokens = await Bridge.tokens({
client: tx.client,
destinationChainId: tx.chain.id,
destinationTokenAddress: _erc20Value?.tokenAddress,
chainId: tx.chain.id,
tokenAddress: _erc20Value?.tokenAddress,
}).catch((err) => {
trackPayEvent({
client: tx.client,
@@ -269,11 +269,8 @@ export function useSendTransactionCore(args: {
return null;
});
if (
!supportedDestinations ||
supportedDestinations.length === 0
) {
// not a supported destination -> show deposit screen
if (!tokens || tokens.length === 0) {
// not a supported token -> show deposit screen
trackPayEvent({
client: tx.client,
error: JSON.stringify({
@@ -1,137 +0,0 @@
import {
beforeEach,
describe,
expect,
it,
type MockedFunction,
vi,
} from "vitest";
import { routes } from "../../../bridge/Routes.js";
import { ApiError } from "../../../bridge/types/Errors.js";
import type { Route } from "../../../bridge/types/Route.js";
import type { ThirdwebClient } from "../../../client/client.js";
import type { UseBridgeRoutesParams } from "./useBridgeRoutes.js";
// Mock the Bridge routes function
vi.mock("../../../bridge/Routes.js", () => ({
routes: vi.fn(),
}));
const mockRoutes = routes as MockedFunction<typeof routes>;
// Mock client
const mockClient = { clientId: "test" } as ThirdwebClient;
// Mock route data
const mockRouteData: Route[] = [
{
destinationToken: {
address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
chainId: 137,
decimals: 18,
name: "Wrapped Ethereum",
priceUsd: 2000.0,
symbol: "WETH",
},
originToken: {
address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
chainId: 1,
decimals: 18,
name: "Ethereum",
priceUsd: 2000.0,
symbol: "ETH",
},
},
];
describe("useBridgeRoutes", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should export correct hook parameters type", () => {
// Type-only test to verify UseBridgeRoutesParams interface
const params: UseBridgeRoutesParams = {
client: mockClient,
destinationChainId: 137,
enabled: true,
originChainId: 1,
};
expect(params).toBeDefined();
expect(params.client).toBe(mockClient);
expect(params.originChainId).toBe(1);
expect(params.destinationChainId).toBe(137);
expect(params.enabled).toBe(true);
});
it("should handle different parameter combinations", () => {
const fullParams: UseBridgeRoutesParams = {
client: mockClient,
destinationChainId: 137,
destinationTokenAddress: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
enabled: false,
limit: 10,
maxSteps: 3,
offset: 0,
originChainId: 1,
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
sortBy: "popularity",
};
expect(fullParams).toBeDefined();
expect(fullParams.sortBy).toBe("popularity");
expect(fullParams.maxSteps).toBe(3);
expect(fullParams.limit).toBe(10);
expect(fullParams.offset).toBe(0);
});
it("should have optional enabled parameter defaulting to true", () => {
const paramsWithoutEnabled: UseBridgeRoutesParams = {
client: mockClient,
destinationChainId: 137,
originChainId: 1,
};
expect(paramsWithoutEnabled.enabled).toBeUndefined(); // Should be optional
});
it("should validate that Bridge.routes would be called with correct parameters", async () => {
const testParams = {
client: mockClient,
destinationChainId: 137,
originChainId: 1,
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" as const,
};
// Mock the routes function to return our test data
mockRoutes.mockResolvedValue(mockRouteData);
// Directly call the routes function to verify it works with our parameters
const result = await routes(testParams);
expect(mockRoutes).toHaveBeenCalledWith(testParams);
expect(result).toEqual(mockRouteData);
});
it("should handle API errors properly", async () => {
const apiError = new ApiError({
code: "INVALID_INPUT",
message: "Invalid parameters",
statusCode: 400,
});
mockRoutes.mockRejectedValue(apiError);
try {
await routes({
client: mockClient,
destinationChainId: 137,
originChainId: 1,
});
} catch (error) {
expect(error).toBe(apiError);
expect(error).toBeInstanceOf(ApiError);
}
});
});
@@ -1,75 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import type { routes as RoutesTypes } from "../../../bridge/Routes.js";
import { routes } from "../../../bridge/Routes.js";
import { ApiError } from "../../../bridge/types/Errors.js";
import { mapBridgeError } from "../errors/mapBridgeError.js";
/**
* Parameters for the useBridgeRoutes hook
*/
export type UseBridgeRoutesParams = RoutesTypes.Options & {
/**
* Whether to enable the query. Useful for conditional fetching.
* @default true
*/
enabled?: boolean;
};
/**
* Hook that fetches available bridge routes with caching and retry logic
*
* @param params - Parameters for fetching routes including client and filter options
* @returns React Query result with routes data, loading state, and error handling
*
* @example
* ```tsx
* const { data: routes, isLoading, error } = useBridgeRoutes({
* client: thirdwebClient,
* originChainId: 1,
* destinationChainId: 137,
* originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
* });
* ```
*/
export function useBridgeRoutes(params: UseBridgeRoutesParams) {
const { enabled = true, ...routeParams } = params;
return useQuery({
enabled: enabled && !!routeParams.client,
gcTime: 10 * 60 * 1000,
queryFn: () => routes(routeParams),
queryKey: [
"bridge-routes",
{
destinationChainId: routeParams.destinationChainId,
destinationTokenAddress: routeParams.destinationTokenAddress,
limit: routeParams.limit,
maxSteps: routeParams.maxSteps,
offset: routeParams.offset,
originChainId: routeParams.originChainId,
originTokenAddress: routeParams.originTokenAddress,
sortBy: routeParams.sortBy,
},
], // 5 minutes - routes are relatively stable
retry: (failureCount, error) => {
// Handle both ApiError and generic Error instances
if (error instanceof ApiError) {
const bridgeError = mapBridgeError(error);
// Don't retry on client-side errors (4xx)
if (
bridgeError.statusCode &&
bridgeError.statusCode >= 400 &&
bridgeError.statusCode < 500
) {
return false;
}
}
// Retry up to 3 times for server errors or network issues
return failureCount < 3;
}, // 10 minutes garbage collection
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
staleTime: 5 * 60 * 1000, // Exponential backoff, max 30s
});
}
@@ -142,7 +142,9 @@ export function ConnectModal(
const inAppWallet = wallets.find((wallet) => wallet.id === "inApp") as
| Wallet<"inApp">
| undefined;
const externalWallets = wallets.filter((wallet) => wallet.id !== "inApp");
const externalWallets = wallets
.filter((wallet) => wallet.id !== "inApp")
.filter((wallet) => !props.hiddenWallets?.includes(wallet.id));
const showBranding = props.connectModal?.showThirdwebBranding !== false;
const connectionManager = useConnectionManager();
@@ -25,6 +25,7 @@ import type {
import type { CompletedStatusResult } from "../../../core/hooks/useStepExecutor.js";
import type { SupportedTokens } from "../../../core/utils/defaultTokens.js";
import { webWindowAdapter } from "../../adapters/WindowAdapter.js";
import { AutoConnect } from "../AutoConnect/AutoConnect.js";
import connectLocaleEn from "../ConnectWallet/locale/en.js";
import { EmbedContainer } from "../ConnectWallet/Modal/ConnectEmbed.js";
import { DynamicHeight } from "../components/DynamicHeight.js";
@@ -359,6 +360,21 @@ export function BuyWidget(props: BuyWidgetProps) {
className={props.className}
style={props.style}
>
{props.connectOptions?.autoConnect !== false && (
<AutoConnect
client={props.client}
wallets={props.connectOptions?.wallets}
timeout={
typeof props.connectOptions?.autoConnect === "object"
? props.connectOptions?.autoConnect?.timeout
: undefined
}
appMetadata={props.connectOptions?.appMetadata}
accountAbstraction={props.connectOptions?.accountAbstraction}
chain={props.connectOptions?.chain}
/>
)}
<BridgeWidgetContent
{...props}
theme={props.theme || "dark"}
@@ -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>
);
@@ -298,6 +298,7 @@ export function FundWallet(props: FundWalletProps) {
}}
theme={theme}
{...props.connectOptions}
autoConnect={false}
/>
) : (
<Button
@@ -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>
)}
@@ -8,6 +8,7 @@ import walletInfos from "../../../../../wallets/__generated__/wallet-infos.js";
import { NON_SEARCHABLE_WALLETS } from "../../../../../wallets/constants.js";
import { createWallet } from "../../../../../wallets/create-wallet.js";
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
import type { WalletId } from "../../../../../wallets/wallet-types.js";
import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
import { iconSize, spacing } from "../../../../core/design-system/index.js";
import { useSetSelectionData } from "../../../providers/wallet-ui-states-provider.js";
@@ -34,15 +35,16 @@ function AllWalletsUI(props: {
recommendedWallets: Wallet[] | undefined;
connectLocale: ConnectLocale;
disableSelectionDataReset?: boolean;
walletIdsToHide?: WalletId[];
}) {
const { itemsToShow, lastItemRef } = useShowMore<HTMLLIElement>(10, 10);
const setSelectionData = useSetSelectionData();
const walletList = useMemo(() => {
return walletInfos.filter(
(info) => !NON_SEARCHABLE_WALLETS.includes(info.id),
);
}, []);
return walletInfos
.filter((info) => !NON_SEARCHABLE_WALLETS.includes(info.id))
.filter((info) => !props.walletIdsToHide?.includes(info.id));
}, [props.walletIdsToHide]);
const fuseInstance = useMemo(() => {
return new Fuse(walletList, {

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