Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c37af4f191 Version Packages (#8389)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-10 14:31:30 -08:00
Joaquim VergesandGitHub f964b8aaff [SDK] Loosen peer dependency requirements for React Native (#8390) 2025-11-10 10:47:57 -08:00
YashandGitHub 424e6b8f9b Stylus modules (#8383) 2025-11-10 10:33:56 -08:00
MananTank 763d3de5a8 [MNY-303] Various UI/UX Improvements in Bridge components (#8388)
<!--

## 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 various UI/UX improvements across multiple components in the `thirdweb` package, particularly enhancing visual consistency and user interaction within the Bridge and Token Selection components.

### Detailed summary
- Adjusted font weights in several components from `600` to `500`.
- Updated `Spacer` component usage for consistent spacing.
- Enhanced `TokenSelection` UI with tabbed navigation for "All Tokens" and "Your Tokens".
- Added fallback images for icons in `TokenAndChain` and `ChainButton`.
- Refactored `CopyIcon` to use a `Button` instead of a `div` for better accessibility.
- Improved loading states and skeletons in `TokenSelectionScreen`.
- Simplified conditional rendering for transaction details in `PaymentReceipt`.

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

<!-- end pr-codex -->

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

* **New Features**
  * Token selection: separate "All Tokens" and "Your Tokens" tabs with per-tab paging and "Load More"
  * Payment receipts: per-transaction explorer links and copy-to-clipboard controls
  * Enhanced success/details views showing expanded per-transaction status entries

* **UI/UX Improvements**
  * Refined typography weights across Bridge screens for clearer hierarchy
  * Adjusted icon sizing and added visual fallbacks for missing chain/token icons
  * Improved spacing, alignment, scrolling and containment in Bridge layouts
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-10 18:11:43 +00:00
Joaquim VergesandGitHub f6f138ccbc [Portal] Move x402 from payments to top-level section (#8386) 2025-11-09 18:09:15 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f5d5e73e57 Version Packages (#8385)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-09 15:17:10 -08:00
Joaquim VergesandGitHub 673e51b11c [SDK] Replace Celo Alfajores with Celo Sepolia Testnet (#8384) 2025-11-09 14:46:12 -08:00
MananTank 282edf7ccb [PRO-73] Portal: Update hero images (#8381)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on making stylistic updates to the `Hero` component in the `page.tsx` file, including adjustments to padding and text size in the layout.

### Detailed summary
- Changed padding in the `section` from `py-14` to `pt-14 pb-6`.
- Modified paragraph class from `text-lg` to `text-base` for smaller text size on smaller screens while keeping `lg:text-lg`.

> ✨ 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**
  * Adjusted Hero section vertical spacing for improved layout balance.
  * Enhanced introductory text with responsive font sizing and refined spacing across different screen sizes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-07 20:40:50 +00:00
MananTank a87e91b99c [MNY-305] Dashboard: Checksum addresses in tx page (#8380)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing address handling in the `GeneericTxDetails` and `TokenInfo` components by introducing the `getAddress` function from `thirdweb`, ensuring that addresses are consistently formatted.

### Detailed summary
- Added `getAddress` import in `page.tsx` and `bridge-status.tsx`.
- Replaced direct usage of `transaction.from` and `transaction.to` with `fromAddress` and `toAddress` in `GeneericTxDetails`.
- Updated `textToCopy` and `textToShow` in `CopyTextButton` components to use `fromAddress` and `toAddress`.
- Updated `TokenInfo` to use `getAddress` for `props.token.address` and `props.walletAddress`.
- Changed `textToCopy` and `textToShow` in `TokenInfo` to use `tokenAddress`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved address normalization in transaction and bridge status displays to ensure consistent formatting across the dashboard, preventing potential issues with address validation and comparison.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-07 20:28:47 +00:00
MananTank 0e630c4550 [MNY-304] Move engine tx summary request to client side to fix page crash (#8379)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the transaction analytics feature by updating data fetching methods, improving UI components, and enhancing error handling for transaction summaries and charts.

### Detailed summary
- Removed outdated files: `utils.ts` and `filter.tsx`.
- Updated import paths for `TransactionSummaryData`.
- Changed UI text in `tx-chart-ui.tsx` for clarity.
- Enhanced error handling in data fetching functions.
- Introduced `getTransactionsChartData` and `getTransactionAnalyticsSummary` for improved data management.
- Refactored `TransactionsAnalyticsPageContent` to streamline data flow and UI rendering.
- Added new components for date and interval selection in transaction charts.

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

* **Refactor**
  * Analytics now load client-side with a spinner and a streamlined layout; analytics sections only show when transactions exist.

* **New Features**
  * Direct date-range and interval controls in the analytics view.
  * Unified analytics summary and charts presented above the transactions table.
  * Chart header updated to "Transactions" with revised description.

* **Chores**
  * Removed URL-synced filter behavior; filters are local.
  * Empty-chart messaging changed to "No transactions found."
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-07 20:13:34 +00:00
Joaquim VergesandGitHub 11c7bf0955 [Playground] Remove hardcoded limit from wrapFetchWithPayment (#8376) 2025-11-07 02:38:54 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c9e430d092 Version Packages (#8375)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-07 17:09:03 +07:00
FirekeeperandGitHub 1f440d0c97 Add Epic Games as new auth provider (#8343) 2025-11-07 15:59:08 +07:00
Yash094 3a0aac4393 Change TransactionWidget fiat decimals to two (#8366)
<!--

## [SDK] Fix: Format TransactionWidget fiat amounts to 2 decimals

## Notes for the reviewer

This PR updates the `formatMoney` function to display fiat amounts with a maximum of 2 decimal places instead of 5. This standardizes currency formatting for improved readability within the TransactionWidget.

## How to test

- Verify fiat amounts in the TransactionWidget (e.g., payment screens, token selection) are displayed with a maximum of 2 decimal places.
- Unit tests passed.

-->

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

<a href="https://cursor.com/background-agent?bcId=bc-65f07aee-1cee-4915-9656-8e4d9904ae3c"><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-65f07aee-1cee-4915-9656-8e4d9904ae3c"><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 modifying the formatting of token balances in the `formatTokenBalance.ts` file to limit the maximum number of fraction digits displayed.

### Detailed summary
- Changed `maximumFractionDigits` from `5` to `2` in the currency formatting options.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Updated currency formatting to display a maximum of 2 decimal places instead of 5, improving readability and consistency of financial amounts throughout the app while maintaining appropriate minimum precision.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-07 07:36:09 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
7b13015053 Version Packages (#8374)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-06 16:29:15 -08:00
Joaquim VergesandGitHub 7f081618e1 [SDK] Expose useInvalidateBalances() (#8373) 2025-11-06 16:21:38 -08:00
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
120 changed files with 2094 additions and 1003 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ module.exports = [
"/wallets",
"/account-abstraction",
"/payments",
"/payments/x402",
"/x402",
"/nexus",
"/auth",
"/in-app-wallets",
+5
View File
@@ -479,6 +479,11 @@ async function redirects() {
destination: "/tokens",
permanent: false,
},
{
source: "/payments/x402",
destination: "/x402",
permanent: false,
},
];
}
+2 -1
View File
@@ -21,7 +21,8 @@ export type AuthOption =
| "steam"
| "apple"
| "coinbase"
| "line";
| "line"
| "epic";
export type Ecosystem = {
name: string;
+2
View File
@@ -7,6 +7,8 @@ const customClaimAmounts: Record<number, number> = {
531050104: 5,
// Arc testnet
5042002: 1,
// Injective testnet
1439: 1,
};
const defaultClaimAmount = 0.01;
@@ -225,6 +225,10 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
moduleInfo: {
bytecodeUri: selectedModule.metadata.bytecodeUri,
},
isStylus:
(selectedModule.metadata.compilers?.stylus &&
selectedModule.metadata.compilers?.stylus?.length > 0) ||
false,
});
},
queryKey: [
@@ -432,6 +436,7 @@ async function isModuleCompatible(options: {
moduleInfo: {
bytecodeUri: string;
};
isStylus: boolean;
client: ThirdwebClient;
}) {
// 1. get module's bytecode
@@ -444,15 +449,17 @@ async function isModuleCompatible(options: {
// 2. check compatibility with core and installed modules
try {
const isCompatible = await checkModulesCompatibility({
chain: options.contractInfo.chain,
client: options.client,
coreBytecode: options.contractInfo.bytecode,
moduleBytecodes: [
moduleBytecode,
...options.contractInfo.installedModuleBytecodes,
],
});
const isCompatible =
options.isStylus ||
(await checkModulesCompatibility({
chain: options.contractInfo.chain,
client: options.client,
coreBytecode: options.contractInfo.bytecode,
moduleBytecodes: [
moduleBytecode,
...options.contractInfo.installedModuleBytecodes,
],
}));
return isCompatible;
} catch (e) {
@@ -10,7 +10,11 @@ import {
CircleXIcon,
} from "lucide-react";
import Link from "next/link";
import { NATIVE_TOKEN_ADDRESS, type ThirdwebClient } from "thirdweb";
import {
getAddress,
NATIVE_TOKEN_ADDRESS,
type ThirdwebClient,
} from "thirdweb";
import type { Status, Token } from "thirdweb/bridge";
import { status } from "thirdweb/bridge";
import { toTokens } from "thirdweb/utils";
@@ -123,6 +127,8 @@ function TokenInfo(props: {
const isNativeToken =
props.token.address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase();
const tokenAddress = getAddress(props.token.address);
return (
<div className="flex-1 pt-10 pb-9 px-6 lg:px-8">
<div className="flex justify-between items-center">
@@ -206,7 +212,7 @@ function TokenInfo(props: {
<div className="space-y-1">
<p className="text-sm text-foreground ">{props.addressLabel}</p>
<WalletAddress
address={props.walletAddress}
address={getAddress(props.walletAddress)}
client={props.client}
className="py-0.5 h-auto text-sm [&>*]:!font-sans tabular-nums [&>*]:font-normal text-muted-foreground hover:text-foreground"
iconClassName="size-3"
@@ -222,8 +228,8 @@ function TokenInfo(props: {
? `/${chainQuery.data?.slug || props.token.chainId}`
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
}
textToShow={props.token.address}
textToCopy={props.token.address}
textToShow={tokenAddress}
textToCopy={tokenAddress}
copyTooltip="Copy Token Address"
className="-translate-x-1"
/>
@@ -8,7 +8,7 @@ import {
Clock4Icon,
InfoIcon,
} from "lucide-react";
import { toTokens } from "thirdweb";
import { getAddress, toTokens } from "thirdweb";
import { status } from "thirdweb/bridge";
import type { ChainMetadata } from "thirdweb/chains";
import {
@@ -153,6 +153,9 @@ function GeneericTxDetails(props: {
const timestamp = getDatefromTimestamp(block.timestamp);
const fromAddress = getAddress(transaction.from);
const toAddress = transaction.to ? getAddress(transaction.to) : undefined;
return (
<div className="border rounded-xl p-4 lg:p-6 bg-card">
{/* section 1 */}
@@ -216,8 +219,8 @@ function GeneericTxDetails(props: {
<section className="border-b py-6 space-y-5 lg:space-y-3">
<GridItem label="From" tooltip="The sending party of the transaction.">
<CopyTextButton
textToCopy={transaction.from}
textToShow={transaction.from}
textToCopy={fromAddress}
textToShow={fromAddress}
tooltip="Copy from address"
variant="ghost"
copyIconPosition="right"
@@ -225,14 +228,14 @@ function GeneericTxDetails(props: {
/>
</GridItem>
{transaction.to && (
{toAddress && (
<GridItem
label="Interacted with (To)"
tooltip="The receiving party of the transaction (could be a contract address)."
>
<CopyTextButton
textToCopy={transaction.to}
textToShow={transaction.to}
textToCopy={toAddress}
textToShow={toAddress}
tooltip="Copy to address"
variant="ghost"
copyIconPosition="right"
@@ -183,13 +183,64 @@ const STYLUS = {
],
description:
"Airdrop your NFTs or tokens to a large number of recipients. Built with Arbitrum Stylus.",
displayName: "Arbitrum Stylus Contracts",
displayName: "Airdrop - Arbitrum Stylus",
id: "stylus",
name: "Stylus",
showInExplore: true,
isBeta: true,
} satisfies ExploreCategory;
const MODULAR_CONTRACTS_STYLUS = {
contracts: [
// erc721 token
[
"thirdweb.eth/ERC721CoreInitializable",
[
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC721",
"deployer.thirdweb.eth/BatchMetadataERC721",
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC721",
],
{
description: "ERC721 NFTs that only owners can mint.",
title: "Modular NFT Collection",
},
],
// erc1155 token
[
"thirdweb.eth/ERC1155CoreInitializable",
[
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC1155",
"deployer.thirdweb.eth/BatchMetadataERC1155",
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC1155",
"deployer.thirdweb.eth/SequentialTokenIdERC1155",
],
{
description: "ERC1155 NFTs that only owners can mint.",
title: "Modular Edition",
},
],
// erc20 token
[
"thirdweb.eth/ERC20CoreInitializable",
[
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC20",
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC20",
],
{
description: "ERC20 Tokens that only owners can mint.",
title: "Modular Token",
},
],
],
description:
"Collection of highly customizable and upgradeable smart contracts built with the modular contracts framework.",
displayName: "Modular Contracts - Arbitrum Stylus",
id: "modular-contracts",
name: "modular",
showInExplore: true,
isBeta: true,
} satisfies ExploreCategory;
const CATEGORIES: Record<string, ExploreCategory> = {
[POPULAR.id]: POPULAR,
[NFTS.id]: NFTS,
@@ -203,6 +254,7 @@ const CATEGORIES: Record<string, ExploreCategory> = {
[COMMERCE.id]: COMMERCE,
[STAKING.id]: STAKING,
[GOVERNANCE.id]: GOVERNANCE,
[MODULAR_CONTRACTS_STYLUS.id]: MODULAR_CONTRACTS_STYLUS,
};
export function getCategory(id: string) {
@@ -58,6 +58,7 @@ const authOptions = [
"apple",
"coinbase",
"line",
"epic",
] as const satisfies AuthOption[];
type AuthOptionsFormData = {
@@ -1,45 +1,95 @@
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
"use client";
import { useQuery } from "@tanstack/react-query";
import { Spinner } from "@workspace/ui/components/spinner";
import type { ThirdwebClient } from "thirdweb";
import type { Project } from "@/api/project/projects";
import { UnifiedTransactionsTable } from "../components/transactions-table.client";
import { getTransactionAnalyticsSummary } from "../lib/analytics-summary.client";
import type { Wallet } from "../server-wallets/wallet-table/types";
import { TransactionAnalyticsFilter } from "./filter";
import { TransactionsChartCard } from "./tx-chart/tx-chart";
import type { SolanaWallet } from "../solana-wallets/wallet-table/types";
import { EngineChecklist } from "./ftux.client";
import { TransactionAnalyticsSummary } from "./summary";
import { TransactionsAnalytics } from "./tx-chart/tx-chart";
export function TransactionsAnalyticsPageContent(props: {
searchParams: {
from?: string | undefined | string[];
to?: string | undefined | string[];
interval?: string | undefined | string[];
};
project: Project;
showAnalytics: boolean;
wallets?: Wallet[];
wallets: Wallet[];
teamSlug: string;
client: ThirdwebClient;
authToken: string;
teamId: string;
isManagedVault: boolean;
testTxWithWallet: string | undefined;
testSolanaTxWithWallet: string | undefined;
solanaWallets: SolanaWallet[];
}) {
return (
<ResponsiveSearchParamsProvider value={props.searchParams}>
<div className="flex grow flex-col gap-6">
{props.showAnalytics && (
<>
<div className="flex justify-end">
<TransactionAnalyticsFilter />
</div>
<TransactionsChartCard
project={props.project}
searchParams={props.searchParams}
teamSlug={props.teamSlug}
wallets={props.wallets ?? []}
/>
</>
)}
<UnifiedTransactionsTable
client={props.client}
project={props.project}
teamSlug={props.teamSlug}
/>
const engineTxSummaryQuery = useQuery({
queryKey: [
"engine-tx-analytics-summary",
props.teamId,
props.project.publishableKey,
props.authToken,
],
queryFn: async () => {
const data = await getTransactionAnalyticsSummary({
clientId: props.project.publishableKey,
teamId: props.teamId,
authToken: props.authToken,
});
return data;
},
refetchOnWindowFocus: false,
refetchOnMount: false,
});
if (engineTxSummaryQuery.isPending) {
return (
<div className="flex h-[642px] grow items-center justify-center bg-card rounded-xl border">
<Spinner className="size-10" />
</div>
</ResponsiveSearchParamsProvider>
);
}
const hasTransactions = engineTxSummaryQuery.data
? engineTxSummaryQuery.data.totalCount > 0
: false;
return (
<div className="flex grow flex-col gap-10">
<EngineChecklist
isManagedVault={props.isManagedVault}
client={props.client}
hasTransactions={hasTransactions}
project={props.project}
teamSlug={props.teamSlug}
testTxWithWallet={props.testTxWithWallet}
testSolanaTxWithWallet={props.testSolanaTxWithWallet}
wallets={props.wallets}
solanaWallets={props.solanaWallets}
/>
{props.showAnalytics && hasTransactions && (
<div className="flex flex-col gap-6">
<TransactionAnalyticsSummary
clientId={props.project.publishableKey}
teamId={props.project.teamId}
initialData={engineTxSummaryQuery.data}
/>
<TransactionsAnalytics
project={props.project}
authToken={props.authToken}
teamSlug={props.teamSlug}
wallets={props.wallets ?? []}
/>
</div>
)}
<UnifiedTransactionsTable
client={props.client}
project={props.project}
teamSlug={props.teamSlug}
/>
</div>
);
}
@@ -1,51 +0,0 @@
"use client";
import {
useResponsiveSearchParams,
useSetResponsiveSearchParams,
} from "responsive-rsc";
import { DateRangeSelector } from "@/components/analytics/date-range-selector";
import { IntervalSelector } from "@/components/analytics/interval-selector";
import { normalizeTimeISOString } from "@/lib/time";
import { getTxAnalyticsFiltersFromSearchParams } from "../lib/utils";
export function TransactionAnalyticsFilter() {
const responsiveSearchParams = useResponsiveSearchParams();
const setResponsiveSearchParams = useSetResponsiveSearchParams();
const { range, interval } = getTxAnalyticsFiltersFromSearchParams({
from: responsiveSearchParams.from,
interval: responsiveSearchParams.interval,
to: responsiveSearchParams.to,
});
return (
<div className="no-scrollbar flex items-center gap-3 max-sm:overflow-auto">
<DateRangeSelector
popoverAlign="end"
range={range}
setRange={(newRange) => {
setResponsiveSearchParams((v) => {
return {
...v,
from: normalizeTimeISOString(newRange.from),
to: normalizeTimeISOString(newRange.to),
};
});
}}
/>
<IntervalSelector
intervalType={interval}
setIntervalType={(newInterval) => {
setResponsiveSearchParams((v) => {
return {
...v,
interval: newInterval,
};
});
}}
/>
</div>
);
}
@@ -1,7 +1,7 @@
import { ActivityIcon, CoinsIcon } from "lucide-react";
import { toEther } from "thirdweb/utils";
import { StatCard } from "@/components/analytics/stat"; // Assuming correct path
import type { TransactionSummaryData } from "../lib/analytics";
import type { TransactionSummaryData } from "../lib/analytics-summary.client";
// Renders the UI based on fetched data or pending state
function TransactionAnalyticsSummaryUI(props: {
@@ -0,0 +1,75 @@
"use client";
import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs";
import type { TransactionStats } from "@/types/analytics";
export async function getTransactionsChartData({
teamId,
clientId,
from,
to,
interval,
authToken,
}: {
teamId: string;
clientId: string;
from: string;
to: string;
interval: "day" | "week";
authToken: string;
}): Promise<TransactionStats[]> {
const filters = {
endDate: to,
resolution: interval,
startDate: from,
};
const response = await fetch(
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics`,
{
body: JSON.stringify(filters),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": clientId,
"x-team-id": teamId,
},
method: "POST",
},
);
if (!response.ok) {
if (response.status === 401 || response.status === 400) {
return [];
}
// TODO - need to handle this error state, like we do with the connect charts
throw new Error(
`Error fetching transactions chart data: ${response.status} ${
response.statusText
} - ${await response.text().catch(() => "Unknown error")}`,
);
}
type TransactionsChartResponse = {
result: {
analytics: Array<{
timeBucket: string;
chainId: string;
count: number;
}>;
metadata: {
resolution: string;
startDate: string;
endDate: string;
};
};
};
const data = (await response.json()) as TransactionsChartResponse;
return data.result.analytics.map((stat) => ({
chainId: Number(stat.chainId),
count: stat.count,
date: stat.timeBucket,
}));
}
@@ -108,10 +108,10 @@ export function TransactionsChartCardUI(props: {
customHeader={
<div className="relative px-6 pt-6">
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">
Daily Transactions
Transactions
</h3>
<p className="text-muted-foreground text-sm">
Amount of daily transactions by chain.
transactions broken down by chain
</p>
<div className="top-6 right-6 mb-8 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
@@ -194,13 +194,7 @@ function EmptyChartContent(props: {
</div>
</>
) : (
<p className="flex items-center gap-2 rounded-full border bg-background px-3.5 py-1.5 text-sm">
<span className="!pointer-events-auto relative flex size-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75" />
<span className="relative inline-flex size-2 rounded-full bg-primary" />
</span>
Waiting for transactions...
</p>
<p className="text-sm text-muted-foreground">No transactions found</p>
)}
</div>
);
@@ -1,73 +1,68 @@
import { ResponsiveSuspense } from "responsive-rsc";
"use client";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import type { Project } from "@/api/project/projects";
import { getTransactionsChart } from "../../lib/analytics";
import { getTxAnalyticsFiltersFromSearchParams } from "../../lib/utils";
import {
DateRangeSelector,
getLastNDaysRange,
} from "@/components/analytics/date-range-selector";
import { IntervalSelector } from "@/components/analytics/interval-selector";
import { normalizeTimeISOString } from "@/lib/time";
import type { Wallet } from "../../server-wallets/wallet-table/types";
import { getTransactionsChartData } from "./data";
import { TransactionsChartCardUI } from "./tx-chart-ui";
async function AsyncTransactionsChartCard(props: {
from: string;
to: string;
interval: "day" | "week";
export function TransactionsAnalytics(props: {
project: Project;
wallets: Wallet[];
authToken: string;
teamSlug: string;
}) {
const data = await getTransactionsChart({
const [range, setRange] = useState(() => getLastNDaysRange("last-30"));
const [interval, setInterval] = useState<"day" | "week">("day");
const params = {
clientId: props.project.publishableKey,
from: props.from,
interval: props.interval,
from: normalizeTimeISOString(range.from),
interval: interval,
teamId: props.project.teamId,
to: props.to,
to: normalizeTimeISOString(range.to),
authToken: props.authToken,
};
const engineTxAnalytics = useQuery({
queryKey: ["engine-tx-analytics", params],
queryFn: async () => {
const data = await getTransactionsChartData(params);
return data;
},
refetchOnWindowFocus: false,
refetchOnMount: false,
});
return (
<TransactionsChartCardUI
isPending={false}
project={props.project}
teamSlug={props.teamSlug}
userOpStats={data}
wallets={props.wallets}
/>
);
}
<div className="flex flex-col gap-6">
<div className="flex justify-end">
<div className="no-scrollbar flex items-center gap-3 max-sm:overflow-auto">
<DateRangeSelector
popoverAlign="end"
range={range}
setRange={setRange}
/>
export function TransactionsChartCard(props: {
searchParams: {
from?: string | undefined | string[];
to?: string | undefined | string[];
interval?: string | undefined | string[];
};
project: Project;
wallets: Wallet[];
teamSlug: string;
}) {
const { range, interval } = getTxAnalyticsFiltersFromSearchParams(
props.searchParams,
);
return (
<ResponsiveSuspense
// TODO - change this if this component does not end up using these params
fallback={
<TransactionsChartCardUI
isPending={true}
project={props.project}
teamSlug={props.teamSlug}
userOpStats={[]}
wallets={[]}
/>
}
searchParamsUsed={["from", "to", "interval"]}
>
<AsyncTransactionsChartCard
from={range.from.toISOString()}
interval={interval}
<IntervalSelector
intervalType={interval}
setIntervalType={setInterval}
/>
</div>
</div>
<TransactionsChartCardUI
isPending={engineTxAnalytics.isPending}
project={props.project}
teamSlug={props.teamSlug}
to={range.to.toISOString()}
userOpStats={engineTxAnalytics.data ?? []}
wallets={props.wallets}
/>
</ResponsiveSuspense>
</div>
);
}
@@ -0,0 +1,75 @@
"use client";
import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs";
export type TransactionSummaryData = {
totalCount: number;
totalGasCostWei: string;
totalGasUnitsUsed: string; // Keep fetched data structure
};
type AnalyticsSummaryApiResponse = {
result: {
summary: {
totalCount: number;
totalGasCostWei: string;
totalGasUnitsUsed: string;
};
metadata: {
startDate?: string;
endDate?: string;
};
};
};
// Fetches data from the /analytics-summary endpoint
export async function getTransactionAnalyticsSummary(props: {
teamId: string;
clientId: string;
authToken: string;
}): Promise<TransactionSummaryData> {
const body = {};
const defaultData: TransactionSummaryData = {
totalCount: 0,
totalGasCostWei: "0",
totalGasUnitsUsed: "0",
};
try {
const response = await fetch(
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics-summary`,
{
body: JSON.stringify(body),
headers: {
Authorization: `Bearer ${props.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
},
method: "POST",
},
);
if (!response.ok) {
if (response.status === 401 || response.status === 400) {
console.error("Unauthorized fetching transaction summary");
return defaultData;
}
const errorText = await response.text();
throw new Error(
`Error fetching transaction summary: ${response.status} ${response.statusText} - ${errorText}`,
);
}
const data = (await response.json()) as AnalyticsSummaryApiResponse;
return {
totalCount: data.result.summary.totalCount ?? 0,
totalGasCostWei: data.result.summary.totalGasCostWei ?? "0",
totalGasUnitsUsed: data.result.summary.totalGasUnitsUsed ?? "0",
};
} catch (error) {
console.error("Failed to fetch transaction summary:", error);
return defaultData;
}
}
@@ -1,157 +1,10 @@
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs";
import type { TransactionStats } from "@/types/analytics";
import type {
Transaction,
TransactionsResponse,
} from "../analytics/tx-table/types";
// Define the structure of the data we expect back from our fetch function
export type TransactionSummaryData = {
totalCount: number;
totalGasCostWei: string;
totalGasUnitsUsed: string; // Keep fetched data structure
};
// Define the structure of the API response
type AnalyticsSummaryApiResponse = {
result: {
summary: {
totalCount: number;
totalGasCostWei: string;
totalGasUnitsUsed: string;
};
metadata: {
startDate?: string;
endDate?: string;
};
};
};
// Fetches data from the /analytics-summary endpoint
export async function getTransactionAnalyticsSummary(props: {
teamId: string;
clientId: string;
}): Promise<TransactionSummaryData> {
const authToken = await getAuthToken();
const body = {};
const defaultData: TransactionSummaryData = {
totalCount: 0,
totalGasCostWei: "0",
totalGasUnitsUsed: "0",
};
try {
const response = await fetch(
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics-summary`,
{
body: JSON.stringify(body),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
},
method: "POST",
},
);
if (!response.ok) {
if (response.status === 401 || response.status === 400) {
console.error("Unauthorized fetching transaction summary");
return defaultData;
}
const errorText = await response.text();
throw new Error(
`Error fetching transaction summary: ${response.status} ${response.statusText} - ${errorText}`,
);
}
const data = (await response.json()) as AnalyticsSummaryApiResponse;
return {
totalCount: data.result.summary.totalCount ?? 0,
totalGasCostWei: data.result.summary.totalGasCostWei ?? "0",
totalGasUnitsUsed: data.result.summary.totalGasUnitsUsed ?? "0",
};
} catch (error) {
console.error("Failed to fetch transaction summary:", error);
return defaultData;
}
}
export async function getTransactionsChart({
teamId,
clientId,
from,
to,
interval,
}: {
teamId: string;
clientId: string;
from: string;
to: string;
interval: "day" | "week";
}): Promise<TransactionStats[]> {
const authToken = await getAuthToken();
const filters = {
endDate: to,
resolution: interval,
startDate: from,
};
const response = await fetch(
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics`,
{
body: JSON.stringify(filters),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": clientId,
"x-team-id": teamId,
},
method: "POST",
},
);
if (!response.ok) {
if (response.status === 401 || response.status === 400) {
return [];
}
// TODO - need to handle this error state, like we do with the connect charts
throw new Error(
`Error fetching transactions chart data: ${response.status} ${
response.statusText
} - ${await response.text().catch(() => "Unknown error")}`,
);
}
type TransactionsChartResponse = {
result: {
analytics: Array<{
timeBucket: string;
chainId: string;
count: number;
}>;
metadata: {
resolution: string;
startDate: string;
endDate: string;
};
};
};
const data = (await response.json()) as TransactionsChartResponse;
return data.result.analytics.map((stat) => ({
chainId: Number(stat.chainId),
count: stat.count,
date: stat.timeBucket,
}));
}
export async function getSingleTransaction({
teamId,
clientId,
@@ -1,14 +0,0 @@
import { getFiltersFromSearchParams } from "@/lib/time";
export function getTxAnalyticsFiltersFromSearchParams(params: {
from?: string | undefined | string[];
to?: string | undefined | string[];
interval?: string | undefined | string[];
}) {
return getFiltersFromSearchParams({
defaultRange: "last-30",
from: params.from,
interval: params.interval,
to: params.to,
});
}
@@ -7,10 +7,7 @@ import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { TransactionsAnalyticsPageContent } from "./analytics/analytics-page";
import { EngineChecklist } from "./analytics/ftux.client";
import { TransactionAnalyticsSummary } from "./analytics/summary";
import { ServerWalletsTable } from "./components/server-wallets-table.client";
import { getTransactionAnalyticsSummary } from "./lib/analytics";
import type { Wallet } from "./server-wallets/wallet-table/types";
import { listSolanaAccounts } from "./solana-wallets/lib/vault.client";
import type { SolanaWallet } from "./solana-wallets/wallet-table/types";
@@ -110,12 +107,6 @@ export default async function TransactionsAnalyticsPage(props: {
const isSolanaPermissionError =
solanaAccounts.error?.message.includes("AUTH_INSUFFICIENT_SCOPE") ?? false;
const initialData = await getTransactionAnalyticsSummary({
clientId: project.publishableKey,
teamId: project.teamId,
}).catch(() => undefined);
const hasTransactions = initialData ? initialData.totalCount > 0 : false;
const client = getClientThirdwebClient({
jwt: authToken,
teamId: project.teamId,
@@ -152,41 +143,30 @@ export default async function TransactionsAnalyticsPage(props: {
}}
>
<div className="flex flex-col gap-10">
<EngineChecklist
isManagedVault={isManagedVault}
client={client}
hasTransactions={hasTransactions}
project={project}
teamSlug={params.team_slug}
testTxWithWallet={searchParams.testTxWithWallet as string | undefined}
testSolanaTxWithWallet={
searchParams.testSolanaTxWithWallet as string | undefined
}
wallets={wallets ?? []}
solanaWallets={solanaAccounts.data.items}
/>
{hasTransactions &&
!searchParams.testTxWithWallet &&
!searchParams.testSolanaTxWithWallet && (
<TransactionAnalyticsSummary
clientId={project.publishableKey}
initialData={initialData}
teamId={project.teamId}
/>
)}
{/* transactions */}
<TransactionsAnalyticsPageContent
client={client}
authToken={authToken}
project={project}
searchParams={searchParams}
showAnalytics={
hasTransactions &&
!searchParams.testTxWithWallet &&
!searchParams.testSolanaTxWithWallet
}
teamSlug={params.team_slug}
wallets={wallets}
wallets={wallets ?? []}
solanaWallets={solanaAccounts.data.items}
teamId={project.teamId}
isManagedVault={isManagedVault}
testTxWithWallet={
typeof searchParams.testTxWithWallet === "string"
? searchParams.testTxWithWallet
: undefined
}
testSolanaTxWithWallet={
typeof searchParams.testSolanaTxWithWallet === "string"
? searchParams.testSolanaTxWithWallet
: undefined
}
/>
{/* Server Wallets (EVM + Solana) */}
@@ -87,6 +87,7 @@ const authMethodsToPickFrom: InAppWalletAuth[] = [
"line",
"x",
"tiktok",
"epic",
"coinbase",
"farcaster",
"telegram",
+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",
@@ -43,7 +43,6 @@ export function X402RightSection(props: { options: X402PlaygroundOptions }) {
fetch,
THIRDWEB_CLIENT,
activeWallet,
BigInt(1 * 10 ** 18),
);
const searchParams = new URLSearchParams();
searchParams.set("chainId", props.options.chain.id.toString());
@@ -20,6 +20,7 @@ const allInAppWalletLoginMethods: InAppWalletAuth[] = [
"line",
"x",
"tiktok",
"epic",
"facebook",
"apple",
"coinbase",
+8
View File
@@ -742,6 +742,14 @@ const paymentRedirects = {
"/payments/swap": "/bridge/swap",
"/payments/tokens": "/bridge/tokens",
"/payments/routes": "/bridge/routes",
"/payments/x402": "/x402",
"/payments/x402/:path*": "/x402/:path*",
"/payments/fund": "/bridge/fund",
"/payments/products": "/bridge/products",
"/payments/transactions": "/bridge/transactions",
"/payments/send": "/bridge/send",
"/payments/webhooks": "/bridge/webhooks",
"/payments/custom-data": "/bridge/custom-data",
};
const contractRedirects = {
+2 -2
View File
@@ -40,8 +40,8 @@ const links = [
name: "Contracts",
},
{
href: "/payments",
name: "Payments",
href: "/x402",
name: "x402",
},
{
href: "/bridge",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 310 KiB

@@ -188,8 +188,8 @@ Any purchase data you provide to a payment will be included on all webhooks, sta
To connect with other auth strategies, use external wallets, or sponsor gas for users, check out the following guides:
- [Send a Payment](/payments/send)
- [Webhooks](/payments/webhooks)
- [Send a Payment](/bridge/send)
- [Webhooks](/bridge/webhooks)
## Explore Full API References
@@ -74,8 +74,8 @@ The BuyWidget handles the complete payment flow, supporting both crypto and fiat
## Going Further
- [Custom Payment Data](/payments/custom-data) - Attach metadata to payments
- [Webhooks](/payments/webhooks) - Get notified when payments complete
- [Custom Payment Data](/bridge/custom-data) - Attach metadata to payments
- [Webhooks](/bridge/webhooks) - Get notified when payments complete
## API Reference
+1 -1
View File
@@ -97,7 +97,7 @@ const preparedQuote = await Bridge.Buy.prepare({
The prepared quote will contain details about the payment, including the transactions needed to execute it.
You can execute the included transactions using the wallet of your choice. To learn how to send it using thirdweb Wallets, checkout the [Send a Payment](/payments/send) guide.
You can execute the included transactions using the wallet of your choice. To learn how to send it using thirdweb Wallets, checkout the [Send a Payment](/bridge/send) guide.
</TabsContent>
@@ -77,8 +77,8 @@ The CheckoutWidget handles the complete payment flow, supporting both crypto and
## Going Further
- [Custom Payment Data](/payments/custom-data) - Attach metadata to payments
- [Webhooks](/payments/webhooks) - Get notified when payments complete
- [Custom Payment Data](/bridge/custom-data) - Attach metadata to payments
- [Webhooks](/bridge/webhooks) - Get notified when payments complete
## API Reference
+2 -2
View File
@@ -52,14 +52,14 @@ client,
});
```
You can execute this quote exactly like a buy. See the [Send a Payment](/payments/send) guide for more details on this process.
You can execute this quote exactly like a buy. See the [Send a Payment](/bridge/send) guide for more details on this process.
## 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)
- [Send a Payment](/bridge/send)
## Explore Full API References
@@ -262,8 +262,8 @@ Once this code completes, your payment has been fully executed. Payments normall
## Going further
- [Sell a Product](/payments/products)
- [Token Prices](/payments/tokens)
- [Sell a Product](/bridge/products)
- [Token Prices](/bridge/tokens)
## Explore API References
+30 -6
View File
@@ -16,21 +16,45 @@ export const sidebar: SideBar = {
{
isCollapsible: false,
links: [
{
href: `${bridgeSlug}/sell`,
name: "Sell Tokens",
},
{
href: `${bridgeSlug}/swap`,
name: "Swap Tokens",
},
{
href: `${bridgeSlug}/bridge-widget-script`,
name: "BridgeWidget Script",
},
{
href: `${bridgeSlug}/fund`,
name: "Fund Wallets",
},
{
href: `${bridgeSlug}/products`,
name: "Sell a Product",
},
{
href: `${bridgeSlug}/transactions`,
name: "Pay for Transactions",
},
{
href: `${bridgeSlug}/send`,
name: "Send a Payment",
},
{
href: `${bridgeSlug}/sell`,
name: "Sell Tokens",
},
{
href: `${bridgeSlug}/tokens`,
name: "Get Token Prices",
},
{
href: `${bridgeSlug}/bridge-widget-script`,
name: "BridgeWidget Script",
href: `${bridgeSlug}/webhooks`,
name: "Webhooks",
},
{
href: `${bridgeSlug}/custom-data`,
name: "Custom Data",
},
],
name: "Guides",
+2 -2
View File
@@ -317,9 +317,9 @@ async function swapEthToMatic(amount: string, userAccount: Account) {
## Going Further
- [Send a Payment](/payments/send) - Learn about peer-to-peer transfers
- [Send a Payment](/bridge/send) - Learn about peer-to-peer transfers
- [Token Prices](/bridge/tokens) - Access real-time token pricing
- [Webhooks](/payments/webhooks) - Get notified when swaps complete
- [Webhooks](/bridge/webhooks) - Get notified when swaps complete
## API Reference
+12 -2
View File
@@ -59,7 +59,17 @@ This returns token information with current price:
symbol: "USDC",
name: "USD Coin",
iconUri: "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png",
priceUsd: 1.00
prices: {
USD: 1.00,
EUR: 0.86,
GBP: 0.74,
JPY: 148.85,
KRW: 1393.14,
CNY: 7.18,
INR: 86.20,
NOK: 10.18,
SEK: 9.68,
}
}
]
```
@@ -118,7 +128,7 @@ 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)
- [Send a Payment](/bridge/send)
## Explore Full API References
@@ -86,8 +86,8 @@ The TransactionWidget handles the complete payment flow, supporting both crypto
## Going Further
- [Custom Payment Data](/payments/custom-data) - Attach metadata to payments
- [Webhooks](/payments/webhooks) - Get notified when payments complete
- [Custom Payment Data](/bridge/custom-data) - Attach metadata to payments
- [Webhooks](/bridge/webhooks) - Get notified when payments complete
## API Reference
@@ -221,8 +221,8 @@ 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)
- [Get Token Prices](/payments/tokens)
- [Send a Payment](/bridge/send)
- [Get Token Prices](/bridge/tokens)
## Explore Full API References
@@ -0,0 +1,160 @@
import { Step, Steps, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "Stylus Minting Modules | thirdweb Documentation",
description:
"Customizable rust modules written for thirdweb's modular contracts",
image: {
title: "Stylus Minting Modules",
icon: "contracts",
},
});
# Stylus Minting Modules
You can now install rust based modules into a solidity based [modular contract](https://github.com/thirdweb-dev/modular-contracts/blob/dev/design-document.md).
This plug-n-play model also allows users to write their own custom minting logic in rust, taking advantage of Stylus' interoperability and gas savings. These modules work with existing router contracts and modules written in solidity.
## Deploy through dashboard
Deploying a Modular Contract and installing modules is easy through the thirdweb dashboard and ideal when you don't want to modify any code on the contract.
<Steps>
<Step title="Select Modular Contract for Stylus">
Navigate to the Modular Contracts section on Explore and select any token contract for your project.
</Step>
<Step title="Deploy Contract">
Select Arbitrum Sepolia or any other Stylus-supported network, then select Deploy. Once you have deployed one of these contracts, the modules come pre-installed, both solidity and rust ones.
</Step>
<Step title="Edit Modules">
If you wish to install a custom module, navigate to modules tab on your contract page. Enter the publisher address and select module from the dropdown, and click install.
You can uninstall a module from the same page.
Here's a list of prebuilt Rust / Stylus specific modules provided by thirdweb:
- [Mintable ERC20](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusMintableERC20)
- [Transferable ERC20](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusTransferableERC20)
- [Mintable ERC721](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusMintableERC721)
- [Transferable ERC721](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusTransferableERC721)
- [Mintable ERC1155](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusMintableERC1155)
- [Transferable ERC1155](https://thirdweb.com/0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusTransferableERC1155)
You can customize these modules via thirdweb CLI as described below.
</Step>
</Steps>
## Build with CLI
If you want to modify the contract code or publish a custom module, you can use the thirdweb CLI.
<Steps>
<Step title="Create a new Stylus module">
In your CLI, run the following command to create a new directory with an template module. Select the module template from the list shown.
```bash
npx thirdweb create-stylus
```
</Step>
<Step title="Modify logic">
In the `src/lib.rs` file you can modify the contract logic such as adding fees, gating logic, analytics events, and more.
</Step>
<Step title="Build & Test">
To build your project, run the following command:
```bash
cargo stylus build
```
</Step>
<Step title="Deploy or Publish Your Module">
You can publish your module and install it to your modular contract. Publishing stores your module metadata in thirdweb’s on-chain registry so anyone (including you) can use that exact version later with a few clicks.
To publish your module, ensure you have your thirdweb secret key from your created project, then run the following command:
```bash
npx thirdweb publish-stylus -k YOUR_TW_SECRET_KEY
```
Once published, the module is now available to install via thirdweb dashboard from the modules tab of your contract.
</Step>
</Steps>
## Interacting with the Contract
Using the thirdweb SDKs, you can interact with your Stylus Modular contracts / modules to mint tokens, transfer ownership, and more.
The following describes how to interact with a ERC721 modular contract using thirdweb SDK. (Swap for ERC20 or ERC1155 variants as needed.)
### Upload metadata
```ts
import { BatchMetadataERC721 } from "thirdweb/modules";
const transaction = BatchMetadataERC721.uploadMetadata({
contract,
metadatas: [
{ name: "My NFT", description: "This is my NFT" },
],
});
await sendTransaction({
transaction,
account,
});
```
### Mint with role
```typescript
import { MintableERC721 } from "thirdweb/modules";
const transaction = MintableERC721.mintWithRole({
contract,
to: "0x...", // Address to mint tokens to
nfts: [
{
name: "My NFT",
description: "This is my NFT",
image: "ipfs://...",
},
],
});
// Send the transaction
await sendTransaction({ transaction, account });
```
### Set transfer rules
```ts
import { sendTransaction } from "thirdweb";
import { TransferableERC20 } from "thirdweb/modules";
const transaction = TransferableERC20.setTransferable({
contract,
enableTransfer: ...,
overrides: {
...
}
});
// Send the transaction
await sendTransaction({ transaction, account });
```
### Resources
- [Modular Contracts Design Doc](https://github.com/thirdweb-dev/modular-contracts/blob/dev/design-document.md)
@@ -0,0 +1,240 @@
import { Step, Steps, createMetadata } from "@doc";
export const metadata = createMetadata({
title: "Stylus Zk Mint Contract | thirdweb Documentation",
description:
"Stylus Zk Minting contract and app template",
image: {
title: "Stylus Zk Mint",
icon: "contracts",
},
});
# ZK Minting with Stylus
You can now create and deploy ZK Proof based token contracts with Stylus. The templates provide you with Stylus contracts, ZK circuit files, and a ready-to-deploy Next.js app + API for proof generation and minting.
Follow the steps below to learn how to build a privacy-preserving ERC721 token system using Zero-Knowledge proofs on Arbitrum Stylus. Users can mint tokens by proving they own a minimum amount of ETH without revealing their exact balance. However, you can customize the circuits and the contracts to add your own checks for minting.
## What you'll build
- ZK Circuit: Proves token ownership without revealing exact balances
- Stylus Contract: Rust-based ERC721 contract that verifies ZK proofs
- Frontend: Next.js app for generating proofs and minting tokens
- Oracle System: Secure balance verification mechanism
## Prerequisites
- [Create a project on your thirdweb account](https://thirdweb.com/dashboard)
- Install thirdweb CLI by running `npm install -g thirdweb`
- Install Rust tool chain by running `curl https://sh.rustup.rs -sSf | sh` or visit rust-lang.org
- Install solc by running `npm install -g solc` or visit soliditylang.org
- Install Node version 18 or higher
<Steps>
<Step title="Project Setup">
In your CLI, run the following command to create a new directory with an template module. Select the module template from the list shown.
```bash
npx thirdweb create-stylus
```
Select **"Stylus ZK ERC721"** from the dropdown menu. This will:
- Clone the repository to your machine
- Set up the project structure
- Install basic dependencies
</Step>
<Step title="Install Dependencies">
Install dependencies for all components:
```bash
# Install root dependencies
pnpm install
# Install circuit dependencies
cd circuits
pnpm install
cd ..
# Install frontend dependencies
cd app
pnpm install
cd ..
```
</Step>
<Step title="Generate Cryptographic Keys">
Run the setup script to generate oracle keys and build the ZK circuit:
```bash
chmod +x setup.sh
./setup.sh
```
This script will:
- Generate a random oracle secret key
- Inject the secret into the ZK circuit
- Compile the circuit with circom
- Generate proving and verification keys
- Create the trusted setup for Groth16
**⚠️ Important**: The oracle secret is critical for security. Keep it private!
</Step>
<Step title="Deploy the Contract">
### Using thirdweb CLI
```bash
cd contracts
npx thirdweb@latest deploy-stylus -k <THIRDWEB_SECRET_KEY>
```
### Using Stylus CLI (Alternative)
```bash
cd contracts
cargo stylus deploy --endpoint arbitrum-sepolia
```
Copy the deployed contract address - you'll need it for the frontend.
</Step>
<Step title="Configure the Frontend">
Update the contract address in your frontend:
```bash
cd app
# Edit pages/index.tsx or lib/config.ts
# Update ZK_MINT_CONTRACT_ADDRESS with your deployed address
```
Create environment file:
```bash
# app/.env.local
RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
ORACLE_SECRET_KEY=<your_oracle_secret_from_setup>
```
</Step>
<Step title="Run the Application">
```bash
cd app
pnpm dev
# Visit http://localhost:3000
```
</Step>
<Step title="Test the System">
1. **Connect Wallet**: Connect to Arbitrum Sepolia testnet
2. **Generate Proof**: Click "Generate ZK Proof" - this proves you have sufficient balance
3. **Mint Tokens**: Use the proof to mint ERC721 tokens
</Step>
</Steps>
## Customizing the ZK Logic
The above described logic for ZK verification of user balance is provided as a template. You can modify the logic and write your own custom ZK verification code and circuits as described below.
### Understanding the Circuit
The core circuit (`circuits/token_ownership.circom`) has these components:
```txt
template TokenOwnership(oracle_secret) {
// Private inputs (hidden from public)
signal input actual_balance; // Real balance from oracle
signal input salt; // Randomness for uniqueness
// Public inputs (visible on-chain)
signal input min_required_balance; // Minimum threshold
signal input token_contract_hash; // Which token to check
signal input user_address_hash; // User's address hash
signal input timestamp; // When oracle signed data
signal input oracle_commitment; // Oracle's commitment
// Output
signal output nullifier; // Prevents replay attacks
}
```
### Customization Examples
#### 1. Change Balance Threshold Logic
Replace the balance check with custom logic:
```txt
// Original: Simple greater-than check
component gte = GreaterEqThan(64);
gte.in[0] <== actual_balance;
gte.in[1] <== min_required_balance;
gte.out === 1;
// Custom: Logarithmic scaling
component log_check = LogarithmicCheck();
log_check.balance <== actual_balance;
log_check.threshold <== min_required_balance;
log_check.out === 1;
```
#### 2. Add Multiple Token Support
Extend the circuit to verify multiple token balances:
```txt
template MultiTokenOwnership(oracle_secret, num_tokens) {
signal input actual_balances[num_tokens];
signal input min_required_balances[num_tokens];
signal input token_addresses[num_tokens];
// Verify each token separately
component checks[num_tokens];
for (var i = 0; i < num_tokens; i++) {
checks[i] = GreaterEqThan(64);
checks[i].in[0] <== actual_balances[i];
checks[i].in[1] <== min_required_balances[i];
checks[i].out === 1;
}
}
```
#### 3. Add Time-Based Constraints
Add expiration logic to proofs:
```txt
// Add time validation
component time_check = LessThan(64);
time_check.in[0] <== timestamp;
time_check.in[1] <== max_timestamp; // New public input
time_check.out === 1;
```
### Rebuilding After Changes
After modifying the circuit:
```bash
cd circuits
pnpm run build
# Re-inject verification keys
cd ..
node scripts/inject-vk.js
# Recompile contract
cd contracts
cargo build
```
### Resources
- [thirdweb Arbitrum Documentation](https://portal.thirdweb.com/contracts/arbitrum-stylus/stylus-contract)
- [Arbitrum Stylus Documentation](https://docs.arbitrum.io/stylus/concepts/how-it-works)
@@ -56,6 +56,14 @@ export const sidebar: SideBar = {
href: `${slug}/arbitrum-stylus/airdrop-contract`,
name: "Stylus Airdrop Contract",
},
{
href: `${slug}/arbitrum-stylus/zk-mint`,
name: "Stylus Zk Mint",
},
{
href: `${slug}/arbitrum-stylus/minting-modules`,
name: "Stylus Minting Modules",
},
],
name: "Arbitrum Stylus",
},
+6 -6
View File
@@ -41,14 +41,14 @@ export default function Page() {
function Hero() {
return (
<section className="grid gap-4 py-14 lg:grid-cols-[1fr_420px] lg:py-0">
<section className="grid gap-4 pt-14 pb-6 lg:grid-cols-[1fr_420px] lg:py-0">
{/* Left */}
<div className="flex flex-col justify-center">
<div>
<h1 className="mb-3 font-bold text-4xl tracking-tighter lg:text-6xl">
thirdweb Documentation
</h1>
<p className="mb-8 max-w-lg text-lg text-muted-foreground leading-normal text-pretty">
<p className="mb-6 max-w-lg text-base lg:text-lg text-muted-foreground leading-normal text-pretty">
Platform for building the next generation of internet products
</p>
<div className="flex gap-3">
@@ -178,10 +178,10 @@ function LearningResourcesSection() {
title="Wallets"
/>
<ArticleCardIndex
description="Create onchain payments and sell products"
href="/payments"
icon={PaymentsIcon}
title="Payments"
description="Instant payments for APIs, websites, and agents"
href="/x402"
icon={ZapIcon}
title="x402"
/>
<ArticleCardIndex
description="Bridge, swap, and onramp tokens on any chain"
-27
View File
@@ -61,33 +61,6 @@ export const sidebar: SideBar = {
name: "Guides",
},
{ separator: true },
{
isCollapsible: false,
links: [
{
href: `${paymentsSlug}/x402`,
name: "Get Started",
},
{
href: `${paymentsSlug}/x402/client`,
name: "Client Side",
},
{
href: `${paymentsSlug}/x402/server`,
name: "Server Side",
},
{
href: `${paymentsSlug}/x402/agents`,
name: "Agents",
},
{
href: `${paymentsSlug}/x402/facilitator`,
name: "Facilitator",
},
],
name: "x402",
},
{ separator: true },
{
isCollapsible: false,
links: [
+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
@@ -75,7 +75,7 @@ The client library wraps the native `fetch` API and handles:
</TabsContent>
<TabsContent value="http">
## Prepare Payment
## Fetch with Payment
Fetch any x402-compatible API and automatically handle payment flows when APIs return a `402 Payment Required` response using the authenticated wallet.
@@ -27,7 +27,9 @@ You can view all transactions processed by your facilitator in your project dash
## Chain and token support
Our facilitator supports payments on any EVM chain, as long as the payment token supports either:
The thirdweb x402 client/server stack supports payments on [170+ EVM chains](https://thirdweb.com/chainlist?service=account-abstraction).
The payment token must support either:
- ERC-2612 permit (most ERC20 tokens)
- ERC-3009 sign with authorization (USDC on all chains)
+21
View File
@@ -0,0 +1,21 @@
import { createMetadata } from "@doc";
import { DocLayout } from "@/components/Layouts/DocLayout";
import { sidebar } from "./sidebar";
export default async function Layout(props: { children: React.ReactNode }) {
return (
<DocLayout editPageButton={true} sideBar={sidebar}>
{props.children}
</DocLayout>
);
}
export const metadata = createMetadata({
description:
"Instant payments for your APIs, websites, and autonomous agents. x402 is an open-source protocol that turns the HTTP 402 status code into a fully-featured, on-chain payment layer.",
image: {
icon: "payments",
title: "x402 Payments Docs",
},
title: "x402 Payments Documentation",
});
@@ -12,9 +12,8 @@ export const metadata = createMetadata({
# x402 Payments
x402 is an open-source protocol that turns the dormant HTTP 402 Payment Required status code into a fully-featured, on-chain payment layer for APIs, websites, and autonomous agents.
[x402](https://x402.org) is an open-source protocol that turns the dormant HTTP 402 Payment Required status code into a fully-featured, on-chain payment layer for APIs, websites, and autonomous agents.
Learn more about the protocol at [x402.org](https://x402.org).
<ArticleIconCard
title="x402 Playground"
@@ -23,6 +22,15 @@ Learn more about the protocol at [x402.org](https://x402.org).
href="https://playground.thirdweb.com/payments/x402"
/>
## Chain and token support
The thirdweb x402 client/server stack supports payments on [170+ EVM chains](https://thirdweb.com/chainlist?service=account-abstraction).
The payment token must support either:
- ERC-2612 permit (most ERC20 tokens)
- ERC-3009 sign with authorization (USDC on all chains)
## Client Side
`wrapFetchWithPayment` wraps the native fetch API to automatically handle `402 Payment Required` responses from any x402-compatible API:
@@ -42,7 +50,7 @@ const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
const response = await fetchWithPay('https://api.example.com/paid-endpoint');
```
You can also use the thirdweb API to fetch any x402 compatible endpoint and pay for it with the authenticated wallet. See the [client side docs](/payments/x402/client) for more details.
You can also use the thirdweb API to fetch any x402 compatible endpoint and pay for it with the authenticated wallet. See the [client side docs](/x402/client) for more details.
## Server Side
@@ -88,7 +96,7 @@ export async function GET(request: Request) {
}
```
You can also create middlewares to handle payment for multiple endpoints, see the [server side docs](/payments/x402/server) for more details. The [facilitator](/payments/x402/facilitator) handles settling the payment onchain using your own server wallet.
You can also create middlewares to handle payment for multiple endpoints, see the [server side docs](/x402/server) for more details. The [facilitator](/x402/facilitator) handles settling the payment onchain using your own server wallet.
## Going Further
@@ -97,18 +105,18 @@ You can also create middlewares to handle payment for multiple endpoints, see th
title="Client Side"
description="Learn how to interact with x402-compatible APIs from your client applications"
icon={TypeScriptIcon}
href="/payments/x402/client"
href="/x402/client"
/>
<ArticleIconCard
title="Server Side"
description="Accept x402 payments in your APIs with middleware or dedicated endpoints"
icon={EngineIcon}
href="/payments/x402/server"
href="/x402/server"
/>
<ArticleIconCard
title="Facilitator API"
description="Configure the facilitator service to verify and settle payments"
icon={EngineIcon}
href="/payments/x402/facilitator"
href="/x402/facilitator"
/>
</div>
@@ -421,23 +421,31 @@ Protect multiple endpoints with a shared middleware:
</TabsContent>
</Tabs>
## Price Configuration
## Price and Token Configuration
You can specify prices in multiple ways:
### USD String
This will default to using USDC on the specified network.
```typescript
price: "$0.10" // 10 cents in USD
network: polygon, // or any other EVM chain
price: "$0.10" // 10 cents in USDC
```
### ERC20 Token
You can use any ERC20 token that supports the ERC-2612 permit or ERC-3009 sign with authorization.
Simply pass the amount in base units and the token address.
```typescript
network: arbitrum,
price: {
amount: "100000", // Amount in base units (100000 = 0.1 USDC with 6 decimals)
amount: "1000000000000000", // Amount in base units (0.001 tokens with 18 decimals)
asset: {
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Token address
address: "0xf01E52B0BAC3E147f6CAf956a64586865A0aA928", // Token address
}
}
```

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 376 KiB

+77
View File
@@ -0,0 +1,77 @@
import { ExternalLinkIcon, ZapIcon } from "lucide-react";
import type { SideBar } from "@/components/Layouts/DocLayout";
import { EngineIcon, ReactIcon, TypeScriptIcon, UnityIcon } from "@/icons";
import { UnrealEngineIcon } from "../../icons/sdks/UnrealEngineIcon";
const x402Slug = "/x402";
export const sidebar: SideBar = {
links: [
{
href: x402Slug,
name: "Get Started",
icon: <ZapIcon />,
},
{
href: "https://playground.thirdweb.com/payments/x402",
icon: <ExternalLinkIcon />,
name: "Playground",
},
{ separator: true },
{
isCollapsible: false,
links: [
{
href: `${x402Slug}/client`,
name: "Client Side",
},
{
href: `${x402Slug}/server`,
name: "Server Side",
},
{
href: `${x402Slug}/agents`,
name: "Agents",
},
{
href: `${x402Slug}/facilitator`,
name: "Facilitator",
},
],
name: "Guides",
},
{ separator: true },
{
isCollapsible: false,
links: [
{
href: `https://payments.thirdweb.com/reference`,
icon: <EngineIcon />,
name: "REST API",
},
{
href: "/references/typescript/v5",
icon: <TypeScriptIcon />,
name: "TypeScript",
},
{
href: "/references/typescript/v5",
icon: <ReactIcon />,
name: "React",
},
{
href: "/unity",
icon: <UnityIcon />,
name: "Unity",
},
{
href: "/unreal-engine",
icon: <UnrealEngineIcon />,
name: "Unreal Engine",
},
],
name: "API References",
},
],
name: "x402",
};
@@ -15,6 +15,7 @@ const authOptions: InAppWalletAuth[] = [
"facebook",
"x",
"tiktok",
"epic",
"discord",
"telegram",
"twitch",
@@ -29,6 +29,9 @@ type AuthMethod =
| "telegram"
| "farcaster"
| "github"
| "line"
| "tiktok"
| "epic"
| "passkey"
| "wallet"
| "guest"
@@ -55,6 +58,9 @@ const authMethods: { id: AuthMethod; label: string; description: string }[] = [
{ id: "telegram", label: "Telegram", description: "Telegram OAuth" },
{ id: "farcaster", label: "Farcaster", description: "Farcaster OAuth" },
{ id: "github", label: "GitHub", description: "GitHub OAuth" },
{ id: "line", label: "Line", description: "Line OAuth" },
{ id: "tiktok", label: "TikTok", description: "TikTok OAuth" },
{ id: "epic", label: "Epic Games", description: "Epic Games OAuth" },
{ id: "passkey", label: "Passkey", description: "WebAuthn passkey" },
{
id: "wallet",
@@ -244,6 +250,9 @@ const account = await wallet.connect({
case "telegram":
case "farcaster":
case "github":
case "line":
case "tiktok":
case "epic":
return (
baseSetup +
`// ${authMethod.charAt(0).toUpperCase() + authMethod.slice(1)} OAuth
@@ -561,6 +570,9 @@ function PrebuiltUIExample() {
case "telegram":
case "farcaster":
case "github":
case "line":
case "tiktok":
case "epic":
return (
baseSetup +
` await wallet.connect({\n client,\n strategy: "${authMethod}",\n });\n` +
@@ -639,7 +651,10 @@ var address = await wallet.LoginWithOtp("123456");`
case "x":
case "telegram":
case "farcaster":
case "github": {
case "github":
case "line":
case "tiktok":
case "epic": {
const providerMap: Record<string, string> = {
google: "Google",
apple: "Apple",
@@ -649,6 +664,9 @@ var address = await wallet.LoginWithOtp("123456");`
telegram: "Telegram",
farcaster: "Farcaster",
github: "Github",
line: "Line",
tiktok: "TikTok",
epic: "Epic",
};
return (
baseSetup +
@@ -746,7 +764,10 @@ var wallet = await ThirdwebManager.Instance.ConnectWallet(options);`
case "x":
case "telegram":
case "farcaster":
case "github": {
case "github":
case "line":
case "tiktok":
case "epic": {
const providerMap: Record<string, string> = {
google: "Google",
apple: "Apple",
@@ -756,6 +777,9 @@ var wallet = await ThirdwebManager.Instance.ConnectWallet(options);`
telegram: "Telegram",
farcaster: "Farcaster",
github: "Github",
line: "Line",
tiktok: "TikTok",
epic: "Epic",
};
return (
baseSetup +
+72
View File
@@ -1,5 +1,77 @@
# wagmi-inapp
## 0.0.30
### Patch Changes
- Updated dependencies [[`f964b8a`](https://github.com/thirdweb-dev/js/commit/f964b8aaff22d4078ff4c4c5e09c6ebb5fcb182c), [`424e6b8`](https://github.com/thirdweb-dev/js/commit/424e6b8f9b071d3629ff0291ae6875fa4e4c882e), [`763d3de`](https://github.com/thirdweb-dev/js/commit/763d3de5a8db19a8022b2eda420f13a6787de100)]:
- thirdweb@5.111.10
- @thirdweb-dev/wagmi-adapter@0.2.185
## 0.0.29
### Patch Changes
- Updated dependencies [[`673e51b`](https://github.com/thirdweb-dev/js/commit/673e51b11cf2d06f00ab92bd4cbb45c51d95ec1f)]:
- thirdweb@5.111.9
- @thirdweb-dev/wagmi-adapter@0.2.184
## 0.0.28
### Patch Changes
- Updated dependencies [[`1f440d0`](https://github.com/thirdweb-dev/js/commit/1f440d0c97adf14d26d55666257b57541b2ef01a)]:
- thirdweb@5.111.8
- @thirdweb-dev/wagmi-adapter@0.2.183
## 0.0.27
### Patch Changes
- Updated dependencies [[`7f08161`](https://github.com/thirdweb-dev/js/commit/7f081618e16e7d5754a09780421e35b244186ce8), [`7f08161`](https://github.com/thirdweb-dev/js/commit/7f081618e16e7d5754a09780421e35b244186ce8)]:
- thirdweb@5.111.7
- @thirdweb-dev/wagmi-adapter@0.2.182
## 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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wagmi-inapp",
"private": true,
"version": "0.0.21",
"version": "0.0.30",
"type": "module",
"scripts": {
"dev": "vite",
+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;
+63
View File
@@ -1,5 +1,68 @@
# @thirdweb-dev/nebula
## 0.2.88
### Patch Changes
- Updated dependencies [[`f964b8a`](https://github.com/thirdweb-dev/js/commit/f964b8aaff22d4078ff4c4c5e09c6ebb5fcb182c), [`424e6b8`](https://github.com/thirdweb-dev/js/commit/424e6b8f9b071d3629ff0291ae6875fa4e4c882e), [`763d3de`](https://github.com/thirdweb-dev/js/commit/763d3de5a8db19a8022b2eda420f13a6787de100)]:
- thirdweb@5.111.10
## 0.2.87
### Patch Changes
- Updated dependencies [[`673e51b`](https://github.com/thirdweb-dev/js/commit/673e51b11cf2d06f00ab92bd4cbb45c51d95ec1f)]:
- thirdweb@5.111.9
## 0.2.86
### Patch Changes
- Updated dependencies [[`1f440d0`](https://github.com/thirdweb-dev/js/commit/1f440d0c97adf14d26d55666257b57541b2ef01a)]:
- thirdweb@5.111.8
## 0.2.85
### Patch Changes
- Updated dependencies [[`7f08161`](https://github.com/thirdweb-dev/js/commit/7f081618e16e7d5754a09780421e35b244186ce8), [`7f08161`](https://github.com/thirdweb-dev/js/commit/7f081618e16e7d5754a09780421e35b244186ce8)]:
- thirdweb@5.111.7
## 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
+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.79"
"version": "0.2.88"
}
@@ -1,5 +1,11 @@
# @thirdweb-dev/react-native-adapter
## 1.5.5
### Patch Changes
- [#8390](https://github.com/thirdweb-dev/js/pull/8390) [`f964b8a`](https://github.com/thirdweb-dev/js/commit/f964b8aaff22d4078ff4c4c5e09c6ebb5fcb182c) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Loosen peer dependency requirements for RN
## 1.5.4
### Patch Changes
+5 -5
View File
@@ -34,12 +34,12 @@
"peerDependencies": {
"@biomejs/biome": "2.0.6",
"@coinbase/wallet-mobile-sdk": "^1",
"@react-native-async-storage/async-storage": "^1 || ^2",
"@react-native-async-storage/async-storage": "*",
"@react-native-community/netinfo": "^11",
"@walletconnect/react-native-compat": "^2",
"expo-application": "^5 || ^6",
"expo-linking": "^6 || ^7",
"expo-web-browser": "^13 || ^14",
"expo-application": "*",
"expo-linking": "*",
"expo-web-browser": "*",
"react-native": ">=0.70",
"react-native-aes-gcm-crypto": "^0.2",
"react-native-get-random-values": "^1",
@@ -69,5 +69,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "1.5.4"
"version": "1.5.5"
}
+65
View File
@@ -1,5 +1,70 @@
# thirdweb
## 5.111.10
### Patch Changes
- [#8390](https://github.com/thirdweb-dev/js/pull/8390) [`f964b8a`](https://github.com/thirdweb-dev/js/commit/f964b8aaff22d4078ff4c4c5e09c6ebb5fcb182c) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Loosen peer dependency requirements for RN
- [#8383](https://github.com/thirdweb-dev/js/pull/8383) [`424e6b8`](https://github.com/thirdweb-dev/js/commit/424e6b8f9b071d3629ff0291ae6875fa4e4c882e) Thanks [@kumaryash90](https://github.com/kumaryash90)! - deploy and install stylus modules
- [#8388](https://github.com/thirdweb-dev/js/pull/8388) [`763d3de`](https://github.com/thirdweb-dev/js/commit/763d3de5a8db19a8022b2eda420f13a6787de100) Thanks [@MananTank](https://github.com/MananTank)! - - Various UI/UX Improvements in Bridge components
## 5.111.9
### Patch Changes
- [#8384](https://github.com/thirdweb-dev/js/pull/8384) [`673e51b`](https://github.com/thirdweb-dev/js/commit/673e51b11cf2d06f00ab92bd4cbb45c51d95ec1f) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Replace celo alfajores with celoSepoliaTestnet
## 5.111.8
### Patch Changes
- [#8343](https://github.com/thirdweb-dev/js/pull/8343) [`1f440d0`](https://github.com/thirdweb-dev/js/commit/1f440d0c97adf14d26d55666257b57541b2ef01a) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - Added Epic Games Authentication
## 5.111.7
### Patch Changes
- [#8373](https://github.com/thirdweb-dev/js/pull/8373) [`7f08161`](https://github.com/thirdweb-dev/js/commit/7f081618e16e7d5754a09780421e35b244186ce8) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Respect from address when simulating with Engine.serverWallet
- [#8373](https://github.com/thirdweb-dev/js/pull/8373) [`7f08161`](https://github.com/thirdweb-dev/js/commit/7f081618e16e7d5754a09780421e35b244186ce8) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose useInvalidateBalances() react hook
## 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
+6 -6
View File
@@ -73,8 +73,8 @@
"dotenv-mono": "^1.3.14",
"ethers5": "npm:ethers@5",
"ethers6": "npm:ethers@6",
"expo-linking": "7.0.5",
"expo-web-browser": "14.0.2",
"expo-linking": "8.0.8",
"expo-web-browser": "15.0.9",
"happy-dom": "17.4.4",
"knip": "5.60.2",
"msw": "2.7.5",
@@ -256,10 +256,10 @@
"@aws-sdk/credential-providers": "^3",
"@coinbase/wallet-mobile-sdk": "^1",
"@mobile-wallet-protocol/client": "^1",
"@react-native-async-storage/async-storage": "^1 || ^2",
"@react-native-async-storage/async-storage": "*",
"ethers": "^5 || ^6",
"expo-linking": "^6 || ^7",
"expo-web-browser": "^13 || ^14",
"expo-linking": "*",
"expo-web-browser": "*",
"react": "^18 || ^19",
"react-native": "*",
"react-native-aes-gcm-crypto": "^0.2",
@@ -430,5 +430,5 @@
}
},
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "5.111.1"
"version": "5.111.10"
}
@@ -1,17 +0,0 @@
import { defineChain } from "../utils.js";
/**
* @chain
*/
export const celoAlfajoresTestnet = /* @__PURE__ */ defineChain({
blockExplorers: [
{
name: "Alfajoresscan",
url: "https://alfajores.celoscan.io",
},
],
id: 44787,
name: "Celo Alfajores Testnet",
nativeCurrency: { decimals: 18, name: "CELO", symbol: "CELO" },
testnet: true,
});
@@ -0,0 +1,17 @@
import { defineChain } from "../utils.js";
/**
* @chain
*/
export const celoSepoliaTestnet = /* @__PURE__ */ defineChain({
blockExplorers: [
{
name: "Celo Sepolia Explorer",
url: "https://celo-sepolia.blockscout.com/",
},
],
id: 11142220,
name: "Celo Sepolia Testnet",
nativeCurrency: { decimals: 18, name: "CELO", symbol: "CELO-S" },
testnet: true,
});
@@ -60,6 +60,12 @@ export async function createStylusProject() {
{ title: "Airdrop ERC1155", value: "airdrop1155" },
{ title: "ZK ERC721", value: "zk-erc721" },
{ title: "ZK ERC20", value: "zk-erc20" },
{ title: "Mint module - ERC20", value: "mintable20" },
{ title: "Transfer module - ERC20", value: "transferable20" },
{ title: "Mint module - ERC721", value: "mintable721" },
{ title: "Transfer module - ERC721", value: "transferable721" },
{ title: "Mint module - ERC1155", value: "mintable1155" },
{ title: "Transfer module - ERC1155", value: "transferable1155" },
],
message: "Select a template:",
name: "projectType",
@@ -133,6 +139,49 @@ export async function createStylusProject() {
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
stdio: "inherit",
});
} else if (projectType === "mintable20") {
const repoUrl = "[email protected]:thirdweb-example/stylus-mintable-erc20.git";
spinner.start(`Creating new ERC20 Mintable module: ${projectName}...`);
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
stdio: "inherit",
});
} else if (projectType === "transferable20") {
const repoUrl =
"[email protected]:thirdweb-example/stylus-transferable-erc20.git";
spinner.start(`Creating new ERC20 Transferable module: ${projectName}...`);
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
stdio: "inherit",
});
} else if (projectType === "mintable721") {
const repoUrl =
"[email protected]:thirdweb-example/stylus-mintable-erc721.git";
spinner.start(`Creating new ERC721 Mintable module: ${projectName}...`);
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
stdio: "inherit",
});
} else if (projectType === "transferable721") {
const repoUrl =
"[email protected]:thirdweb-example/stylus-transferable-erc721.git";
spinner.start(`Creating new ERC721 Transferable module: ${projectName}...`);
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
stdio: "inherit",
});
} else if (projectType === "mintable1155") {
const repoUrl =
"[email protected]:thirdweb-example/stylus-mintable-erc1155.git";
spinner.start(`Creating new ERC1155 Mintable module: ${projectName}...`);
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
stdio: "inherit",
});
} else if (projectType === "transferable1155") {
const repoUrl =
"[email protected]:thirdweb-example/stylus-transferable-erc1155.git";
spinner.start(
`Creating new ERC1155 Transferable module: ${projectName}...`,
);
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
stdio: "inherit",
});
}
if (!newProject || newProject.status !== 0) {
@@ -1,6 +1,6 @@
export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
232: {
MarketplaceV3: "0xB7387cF425eC8Da1b04b8E34De86245cb32bCA6D",
MarketplaceV3: "0x4e0C3577335961Ff800FFDA24981EB2F38D94483",
},
300: {
MarketplaceV3: "0x58e0F289C7dD2025eBd0696d913ECC0fdc1CC8bc",
@@ -18,7 +18,7 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
MarketplaceV3: "0x2dA4Dd326A6482679547071be21f74685d730504",
},
37111: {
MarketplaceV3: "0xF8fd1016420c2B7832714241d36Efe55D41df126",
MarketplaceV3: "0x12A305cc7168fa3b7B172fE53c57b9a22716F667",
},
50104: {
MarketplaceV3: "0x292c324920d4120F928dA6f04548442fa10B8B0f",
@@ -332,6 +332,7 @@ export function serverWallet(options: ServerWalletOptions): ServerWallet {
if (args.simulate) {
serializedTransaction = await toSerializableTransaction({
transaction: args.transaction,
from: address,
});
} else {
const [to, data, value] = await Promise.all([
+1 -1
View File
@@ -24,7 +24,7 @@ export { blastSepolia } from "../chains/chain-definitions/blast-sepolia.js";
export { bsc } from "../chains/chain-definitions/bsc.js";
export { bscTestnet } from "../chains/chain-definitions/bsc-testnet.js";
export { celo } from "../chains/chain-definitions/celo.js";
export { celoAlfajoresTestnet } from "../chains/chain-definitions/celo-alfajores-testnet.js";
export { celoSepoliaTestnet } from "../chains/chain-definitions/celo-sepolia-testnet.js";
export { coreMainnet } from "../chains/chain-definitions/core-mainnet.js";
export { coreTestnet } from "../chains/chain-definitions/core-testnet.js";
export { cronos } from "../chains/chain-definitions/cronos.js";
@@ -22,6 +22,7 @@ export { useContractEvents } from "../react/core/hooks/contract/useContractEvent
// contract
export { useReadContract } from "../react/core/hooks/contract/useReadContract.js";
export { useWaitForReceipt } from "../react/core/hooks/contract/useWaitForReceipt.js";
export { useInvalidateBalances } from "../react/core/hooks/others/useInvalidateBalances.js";
export { useInvalidateContractQuery } from "../react/core/hooks/others/useInvalidateQueries.js";
export { useWalletBalance } from "../react/core/hooks/others/useWalletBalance.js";
export {
+1
View File
@@ -29,6 +29,7 @@ export { useReadContract } from "../react/core/hooks/contract/useReadContract.js
export { useWaitForReceipt } from "../react/core/hooks/contract/useWaitForReceipt.js";
// chain hooks
export { useChainMetadata } from "../react/core/hooks/others/useChainQuery.js";
export { useInvalidateBalances } from "../react/core/hooks/others/useInvalidateBalances.js";
export { useInvalidateContractQuery } from "../react/core/hooks/others/useInvalidateQueries.js";
export { useWalletBalance } from "../react/core/hooks/others/useWalletBalance.js";
export {
@@ -1,6 +1,7 @@
import type { ThirdwebContract } from "../../../contract/contract.js";
import { getOrDeployInfraForPublishedContract } from "../../../contract/deployment/utils/bootstrap.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import { deployPublishedContract } from "../../prebuilts/deploy-published.js";
import { installModule } from "../__generated__/IModularCore/write/installModule.js";
/**
@@ -49,17 +50,35 @@ export function installPublishedModule(options: InstallPublishedModuleOptions) {
return installModule({
asyncParams: async () => {
const deployedModule = await getOrDeployInfraForPublishedContract({
account,
chain: contract.chain,
client: contract.client,
constructorParams,
contractId: moduleName,
publisher,
});
let implementationAddress: string;
if (moduleName.toLowerCase().includes("stylus")) {
// TODO: switch to deterministic / create2 when available
implementationAddress = await deployPublishedContract({
account,
chain: contract.chain,
client: contract.client,
contractParams: constructorParams,
contractId: moduleName,
publisher,
});
} else {
const deployedModule = await getOrDeployInfraForPublishedContract({
account,
chain: contract.chain,
client: contract.client,
constructorParams,
contractId: moduleName,
publisher,
});
implementationAddress = deployedModule.implementationContract
.address as string;
}
return {
data: moduleData || "0x",
moduleContract: deployedModule.implementationContract.address as string,
moduleContract: implementationAddress,
};
},
contract,
@@ -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 &&
!(await isZkSyncChain(chain))
) {
for (const e of deployMetadata.defaultExtensions) {
await getOrDeployInfraForPublishedContract({
@@ -0,0 +1,18 @@
import { useQueryClient } from "@tanstack/react-query";
import { invalidateWalletBalance } from "../../providers/invalidateWalletBalance.js";
/**
* Invalidate the balances for a given chainId. If no chainId is provided, invalidate all balances.
* @example
* ```ts
* const invalidateBalances = useInvalidateBalances();
* invalidateBalances();
* ```
*/
export function useInvalidateBalances() {
const queryClient = useQueryClient();
return ({ chainId }: { chainId?: number }) => {
invalidateWalletBalance(queryClient, chainId);
};
}
@@ -55,6 +55,10 @@ describe("walletIcon", () => {
expect(getSocialIcon("tiktok")).toBe(socialIcons.tiktok);
});
it("should return epic icon", () => {
expect(getSocialIcon("epic")).toBe(socialIcons.epic);
});
it("should return farcaster icon", () => {
expect(getSocialIcon("farcaster")).toBe(socialIcons.farcaster);
});
File diff suppressed because one or more lines are too long
@@ -11,6 +11,7 @@ import {
APPLE_ICON,
DISCORD_ICON,
EMAIL_ICON,
EPIC_ICON,
FACEBOOK_ICON,
FARCASTER_ICON,
GITHUB_ICON,
@@ -106,6 +107,8 @@ export function getAuthProviderImage(authProvider: string | null): string {
return X_ICON;
case "tiktok":
return TIKTOK_ICON;
case "epic":
return EPIC_ICON;
case "farcaster":
return FARCASTER_ICON;
case "telegram":
@@ -32,6 +32,7 @@ import {
APPLE_ICON,
COINBASE_ICON,
DISCORD_ICON,
EPIC_ICON,
FACEBOOK_ICON,
FARCASTER_ICON,
GITHUB_ICON,
@@ -69,6 +70,7 @@ const socialIcons = {
twitch: TWITCH_ICON,
x: X_ICON,
tiktok: TIKTOK_ICON,
epic: EPIC_ICON,
};
type InAppWalletFormUIProps = {
File diff suppressed because one or more lines are too long
@@ -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"}
@@ -298,6 +298,7 @@ export function FundWallet(props: FundWalletProps) {
}}
theme={theme}
{...props.connectOptions}
autoConnect={false}
/>
) : (
<Button
@@ -219,7 +219,7 @@ export function TransactionPayment({
}}
>
{/* USD Value */}
<Text color="primaryText" size="xl" weight={600}>
<Text color="primaryText" size="xl" weight={500}>
{costToDisplay}
</Text>
@@ -82,7 +82,7 @@ export function TokenAndChain({
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
weight={600}
weight={500}
trackingTight
>
{token.name}
@@ -68,18 +68,9 @@ export function TokenBalanceRow({
overflow: "hidden",
}}
>
<FiatValue
currency={currency}
chain={chain}
client={client}
<Text
color="primaryText"
size="sm"
token={token}
tokenAmount={amount}
/>
<Text
color="secondaryText"
size="xs"
style={{
overflow: "hidden",
textOverflow: "ellipsis",
@@ -91,6 +82,16 @@ export function TokenBalanceRow({
minimumFractionDigits: 0,
})} ${token.symbol}`}
</Text>
<FiatValue
currency={currency}
chain={chain}
client={client}
color="secondaryText"
size="sm"
token={token}
tokenAmount={amount}
/>
</Container>
</Container>
</StyledButton>
@@ -100,13 +100,23 @@ export function SelectedTokenButton(props: {
}}
>
<Img
src={props.chain?.icon}
src={props.chain?.icon || ""}
client={props.client}
width={iconSize.sm}
height={iconSize.sm}
style={{
borderRadius: radius.full,
}}
fallback={
<Container
style={{
background: `linear-gradient(45deg, white, ${theme.colors.accentText})`,
borderRadius: radius.full,
width: `${iconSize.sm}px`,
height: `${iconSize.sm}px`,
}}
/>
}
/>
</Container>
)}
@@ -63,7 +63,7 @@ export function PaymentOverview(props: {
<WalletRow
address={sender}
client={props.client}
iconSize="md"
iconSize="lg"
textSize="sm"
/>
</Container>
@@ -91,13 +91,14 @@ export function PaymentOverview(props: {
py="sm"
style={{ justifyContent: "space-between" }}
>
{/* left */}
<Container center="y" flex="row" gap="sm">
{getFiatCurrencyIcon({
currency: props.paymentMethod.currency,
size: "lg",
})}
<Container center="y" flex="column" gap="3xs">
<Text color="primaryText" size="sm" style={{ fontWeight: 600 }}>
<Text color="primaryText" size="sm" weight={500}>
{props.paymentMethod.currency}
</Text>
<Text color="secondaryText" size="xs">
@@ -106,7 +107,8 @@ export function PaymentOverview(props: {
</Text>
</Container>
</Container>
<Text color="primaryText" size="sm" style={{ fontWeight: 600 }}>
{/* right */}
<Text color="primaryText" size="sm">
{props.fromAmount}
</Text>
</Container>
@@ -136,11 +138,12 @@ export function PaymentOverview(props: {
<WalletRow
address={props.receiver}
client={props.client}
iconSize="md"
iconSize="lg"
textSize="sm"
/>
</Container>
)}
{props.modeInfo.mode === "direct_payment" && (
<Container
center="y"
@@ -150,7 +153,7 @@ export function PaymentOverview(props: {
style={{ justifyContent: "space-between" }}
>
<Container center="y" flex="column" gap="3xs" style={{ flex: 1 }}>
<Text color="primaryText" size="sm" style={{ fontWeight: 600 }}>
<Text color="primaryText" size="sm" weight={500}>
{props.metadata.title || "Payment"}
</Text>
{props.metadata.description && (
@@ -173,7 +176,6 @@ export function PaymentOverview(props: {
size="sm"
token={props.toToken}
tokenAmount={props.modeInfo.paymentInfo.amount}
weight={600}
/>
<Text color="secondaryText" size="xs">
{props.modeInfo.paymentInfo.amount} {props.toToken.symbol}
@@ -285,7 +287,7 @@ const TransactionOverViewCompact = (props: {
style={{ justifyContent: "space-between" }}
>
<Container center="y" flex="column" gap="3xs" style={{ flex: 1 }}>
<Text color="primaryText" size="sm" style={{ fontWeight: 600 }}>
<Text color="primaryText" size="sm" weight={500}>
{props.metadata.title || "Transaction"}
</Text>
{props.metadata.description && (
@@ -89,7 +89,7 @@ function PaymentMethodTokenRow({
<Text
color="primaryText"
size="sm"
style={{ fontWeight: 600, textWrap: "nowrap" }}
style={{ fontWeight: 500, textWrap: "nowrap" }}
>
{formatCurrencyAmount(
currency || "USD",
@@ -1,7 +1,5 @@
"use client";
import { CopyIcon } from "@radix-ui/react-icons";
import { useQuery } from "@tanstack/react-query";
import { useCallback } from "react";
import type { Token } from "../../../../../bridge/types/Token.js";
import type { ChainMetadata } from "../../../../../chains/types.js";
import {
@@ -10,22 +8,18 @@ import {
getChainMetadata,
} from "../../../../../chains/utils.js";
import { shortenHex } from "../../../../../utils/address.js";
import { formatExplorerTxUrl } from "../../../../../utils/url.js";
import type { WindowAdapter } from "../../../../core/adapters/WindowAdapter.js";
import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
import {
iconSize,
radius,
spacing,
} from "../../../../core/design-system/index.js";
import { radius, spacing } from "../../../../core/design-system/index.js";
import type { BridgePrepareResult } from "../../../../core/hooks/useBridgePrepare.js";
import type { CompletedStatusResult } from "../../../../core/hooks/useStepExecutor.js";
import { formatTokenAmount } from "../../ConnectWallet/screens/formatTokenBalance.js";
import { Container, ModalHeader } from "../../components/basic.js";
import { Container, Line, ModalHeader } from "../../components/basic.js";
import { shorterChainName } from "../../components/ChainName.js";
import { CopyIcon } from "../../components/CopyIcon.js";
import { Skeleton } from "../../components/Skeleton.js";
import { Spacer } from "../../components/Spacer.js";
import { Text } from "../../components/text.js";
import { Link, Text } from "../../components/text.js";
interface TransactionInfo {
type: "paymentId" | "transactionHash";
@@ -128,7 +122,6 @@ interface CompletedStepDetailCardProps {
status: CompletedStatusResult;
preparedQuote: BridgePrepareResult;
windowAdapter: WindowAdapter;
onCopyToClipboard: (text: string) => Promise<void>;
}
/**
@@ -137,29 +130,12 @@ interface CompletedStepDetailCardProps {
function CompletedStepDetailCard({
status,
preparedQuote,
windowAdapter,
onCopyToClipboard,
}: CompletedStepDetailCardProps) {
const theme = useCustomTheme();
const { data: txInfo, isLoading } = useTransactionInfo(status, preparedQuote);
const { data: txInfo, isPending } = useTransactionInfo(status, preparedQuote);
if (isLoading) {
return (
<Container
flex="column"
gap="sm"
style={{
backgroundColor: theme.colors.tertiaryBg,
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.sm,
padding: spacing.md,
}}
>
<Skeleton height="30px" />
<Skeleton height="30px" />
<Skeleton height="30px" />
</Container>
);
if (isPending) {
return <Skeleton height="200px" style={{ borderRadius: radius.lg }} />;
}
if (!txInfo) {
@@ -174,7 +150,7 @@ function CompletedStepDetailCard({
style={{
backgroundColor: theme.colors.tertiaryBg,
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.sm,
borderRadius: radius.lg,
padding: spacing.md,
}}
>
@@ -182,27 +158,37 @@ function CompletedStepDetailCard({
<Container
flex="row"
gap="sm"
py="4xs"
style={{
alignItems: "center",
justifyContent: "space-between",
}}
>
<Text color="primaryText" size="sm">
<Text color="primaryText" size="sm" weight={500}>
{txInfo.label}
</Text>
<Container
style={{
backgroundColor: theme.colors.success,
borderRadius: radius.sm,
backgroundColor: theme.colors.modalBg,
borderRadius: radius.full,
border: `1px solid ${theme.colors.borderColor}`,
padding: `${spacing["3xs"]} ${spacing.xs}`,
}}
>
<Text size="xs" style={{ color: theme.colors.primaryButtonText }}>
<Text
style={{
color: theme.colors.success,
fontSize: 10,
letterSpacing: "0.025em",
}}
>
COMPLETED
</Text>
</Container>
</Container>
<Line />
{/* Amount Paid */}
{txInfo.amountPaid && (
<Container
@@ -219,22 +205,6 @@ function CompletedStepDetailCard({
</Container>
)}
{/* Origin Chain */}
{txInfo.originChain && (
<Container
center="y"
flex="row"
style={{ justifyContent: "space-between" }}
>
<Text color="secondaryText" size="sm">
Origin Chain
</Text>
<Text color="primaryText" size="sm">
{shorterChainName(txInfo.originChain.name)}
</Text>
</Container>
)}
{/* Amount Received */}
{txInfo.amountReceived && (
<Container
@@ -251,14 +221,30 @@ function CompletedStepDetailCard({
</Container>
)}
{/* Chain */}
{/* Origin Chain */}
{txInfo.originChain && (
<Container
center="y"
flex="row"
style={{ justifyContent: "space-between" }}
>
<Text color="secondaryText" size="sm">
Origin Chain
</Text>
<Text color="primaryText" size="sm">
{shorterChainName(txInfo.originChain.name)}
</Text>
</Container>
)}
{/* Destination Chain */}
<Container
center="y"
flex="row"
style={{ justifyContent: "space-between" }}
>
<Text color="secondaryText" size="sm">
Chain
Destination Chain
</Text>
<Text color="primaryText" size="sm">
{shorterChainName(txInfo.chain.name)}
@@ -266,58 +252,59 @@ function CompletedStepDetailCard({
</Container>
{/* Transaction Info */}
<Container
center="y"
flex="row"
style={{ justifyContent: "space-between" }}
>
<Text color="secondaryText" size="sm">
{txInfo.type === "paymentId" ? "Payment ID" : "Transaction Hash"}
</Text>
<Container flex="row" gap="sm" style={{ alignItems: "center" }}>
<Text
color="accentText"
onClick={
txInfo.type === "paymentId"
? () => onCopyToClipboard(txInfo.id)
: () => {
const explorer = txInfo.chain.explorers?.[0];
if (explorer) {
windowAdapter.open(
formatExplorerTxUrl(explorer.url, txInfo.id),
);
}
}
}
size="sm"
style={{
cursor: "pointer",
fontFamily: "monospace",
}}
>
{shortenHex(txInfo.id)}
{txInfo.type === "paymentId" && (
<Container
center="y"
flex="row"
style={{ justifyContent: "space-between" }}
>
<Text color="secondaryText" size="sm">
Payment ID
</Text>
<Container flex="row" gap="3xs" center="y">
<CopyIcon text={txInfo.id} iconSize={12} tip="Copy Payment ID" />
{txInfo.type === "paymentId" ? (
<button
onClick={() => onCopyToClipboard(txInfo.id)}
style={{
background: "none",
border: "none",
cursor: "pointer",
padding: 0,
}}
type="button"
>
<CopyIcon
color={theme.colors.primaryText}
height={iconSize.sm}
width={iconSize.sm}
/>
</button>
) : null}
<Text color="primaryText" weight={400} size="sm">
{shortenHex(txInfo.id)}
</Text>
</Container>
</Container>
</Container>
)}
{status.transactions.map((tx) => {
const explorerLink = `https://thirdweb.com/${tx.chainId}/tx/${tx.transactionHash}`;
return (
<Container
key={tx.transactionHash}
center="y"
flex="row"
style={{ justifyContent: "space-between" }}
>
<Text color="secondaryText" size="sm">
Transaction Hash
</Text>
<Container flex="row" gap="3xs" center="y">
<CopyIcon
text={tx.transactionHash}
iconSize={12}
tip="Copy Transaction Hash"
/>
<Link
href={explorerLink}
target="_blank"
rel="noreferrer"
color="primaryText"
hoverColor="accentText"
weight={400}
size="sm"
>
{shortenHex(tx.transactionHash)}
</Link>
</Container>
</Container>
);
})}
</Container>
);
}
@@ -350,34 +337,23 @@ export function PaymentReceipt({
onBack,
windowAdapter,
}: PaymentReceitProps) {
// Copy to clipboard
const copyToClipboard = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
// Could add a toast notification here
} catch (error) {
console.warn("Failed to copy to clipboard:", error);
}
}, []);
return (
<Container
flex="column"
fullHeight
p="md"
style={{ maxHeight: "500px", minHeight: "250px", overflowY: "auto" }}
>
<Container flex="column" fullHeight px="md">
<Spacer y="md+" />
<ModalHeader onBack={onBack} title="Payment Receipt" />
<Spacer y="md+" />
<Spacer y="lg" />
<Container flex="column" gap="lg">
<Container
flex="column"
gap="lg"
scrollY
style={{ maxHeight: "500px", minHeight: "400px" }}
>
{/* Status Results */}
<Container flex="column" gap="md">
<Container flex="column" gap="sm">
{completedStatuses.map((status, index) => (
<CompletedStepDetailCard
key={`${status.type}-${index}`}
onCopyToClipboard={copyToClipboard}
preparedQuote={preparedQuote}
status={status}
windowAdapter={windowAdapter}
@@ -385,6 +361,7 @@ export function PaymentReceipt({
))}
</Container>
</Container>
<Spacer y="md+" />
</Container>
);
}
@@ -9,7 +9,7 @@ import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvid
import { iconSize, spacing } from "../../../../core/design-system/index.js";
import type { BridgePrepareResult } from "../../../../core/hooks/useBridgePrepare.js";
import type { CompletedStatusResult } from "../../../../core/hooks/useStepExecutor.js";
import { Container, ModalHeader } from "../../components/basic.js";
import { Container } from "../../components/basic.js";
import { Button } from "../../components/buttons.js";
import { Spacer } from "../../components/Spacer.js";
import { Text } from "../../components/text.js";
@@ -91,9 +91,7 @@ export function SuccessScreen({
return (
<Container flex="column" fullHeight px="md" pb="md" pt="md+">
<ModalHeader title="Payment Complete" />
<Spacer y="xxl" />
<Spacer y="3xl" />
<Container center="x" flex="column" gap="md">
{/* Success Icon with Animation */}
@@ -130,7 +128,7 @@ export function SuccessScreen({
marginBottom: spacing.xxs,
}}
>
Payment Successful!
Payment Successful
</Text>
<Text center color="secondaryText" size="sm">
@@ -143,7 +141,7 @@ export function SuccessScreen({
</div>
</Container>
<Spacer y="xxl" />
<Spacer y="3xl" />
{/* Action Buttons */}
<Container flex="column" gap="sm" style={{ width: "100%" }}>
@@ -1,6 +1,7 @@
import { useMemo, useState } from "react";
import type { Chain as BridgeChain } from "../../../../../bridge/index.js";
import type { ThirdwebClient } from "../../../../../client/client.js";
import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
import {
fontSize,
iconSize,
@@ -170,6 +171,8 @@ function ChainButton(props: {
isSelected: boolean;
isMobile: boolean;
}) {
const theme = useCustomTheme();
const iconSizeValue = props.isMobile ? iconSize.lg : iconSize.md;
return (
<Button
variant={props.isSelected ? "secondary" : "ghost-solid"}
@@ -185,13 +188,25 @@ function ChainButton(props: {
gap="sm"
>
<Img
src={props.chain.icon}
src={props.chain.icon || ""}
client={props.client}
width={props.isMobile ? iconSize.lg : iconSize.md}
height={props.isMobile ? iconSize.lg : iconSize.md}
width={iconSizeValue}
height={iconSizeValue}
style={{
borderRadius: radius.full,
}}
fallback={
<Container color="secondaryText">
<Container
style={{
background: `linear-gradient(45deg, white, ${theme.colors.accentText})`,
borderRadius: radius.full,
width: `${iconSizeValue}px`,
height: `${iconSizeValue}px`,
}}
/>
</Container>
}
/>
{cleanedChainName(props.chain.name)}
</Button>
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import type { Token } from "../../../../../bridge/index.js";
import type { BridgeChain } from "../../../../../bridge/types/Chain.js";
import type { ThirdwebClient } from "../../../../../client/client.js";
@@ -10,8 +10,6 @@ import {
radius,
spacing,
} from "../../../../core/design-system/index.js";
import { CoinsIcon } from "../../ConnectWallet/icons/CoinsIcon.js";
import { WalletDotIcon } from "../../ConnectWallet/icons/WalletDotIcon.js";
import { Container, noScrollBar } from "../../components/basic.js";
import { Button } from "../../components/buttons.js";
import { Img } from "../../components/Img.js";
@@ -52,14 +50,18 @@ function findChain(chains: BridgeChain[], activeChainId: number | undefined) {
return chains.find((chain) => chain.chainId === activeChainId);
}
const ALL_TOKENS_LIMIT = 100;
const OWNED_TOKENS_LIMIT = 50;
/**
* @internal
*/
export function SelectToken(props: SelectTokenUIProps) {
const chainQuery = useBridgeChains(props.client);
const [search, setSearch] = useState("");
const [search, _setSearch] = useState("");
const debouncedSearch = useDebouncedValue(search, 500);
const [limit, setLimit] = useState(1000);
const [allTokensLimit, setAllTokensLimit] = useState(ALL_TOKENS_LIMIT);
const [ownedTokensLimit, setOwnedTokensLimit] = useState(OWNED_TOKENS_LIMIT);
const [_selectedChain, setSelectedChain] = useState<BridgeChain | undefined>(
undefined,
@@ -77,7 +79,7 @@ export function SelectToken(props: SelectTokenUIProps) {
client: props.client,
chainId: selectedChain?.chainId,
search: debouncedSearch,
limit,
limit: allTokensLimit,
offset: 0,
});
@@ -85,7 +87,7 @@ export function SelectToken(props: SelectTokenUIProps) {
const ownedTokensQuery = useTokenBalances({
clientId: props.client.clientId,
chainId: selectedChain?.chainId,
limit,
limit: ownedTokensLimit,
page: 1,
walletAddress: props.activeWalletInfo?.activeAccount.address,
});
@@ -102,43 +104,62 @@ export function SelectToken(props: SelectTokenUIProps) {
});
}, [ownedTokensQuery.data?.tokens, debouncedSearch]);
const isFetching = tokensQuery.isFetching || ownedTokensQuery.isFetching;
const setSearch = useCallback((search: string) => {
_setSearch(search);
setAllTokensLimit(ALL_TOKENS_LIMIT);
setOwnedTokensLimit(OWNED_TOKENS_LIMIT);
}, []);
return (
<SelectTokenUI
{...props}
ownedTokens={filteredOwnedTokens || []}
allTokens={tokensQuery.data || []}
isFetching={isFetching}
ownedTokens={{
data: filteredOwnedTokens || [],
isFetching: ownedTokensQuery.isFetching,
showMore: ownedTokensQuery.data?.pagination.hasMore
? () => {
setOwnedTokensLimit(ownedTokensLimit + OWNED_TOKENS_LIMIT);
}
: undefined,
}}
allTokens={{
data: tokensQuery.data || [],
isFetching: tokensQuery.isFetching,
showMore:
tokensQuery.data?.length === allTokensLimit
? () => {
setAllTokensLimit(allTokensLimit + ALL_TOKENS_LIMIT);
}
: undefined,
}}
selectedChain={selectedChain}
setSelectedChain={setSelectedChain}
search={search}
setSearch={setSearch}
selectedToken={props.selectedToken}
setSelectedToken={props.setSelectedToken}
showMore={
tokensQuery.data?.length === limit
? () => {
setLimit(limit * 2);
}
: undefined
}
/>
);
}
function SelectTokenUI(
props: SelectTokenUIProps & {
ownedTokens: TokenBalance[];
allTokens: Token[];
isFetching: boolean;
allTokens: {
data: Token[];
isFetching: boolean;
showMore: (() => void) | undefined;
};
ownedTokens: {
data: TokenBalance[];
isFetching: boolean;
showMore: (() => void) | undefined;
};
selectedChain: BridgeChain | undefined;
setSelectedChain: (chain: BridgeChain) => void;
search: string;
setSearch: (search: string) => void;
selectedToken: TokenSelection | undefined;
setSelectedToken: (token: TokenSelection) => void;
showMore: (() => void) | undefined;
},
) {
const isMobile = useIsMobile();
@@ -148,7 +169,7 @@ function SelectTokenUI(
// show tokens with icons first
const sortedOwnedTokens = useMemo(() => {
return props.ownedTokens.sort((a, b) => {
return props.ownedTokens.data.sort((a, b) => {
if (a.icon_uri && !b.icon_uri) {
return -1;
}
@@ -157,23 +178,11 @@ function SelectTokenUI(
}
return 0;
});
}, [props.ownedTokens]);
const otherTokens = useMemo(() => {
const ownedTokenSet = new Set(
sortedOwnedTokens.map((t) =>
`${t.token_address}-${t.chain_id}`.toLowerCase(),
),
);
return props.allTokens.filter(
(token) =>
!ownedTokenSet.has(`${token.address}-${token.chainId}`.toLowerCase()),
);
}, [props.allTokens, sortedOwnedTokens]);
}, [props.ownedTokens.data]);
// show tokens with icons first
const sortedOtherTokens = useMemo(() => {
return otherTokens.sort((a, b) => {
const sortedAllTokens = useMemo(() => {
return props.allTokens.data.sort((a, b) => {
if (a.iconUri && !b.iconUri) {
return -1;
}
@@ -182,7 +191,7 @@ function SelectTokenUI(
}
return 0;
});
}, [otherTokens]);
}, [props.allTokens.data]);
// desktop
if (!isMobile) {
@@ -214,15 +223,20 @@ function SelectTokenUI(
}}
isMobile={false}
selectedToken={props.selectedToken}
isFetching={props.isFetching}
ownedTokens={props.ownedTokens}
otherTokens={sortedOtherTokens}
showMore={props.showMore}
ownedTokens={{
...props.ownedTokens,
data: sortedOwnedTokens,
}}
allTokens={{
...props.allTokens,
data: sortedAllTokens,
}}
selectedChain={props.selectedChain}
onSelectChain={() => setScreen("select-chain")}
client={props.client}
search={props.search}
setSearch={props.setSearch}
isWalletConnected={props.activeWalletInfo !== undefined}
/>
</Container>
</Container>
@@ -232,15 +246,20 @@ function SelectTokenUI(
if (screen === "select-token") {
return (
<TokenSelectionScreen
isWalletConnected={props.activeWalletInfo !== undefined}
onSelectToken={(token) => {
props.setSelectedToken(token);
props.onClose();
}}
selectedToken={props.selectedToken}
isFetching={props.isFetching}
ownedTokens={props.ownedTokens}
otherTokens={sortedOtherTokens}
showMore={props.showMore}
ownedTokens={{
...props.ownedTokens,
data: sortedOwnedTokens,
}}
allTokens={{
...props.allTokens,
data: sortedAllTokens,
}}
selectedChain={props.selectedChain}
isMobile={true}
onSelectChain={() => setScreen("select-chain")}
@@ -428,17 +447,36 @@ function TokenSelectionScreen(props: {
client: ThirdwebClient;
search: string;
setSearch: (search: string) => void;
isFetching: boolean;
ownedTokens: TokenBalance[];
otherTokens: Token[];
showMore: (() => void) | undefined;
selectedToken: TokenSelection | undefined;
onSelectToken: (token: TokenSelection) => void;
isWalletConnected: boolean;
allTokens: {
data: Token[];
isFetching: boolean;
showMore: (() => void) | undefined;
};
ownedTokens: {
data: TokenBalance[];
isFetching: boolean;
showMore: (() => void) | undefined;
};
}) {
const noTokensFound =
!props.isFetching &&
props.otherTokens.length === 0 &&
props.ownedTokens.length === 0;
const noAllTokensFound =
!props.allTokens.isFetching && props.allTokens.data.length === 0;
const noOwnedTokensFound =
!props.ownedTokens.isFetching && props.ownedTokens.data.length === 0;
const [activeTab, setActiveTab] = useState<"owned" | "all">("all");
const showSkeletons =
(props.allTokens.isFetching && activeTab === "all") ||
(props.ownedTokens.isFetching && activeTab === "owned");
const showNoTokensFound =
(activeTab === "all" && noAllTokensFound) ||
(activeTab === "owned" && noOwnedTokensFound);
const theme = useCustomTheme();
return (
<Container fullHeight flex="column">
@@ -448,16 +486,14 @@ function TokenSelectionScreen(props: {
</Text>
<Spacer y="3xs" />
<Text
size="xs"
size="sm"
color="secondaryText"
multiline
style={{
textWrap: "pretty",
maxWidth: "70%",
}}
>
Select a token from the list or search for a token by symbol or
address
Select a token from the list or use the search
</Text>
</Container>
@@ -467,7 +503,7 @@ function TokenSelectionScreen(props: {
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "400px",
minHeight: "300px",
}}
>
<Spinner color="secondaryText" size="xl" />
@@ -498,7 +534,28 @@ function TokenSelectionScreen(props: {
/>
</Container>
<Spacer y="xs" />
<Spacer y="sm" />
{/* tabs */}
{props.isWalletConnected && (
<Container
flex="row"
gap="xs"
px="md"
style={{ borderBottom: `1px solid ${theme.colors.borderColor}` }}
>
<TabButton
isSelected={activeTab === "all"}
onSelect={() => setActiveTab("all")}
label="All Tokens"
/>
<TabButton
isSelected={activeTab === "owned"}
onSelect={() => setActiveTab("owned")}
label="Your Tokens"
/>
</Container>
)}
<Container
pb="md"
@@ -507,43 +564,24 @@ function TokenSelectionScreen(props: {
gap="xxs"
flex="column"
style={{
minHeight: "400px",
minHeight: "300px",
maxHeight: props.isMobile ? "450px" : "none",
overflowY: "auto",
scrollbarWidth: "none",
paddingBottom: spacing.md,
}}
>
{props.isFetching &&
{props.isWalletConnected && <Spacer y="xxs" />}
{showSkeletons &&
new Array(20).fill(0).map((_, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: ok
<TokenButtonSkeleton key={i} />
))}
{!props.isFetching && props.ownedTokens.length > 0 && (
<Container
px="xs"
py="xs"
flex="row"
gap="xs"
center="y"
color="secondaryText"
>
<WalletDotIcon size={iconSize.xs} />
<Text
size="sm"
color="secondaryText"
style={{
overflow: "unset",
}}
>
Your Tokens
</Text>
</Container>
)}
{!props.isFetching &&
props.ownedTokens.map((token) => (
{activeTab === "owned" &&
!props.ownedTokens.isFetching &&
props.ownedTokens.data.map((token) => (
<TokenButton
key={token.token_address}
token={token}
@@ -558,33 +596,9 @@ function TokenSelectionScreen(props: {
/>
))}
{!props.isFetching && props.ownedTokens.length > 0 && (
<Container
px="xs"
py="xs"
flex="row"
gap="xs"
center="y"
color="secondaryText"
style={{
marginTop: spacing.sm,
}}
>
<CoinsIcon size={iconSize.xs} />
<Text
size="sm"
color="secondaryText"
style={{
overflow: "unset",
}}
>
Other Tokens
</Text>
</Container>
)}
{!props.isFetching &&
props.otherTokens.map((token) => (
{activeTab === "all" &&
!props.allTokens.isFetching &&
props.allTokens.data.map((token) => (
<TokenButton
key={token.address}
token={token}
@@ -599,19 +613,31 @@ function TokenSelectionScreen(props: {
/>
))}
{props.showMore && (
{activeTab === "all" && props.allTokens.showMore && (
<Button
variant="secondary"
fullWidth
onClick={() => {
props.showMore?.();
props.allTokens.showMore?.();
}}
>
Load More
</Button>
)}
{noTokensFound && (
{activeTab === "owned" && props.ownedTokens.showMore && (
<Button
variant="secondary"
fullWidth
onClick={() => {
props.ownedTokens.showMore?.();
}}
>
Load More
</Button>
)}
{showNoTokensFound && (
<div
style={{
flex: 1,
@@ -620,7 +646,7 @@ function TokenSelectionScreen(props: {
justifyContent: "center",
}}
>
<Text size="md" color="secondaryText">
<Text size="sm" color="secondaryText">
No Tokens Found
</Text>
</div>
@@ -643,3 +669,45 @@ const LeftContainer = /* @__PURE__ */ StyledDiv((_) => {
position: "relative",
};
});
function TabButton(props: {
isSelected: boolean;
onSelect: () => void;
label: string;
}) {
const theme = useCustomTheme();
return (
<div
style={{
paddingBottom: "4px",
position: "relative",
}}
>
<Button
variant="ghost-solid"
onClick={props.onSelect}
style={{
fontSize: fontSize.sm,
padding: `10px ${spacing.xs}`,
color: props.isSelected
? theme.colors.primaryText
: theme.colors.secondaryText,
}}
>
{props.label}
</Button>
{props.isSelected && (
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: "-1.5px",
borderBottom: `2px solid ${theme.colors.primaryText}`,
}}
></div>
)}
</div>
);
}
@@ -46,17 +46,19 @@ export function WalletRow(props: {
const addressOrENS = address
? ensNameQuery.data || shortenAddress(address)
: "";
const iconSizeValue = iconSize[props.iconSize || "md"];
return (
<Container flex="row" style={{ justifyContent: "space-between" }}>
<Container center="y" color="secondaryText" flex="row" gap="md">
<Container center="y" color="secondaryText" flex="row" gap="sm">
{wallet ? (
<WalletImage
client={props.client}
id={wallet.id}
size={iconSize[props.iconSize || "md"]}
size={iconSizeValue}
/>
) : (
<OutlineWalletIcon size={iconSize[props.iconSize || "md"]} />
<OutlineWalletIcon size={iconSizeValue} />
)}
<Container flex="column" gap="3xs">
{props.label ? (
@@ -49,7 +49,7 @@ function formatMoney(
return new Intl.NumberFormat(locale, {
style: "currency",
currency: currencyCode,
maximumFractionDigits: 5,
maximumFractionDigits: 2,
minimumFractionDigits: 0,
}).format(value);
}
@@ -1,7 +1,9 @@
"use client";
import { CheckIcon, CopyIcon as CopyIconSVG } from "@radix-ui/react-icons";
import { radius } from "../../../core/design-system/index.js";
import { useClipboard } from "../hooks/useCopyClipboard.js";
import { Container } from "./basic.js";
import { Button } from "./buttons.js";
import { ToolTip } from "./Tooltip.js";
/**
@@ -19,25 +21,22 @@ export const CopyIcon: React.FC<{
const showCheckIcon = props.hasCopied || hasCopied;
return (
// biome-ignore lint/a11y/noStaticElementInteractions: TODO
<div
<Button
onClick={onCopy}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onCopy();
}
}}
variant="ghost-solid"
style={{
alignItems: "center",
display: "flex",
padding: 2,
justifyContent: "center",
borderRadius: radius.sm,
}}
>
<ToolTip align={props.align} side={props.side} tip={props.tip}>
<div>
<Container
center="both"
color={showCheckIcon ? "success" : undefined}
color={showCheckIcon ? "success" : "secondaryText"}
flex="row"
>
{showCheckIcon ? (
@@ -56,6 +55,6 @@ export const CopyIcon: React.FC<{
</Container>
</div>
</ToolTip>
</div>
</Button>
);
};
@@ -8,6 +8,7 @@ export const Spacer: React.FC<{ y: keyof typeof spacing }> = ({ y }) => {
<div
style={{
height: spacing[y],
flexShrink: 0,
}}
/>
);

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