diff --git a/.changeset/clever-carrots-march.md b/.changeset/clever-carrots-march.md
deleted file mode 100644
index e85a2296560..00000000000
--- a/.changeset/clever-carrots-march.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"thirdweb": minor
----
-
-Add headless components: ChainProvider, ChainIcon & ChainName
diff --git a/.changeset/pink-ducks-flash.md b/.changeset/pink-ducks-flash.md
deleted file mode 100644
index 2b444837147..00000000000
--- a/.changeset/pink-ducks-flash.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"thirdweb": patch
----
-
-Fix UI issue when assetTabs is set to an empty array
diff --git a/.changeset/selfish-deers-destroy.md b/.changeset/selfish-deers-destroy.md
deleted file mode 100644
index 84905ddb343..00000000000
--- a/.changeset/selfish-deers-destroy.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-"thirdweb": minor
----
-
-Adds LoyaltyCard extensions and support for ERC721 deployment.
-
-```ts
-import { deployERC721Contract } from "thirdweb/deploys";
-
-const loyaltyCardContractAddress = await deployERC721Contract({
- chain: "your-chain-id", // replace with your chain ID
- client: yourThirdwebClient, // replace with your Thirdweb client instance
- account: yourAccount, // replace with your account details
- type: "LoyaltyCard",
- params: {
- name: "MyLoyaltyCard",
- symbol: "LOYAL",
- description: "A loyalty card NFT contract",
- image: "path/to/image.png", // replace with your image path
- defaultAdmin: "0xYourAdminAddress", // replace with your admin address
- royaltyRecipient: "0xYourRoyaltyRecipient", // replace with your royalty recipient address
- royaltyBps: 500n, // 5% royalty
- trustedForwarders: ["0xTrustedForwarderAddress"], // replace with your trusted forwarder addresses
- saleRecipient: "0xYourSaleRecipient", // replace with your sale recipient address
- platformFeeBps: 200n, // 2% platform fee
- platformFeeRecipient: "0xYourPlatformFeeRecipient", // replace with your platform fee recipient address
- },
-});
-
-```
diff --git a/.changeset/stupid-buses-wink.md b/.changeset/stupid-buses-wink.md
new file mode 100644
index 00000000000..99248b52728
--- /dev/null
+++ b/.changeset/stupid-buses-wink.md
@@ -0,0 +1,41 @@
+---
+"thirdweb": minor
+---
+
+Add 2 new Pay functions: convertFiatToCrypto and convertCryptoToFiat
+
+Examples:
+
+### Convert fiat (USD) to crypto
+```ts
+import { convertFiatToCrypto } from "thirdweb/pay";
+import { ethereum } from "thirdweb/chains";
+
+// Convert 2 cents to ETH
+const result = await convertFiatToCrypto({
+ from: "USD",
+ // the token address. For native token, use NATIVE_TOKEN_ADDRESS
+ to: "0x...",
+ // the chain (of the chain where the token belong to)
+ chain: ethereum,
+ // 2 cents
+ fromAmount: 0.02,
+});
+// Result: 0.0000057 (a number)
+```
+
+### Convert crypto to fiat (USD)
+
+```ts
+import { convertCryptoToFiat } from "thirdweb/pay";
+
+// Get Ethereum price
+const result = convertCryptoToFiat({
+ fromTokenAddress: NATIVE_TOKEN_ADDRESS,
+ to: "USD",
+ chain: ethereum,
+ fromAmount: 1,
+});
+
+// Result: 3404.11 (number)
+```
\ No newline at end of file
diff --git a/.changeset/wild-games-vanish.md b/.changeset/wild-games-vanish.md
new file mode 100644
index 00000000000..3e26297745a
--- /dev/null
+++ b/.changeset/wild-games-vanish.md
@@ -0,0 +1,5 @@
+---
+"thirdweb": patch
+---
+
+Handle 0 value for maxPriorityFeePerGas in 712 transactions
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx
index bac9835b312..aab2ee3fd38 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/ClaimerSelection.tsx
@@ -1,6 +1,6 @@
+import { Button } from "@/components/ui/button";
import { Box, Flex, Select } from "@chakra-ui/react";
import { UploadIcon } from "lucide-react";
-import { Button, Text } from "tw-components";
import { useClaimConditionsFormContext } from "..";
import { CustomFormControl } from "../common";
@@ -115,13 +115,13 @@ export const ClaimerSelection = () => {
>
{/* disable the "Edit" button when form is disabled, but not when it's a "See" button */}
setOpenIndex(phaseIndex)}
- rightIcon={ }
>
{isAdmin ? "Edit" : "See"} Claimer Snapshot
+
{
}}
ml={2}
>
-
+
●{" "}
{field.snapshot?.length} address
{field.snapshot?.length === 1 ? "" : "es"}
{" "}
in snapshot
-
+
) : (
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
index 342e711acfb..853ae7956a0 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx
@@ -9,7 +9,6 @@ import {
} from "@/components/ui/sheet";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
-import { Box, Flex, Link } from "@chakra-ui/react";
import { useCsvUpload } from "hooks/useCsvUpload";
import { CircleAlertIcon, DownloadIcon, UploadIcon } from "lucide-react";
import { type Dispatch, type SetStateAction, useRef } from "react";
@@ -159,13 +158,7 @@ export const SnapshotUpload: React.FC = ({
columns={columns}
/>
) : (
-
+
= ({
addresses and their
.
(amount each wallet is allowed to claim)
-
Example
snapshot
-
+
You may optionally add and
@@ -219,14 +212,14 @@ export const SnapshotUpload: React.FC = ({
This lets you override the currency and price you would
like to charge per wallet you specified
-
Example
snapshot
-
+
>
) : (
@@ -235,10 +228,14 @@ export const SnapshotUpload: React.FC
= ({
Files must contain one .csv file with a list of
addresses.
-
+
Example
snapshot
-
+
You may optionally add a{" "}
@@ -247,14 +244,14 @@ export const SnapshotUpload: React.FC = ({
claim) If not specified, the default value is the one
you have set on your claim phase.
-
Example
snapshot
-
+
You may optionally add and
@@ -266,14 +263,14 @@ export const SnapshotUpload: React.FC = ({
define a price override.
-
Example
snapshot
-
+
>
)}
@@ -287,25 +284,12 @@ export const SnapshotUpload: React.FC = ({
-
+
)}
-
-
+
+
{!isDisabled && (
-
+
= ({
Next
)}
-
+
)}
-
+
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/nfts-owned.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/nfts-owned.tsx
index b7bd92ff6d0..cc577f6e1a7 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/nfts-owned.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/account/components/nfts-owned.tsx
@@ -2,7 +2,6 @@
import { useWalletNFTs } from "@3rdweb-sdk/react";
import type { ThirdwebContract } from "thirdweb";
-import { Text } from "tw-components";
import { NFTCards } from "../../_components/NFTCards";
interface NftsOwnedProps {
@@ -35,8 +34,8 @@ export const NftsOwned: React.FC = ({ contract }) => {
trackingCategory="account_nfts_owned"
/>
) : isWalletNFTsLoading ? null : error ? (
- Failed to fetch NFTs for this account: {error}
+ Failed to fetch NFTs for this account: {error}
) : (
- This account doesn't own any NFTs.
+ This account doesn't own any NFTs.
);
};
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/accounts/components/create-account-button.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/accounts/components/create-account-button.tsx
index 309bc6719a3..cf6626210d4 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/accounts/components/create-account-button.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/accounts/components/create-account-button.tsx
@@ -1,5 +1,7 @@
"use client";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
import { Tooltip } from "@chakra-ui/react";
import { TransactionButton } from "components/buttons/TransactionButton";
import type { ThirdwebContract } from "thirdweb";
@@ -9,7 +11,6 @@ import {
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
-import { Button, Card, Text } from "tw-components";
interface CreateAccountButtonProps {
contract: ThirdwebContract;
@@ -51,8 +52,8 @@ export const CreateAccountButton: React.FC = ({
return (
- You can only initialize one account per EOA.
+
+ You can only initialize one account per EOA.
}
bg="transparent"
@@ -62,7 +63,7 @@ export const CreateAccountButton: React.FC = ({
placement="right"
shouldWrapChildren
>
-
+
Account Created
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/airdrop-tab.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/airdrop-tab.tsx
index d9a86ef36b9..d010079b64d 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/airdrop-tab.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/airdrop-tab.tsx
@@ -1,5 +1,6 @@
"use client";
+import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
@@ -18,7 +19,6 @@ import type { ThirdwebContract } from "thirdweb";
import { multicall } from "thirdweb/extensions/common";
import { balanceOf, encodeSafeTransferFrom } from "thirdweb/extensions/erc1155";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
-import { Button, Text } from "tw-components";
import {
type AirdropAddressInput,
AirdropUpload,
@@ -118,12 +118,8 @@ const AirdropTab: React.FC = ({ contract, tokenId }) => {
- }
- >
- Upload addresses
+
+ Upload addresses
@@ -149,18 +145,18 @@ const AirdropTab: React.FC = ({ contract, tokenId }) => {
color={addresses.length === 0 ? "orange.500" : "green.500"}
>
{addresses.length > 0 && (
-
+
● {addresses.length} addresses ready to be
airdropped
-
+
)}
-
+
You can airdrop to a maximum of 250 addresses at a time. If you have
more, please do it in multiple transactions.
-
+
= ({
if (!nft) {
return (
-
+
No NFT found with token ID {tokenId}. Please check the token ID and try
again.
-
+
);
}
@@ -182,7 +181,7 @@ export const TokenIdPage: React.FC = ({
label={
tb.isDisabled ? (
- {tb.disabledText}
+ {tb.disabledText}
) : (
""
@@ -214,7 +213,7 @@ export const TokenIdPage: React.FC = ({
- Token ID
+ Token ID
= ({
{nft.owner && (
<>
- Owner
+ Owner
@@ -236,23 +235,21 @@ export const TokenIdPage: React.FC = ({
>
)}
- Token Standard
+ Token Standard
{nft.type}
{nft.type !== "ERC721" && (
<>
- Supply
+ Supply
-
- {nft.supply.toString()}
-
+ {nft.supply.toLocaleString("en-US")}
>
)}
- Token URI
+ Token URI
= ({
{nft.metadata.image && (
<>
- Media URI
+ Media URI
= ({
{properties ? (
- Attributes
+ Attributes
{Array.isArray(properties) &&
String(properties[0]?.value) !== "undefined" ? (
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/configuration/components/system.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/configuration/components/system.tsx
index 05bce7cb136..f077f07a5ee 100644
--- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/configuration/components/system.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/configuration/components/system.tsx
@@ -3,7 +3,6 @@ import {
useEngineSystemHealth,
useEngineSystemMetrics,
} from "@3rdweb-sdk/react/hooks/useEngine";
-import { Text } from "tw-components";
interface EngineSystemProps {
instance: EngineInstance;
@@ -17,7 +16,7 @@ export const EngineSystem: React.FC = ({ instance }) => {
}
return (
-
+
Version: {healthQuery.data.engineVersion ?? "..."}
Enabled: {healthQuery.data.features?.join(", ")}
@@ -25,6 +24,6 @@ export const EngineSystem: React.FC = ({ instance }) => {
CPU: {metricsQuery.data?.data?.cpu?.toFixed(2) ?? "..."}%
Memory: {metricsQuery.data?.data?.memory?.toFixed(0) ?? "..."}MB
-
+
);
};
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/insight/components/BlueprintsExplorer.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/insight/components/BlueprintsExplorer.tsx
index 1c4ff6582ef..578e1893d4e 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/insight/components/BlueprintsExplorer.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/insight/components/BlueprintsExplorer.tsx
@@ -1,7 +1,5 @@
"use client";
-import {} from "@/components/ui/dropdown-menu";
-import {} from "@/components/ui/select";
import { Layers3 } from "lucide-react";
import Link from "next/link";
diff --git a/apps/dashboard/src/components/contract-components/shared/sources-accordion.tsx b/apps/dashboard/src/components/contract-components/shared/sources-accordion.tsx
index 641b24f2671..bd5bcab863c 100644
--- a/apps/dashboard/src/components/contract-components/shared/sources-accordion.tsx
+++ b/apps/dashboard/src/components/contract-components/shared/sources-accordion.tsx
@@ -7,7 +7,6 @@ import {
AccordionPanel,
} from "@chakra-ui/react";
import type { Abi } from "abitype";
-import { Heading } from "tw-components";
import type { SourceFile } from "../types";
interface SourcesAccordionProps {
@@ -33,7 +32,7 @@ export const SourcesAccordion: React.FC = ({
{({ isExpanded }) => (
<>
- ABI
+ ABI
@@ -58,7 +57,7 @@ export const SourcesAccordion: React.FC = ({
{({ isExpanded }) => (
<>
- {signature.filename}
+ {signature.filename}
diff --git a/apps/dashboard/src/components/devRelEvents/AmbassadorCards.tsx b/apps/dashboard/src/components/devRelEvents/AmbassadorCards.tsx
index 2d00d7e76b4..26273a2d1da 100644
--- a/apps/dashboard/src/components/devRelEvents/AmbassadorCards.tsx
+++ b/apps/dashboard/src/components/devRelEvents/AmbassadorCards.tsx
@@ -1,7 +1,6 @@
import { Flex } from "@chakra-ui/react";
import { ChakraNextImage } from "components/Image";
import type { StaticImageData } from "next/image";
-import { Text } from "tw-components";
import type { ComponentWithChildren } from "types/component-with-children";
interface AmbassadorProps {
@@ -41,7 +40,7 @@ export const AmbassadorCard: ComponentWithChildren = ({
lineHeight={1.6}
textAlign="center"
>
- {children}
+ {children}
);
diff --git a/apps/dashboard/src/core-ui/batch-upload/batch-table.tsx b/apps/dashboard/src/core-ui/batch-upload/batch-table.tsx
index 6c9ed3b5769..a9c75bca356 100644
--- a/apps/dashboard/src/core-ui/batch-upload/batch-table.tsx
+++ b/apps/dashboard/src/core-ui/batch-upload/batch-table.tsx
@@ -28,7 +28,6 @@ import {
import { useMemo } from "react";
import { type Column, usePagination, useTable } from "react-table";
import type { NFTInput } from "thirdweb/utils";
-import { Text } from "tw-components";
const FileImage: React.FC = ({ src, ...props }) => {
const img = useImageFileOrUrl(
@@ -174,9 +173,9 @@ export const BatchTable: React.FC = ({
{headerGroup.headers.map((column, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
-
+
{column.render("Header")}
-
+
))}
@@ -227,10 +226,10 @@ export const BatchTable: React.FC = ({
icon={ }
onClick={() => previousPage()}
/>
-
+
Page {pageIndex + 1} of{" "}
{pageOptions.length}
-
+
{
maxW="80%"
/>
-
+
solves for both.
-
+
diff --git a/apps/dashboard/src/pages/rpc-edge.tsx b/apps/dashboard/src/pages/rpc-edge.tsx
index 15dabb6b1ac..a899464cd47 100644
--- a/apps/dashboard/src/pages/rpc-edge.tsx
+++ b/apps/dashboard/src/pages/rpc-edge.tsx
@@ -1,3 +1,4 @@
+import { Card } from "@/components/ui/card";
import { Container, Flex } from "@chakra-ui/react";
import { LandingEndCTA } from "components/landing-pages/end-cta";
import { LandingGridSection } from "components/landing-pages/grid-section";
@@ -8,7 +9,6 @@ import { LandingLayout } from "components/landing-pages/layout";
import { LandingSectionHeading } from "components/landing-pages/section-heading";
import { getAbsoluteUrl } from "lib/vercel-utils";
import { PageId } from "page-id";
-import { Card } from "tw-components";
import type { ThirdwebNextPage } from "utils/types";
const TRACKING_CATEGORY = "rpc-edge-landing";
@@ -95,31 +95,31 @@ const RPCEdgeLanding: ThirdwebNextPage = () => {
/>
-
+
-
+
-
+
-
+
-
+
{
/>
}
>
-
+
-
+
-
+
{
title="Powerful SDKs for every stack"
description="Build web3 applications that can interact with your smart contracts using our powerful SDKs and CLI."
buttonText="Get started"
- buttonLink="https://portal.thirdweb.com/contracts/interact/overview"
+ buttonLink="https://portal.thirdweb.com/contracts"
image={require("../../public/assets/product-pages/sdk/hero.png")}
gradient="linear-gradient(147.15deg, #410AB6 30.17%, #5BFF40 100.01%)"
>
@@ -135,27 +135,30 @@ const Web3SDK: ThirdwebNextPage = () => {
Interact with your contracts from your app in the programming
language that you’re familiar with our{" "}
React
,{" "}
ReactNative
,{" "}
TypeScript
,{" "}
-
+
Unity
{" "}
SDKs.
diff --git a/apps/dashboard/src/pages/storage.tsx b/apps/dashboard/src/pages/storage.tsx
index da7316e8a7d..c94f3b547e3 100644
--- a/apps/dashboard/src/pages/storage.tsx
+++ b/apps/dashboard/src/pages/storage.tsx
@@ -1,3 +1,4 @@
+import { Card } from "@/components/ui/card";
import { Container, Flex } from "@chakra-ui/react";
import { LandingEndCTA } from "components/landing-pages/end-cta";
import { LandingGridSection } from "components/landing-pages/grid-section";
@@ -7,7 +8,6 @@ import { LandingIconSectionItem } from "components/landing-pages/icon-section-it
import { LandingLayout } from "components/landing-pages/layout";
import { getAbsoluteUrl } from "lib/vercel-utils";
import { PageId } from "page-id";
-import { Card } from "tw-components";
import type { ThirdwebNextPage } from "utils/types";
const TRACKING_CATEGORY = "storage-landing";
@@ -108,21 +108,21 @@ const InteractLanding: ThirdwebNextPage = () => {
/>
-
+
-
+
-
+
+Consider using [Smart Backend Wallets](/engine/features/backend-wallets#smart-backend-wallets) instead. They combine EOA and smart account management into a single wallet, with automatic deployment and simplified integration:
+
+- No need to deploy or manage account factories
+- No separate account and backend wallet addresses to track
+- Works with all existing Engine endpoints without modification
+- Built-in transaction batching and recovery
+- Automatic gas management with paymaster
+
+Smart backend wallets are recommended when you need account abstraction for your own backend operations. Use the approach described below when you need to manage smart accounts for your users.
+
+
+
Use Engine to deploy, manage, and transact with smart accounts on behalf of your users.
## Deploy an account factory
diff --git a/apps/portal/src/app/engine/features/backend-wallets/page.mdx b/apps/portal/src/app/engine/features/backend-wallets/page.mdx
index a506013a2ee..64c9acf2ee6 100644
--- a/apps/portal/src/app/engine/features/backend-wallets/page.mdx
+++ b/apps/portal/src/app/engine/features/backend-wallets/page.mdx
@@ -1,4 +1,5 @@
import { createMetadata, Details } from "@doc";
+import { Callout } from "@doc";
export const metadata = createMetadata({
title: "Backend Wallets | thirdweb Engine",
@@ -12,6 +13,50 @@ Engine performs blockchain actions using backend wallets that you own and manage
There are multiple options for securing backend wallets.
+## Smart Backend Wallets
+
+Smart backend wallets are the recommended way to perform blockchain operations with Engine. Each smart backend wallet consists of an EOA (managed internally by Engine) and a smart account (using thirdweb's default account implementation).
+
+### Benefits
+
+Smart backend wallets inherit smart account benefits and offer several advantages over traditional EOAs:
+
+- **Gas Management**: Built-in paymaster eliminates the need to maintain gas tokens. This means you never need to hold crypto or top up gas.
+- **Better Nonce Management**: Smart accounts use multi-dimensional nonces, which are more efficient than EOAs.
+- **Simple Integration**: Works with all existing Engine endpoints without any code changes.
+
+### How it works
+
+The smart account is automatically deployed the first time you send a transaction on a chain. You don't need to think about deploying or managing the smart account - Engine handles everything behind the scenes. All transactions are sent as UserOperations to the EntryPoint contract on chain.
+
+
+ Unlike the [previous account abstraction implementation in
+ Engine](account-abstraction) where you had to manage both the account address
+ (`x-account-address`) and the backend wallet address
+ (`x-backend-wallet-address`) separately, smart backend wallets simplify this.
+ The backend wallet address is now the smart account address itself.
+
+
+
+ At this time, Smart Backend Wallets do not allow for importing a smart
+ account. In use cases where you need to import a smart account (such as with
+ session keys), you should use [Engine AA features that utilise the
+ `x-account-address` header](account-abstraction).
+
+
+### Configuration Types
+
+- `smart:local` - Smart account backed by a local key
+- `smart:aws-kms` - Smart account backed by AWS KMS
+- `smart:gcp-kms` - Smart account backed by Google Cloud KMS
+
+For AWS and Google Cloud KMS options, follow the setup instructions in the respective sections below.
+
+### Pricing
+
+Smart backend wallets have no additional costs to use with your Engine instance. Transactions sent from smart backend wallets follow the [same billing model as regular account abstraction transactions with thirdweb](/connect/account-abstraction/infrastructure#pricing--billing).
+Smart accounts need to be deployed on each chain. This costs gas and is billed to your account like any other transaction.
+
## Local wallet
A [local wallet](/references/wallets/v2/LocalWallet) is a wallet created or imported from a private key. Ensure your private key is backed up before transacting with a local wallet in a production environment.
@@ -31,7 +76,7 @@ An [AWS KMS Wallet](/references/wallets/v2/AwsKmsWallet) is a wallet securely st
- `kms:Sign`
- `kms:CreateAlias`
- `kms:Verify`
-1. On the user page, navigate to **Security credentials > Access keys**.
+1. On the user page, navigate to **Security credentials > Access keys**.
1. Select **Create access key** to get an **Access Key** and **Secret Key**.
1. In the dashboard, navigate to **Configuration > Backend Wallets**.
1. Select **AWS KMS** and provide the following:
@@ -50,7 +95,6 @@ An [AWS KMS Wallet](/references/wallets/v2/AwsKmsWallet) is a wallet securely st
- AWS KMS Key ID (example: `0489da75-9830-4a5a-97e3-e4a6df7775b3`)
- AWS KMS ARN (example: `arn:aws:kms:us-west-1:632186309261:key/0489da75-9830-4a5a-97e3-e4a6df7775b3`)
-
## Google Cloud KMS wallet
#### Setup
@@ -71,6 +115,7 @@ An [AWS KMS Wallet](/references/wallets/v2/AwsKmsWallet) is a wallet securely st
This is the Project ID of the GCP project where the key was created.
**Where to find**:
+
- Navigate to the Google Cloud Console.
- Click on the project dropdown at the top of the page.
- The Project ID is displayed under your project's name.
@@ -82,6 +127,7 @@ An [AWS KMS Wallet](/references/wallets/v2/AwsKmsWallet) is a wallet securely st
This is the location where the keyring was created (e.g., us-central1, europe-west1).
**Where to find**:
+
- In the Google Cloud Console, go to **Security > Cryptographic Keys**.
- Click on the keyring that contains your key.
- The location is displayed in the Location field.
@@ -93,6 +139,7 @@ An [AWS KMS Wallet](/references/wallets/v2/AwsKmsWallet) is a wallet securely st
This is the ID of the keyring where your key is stored.
**Where to find**:
+
- In the Google Cloud Console, go to **Security > Cryptographic Keys**.
- Select the keyring that contains your key.
- The KeyRing ID is displayed in the list or the URL.
@@ -104,6 +151,7 @@ An [AWS KMS Wallet](/references/wallets/v2/AwsKmsWallet) is a wallet securely st
This is the email associated with the service account used for accessing the KMS key.
**Where to find**:
+
- In the Google Cloud Console, go to **IAM & Admin > Service Accounts**.
- Find the service account you are using. its email will be in the format: `name@project.iam.gserviceaccount.com`
@@ -114,6 +162,7 @@ An [AWS KMS Wallet](/references/wallets/v2/AwsKmsWallet) is a wallet securely st
This is the private key of the service account that is used for authenticating API requests.
**Where to find**:
+
- Open the JSON file downloaded above.
- Copy the value of the `private_key` field.
diff --git a/apps/portal/src/app/glossary/gas/page.mdx b/apps/portal/src/app/glossary/gas/page.mdx
index e16fecb2903..8f4415a929c 100644
--- a/apps/portal/src/app/glossary/gas/page.mdx
+++ b/apps/portal/src/app/glossary/gas/page.mdx
@@ -7,7 +7,7 @@ Gas fees are typically paid in the currency that is native to the blockchain you
While using thirdweb is free, you will need to cover the cost of performing actions that update the blockchain's state.
-This includes actions performed via the thirdweb [dashboard](https://thirdweb.com/team), our [SDKs](/contracts/interact/overview), or any of our other products, including:
+This includes actions performed via the thirdweb [dashboard](https://thirdweb.com/team), our [SDKs](/contracts), or any of our other products, including:
- Deploying smart contracts
- [Lazy-minting](/glossary/lazy-minting) the metadata of your NFTs
diff --git a/apps/portal/src/app/glossary/ipfs/page.mdx b/apps/portal/src/app/glossary/ipfs/page.mdx
index e780e953690..0268d26c7c8 100644
--- a/apps/portal/src/app/glossary/ipfs/page.mdx
+++ b/apps/portal/src/app/glossary/ipfs/page.mdx
@@ -9,6 +9,6 @@ To secure content on IPFS forever, a node must [pin](https://docs.ipfs.tech/conc
[Learn more about how IPFS works](https://ipfs.tech/#how).
To read data from IPFS, an [IPFS Gateway](https://docs.ipfs.tech/concepts/ipfs-gateway/) is required. This allows you to access data from the IPFS protocol on browsers and other HTTP clients, such
-as when building an application using our [SDK](/contracts/interact/overview).
+as when building an application using our [SDK](/contracts).
Out of the box, all of our tools use [Storage](/storage) to store, pin, and retrieve data from IPFS via a gateway, currently powered by [Pinata](https://pinata.cloud/) under the hood.
diff --git a/apps/portal/src/app/page.tsx b/apps/portal/src/app/page.tsx
index 711f17a4d39..2ec7194ef0e 100644
--- a/apps/portal/src/app/page.tsx
+++ b/apps/portal/src/app/page.tsx
@@ -318,7 +318,7 @@ function ContractsSection() {
title="Interact"
description="Add smart contract interactions in your app"
icon={ContractInteractIcon}
- href="/contracts/interact/overview"
+ href="/contracts"
/>
;
diff --git a/packages/thirdweb/CHANGELOG.md b/packages/thirdweb/CHANGELOG.md
index e5c7e7b3fc1..93eee62c40a 100644
--- a/packages/thirdweb/CHANGELOG.md
+++ b/packages/thirdweb/CHANGELOG.md
@@ -1,5 +1,43 @@
# thirdweb
+## 5.72.0
+
+### Minor Changes
+
+- [#5495](https://github.com/thirdweb-dev/js/pull/5495) [`d1845f3`](https://github.com/thirdweb-dev/js/commit/d1845f3d6096d81e24bdb3cff38d19efd652ada1) Thanks [@kien-ngo](https://github.com/kien-ngo)! - Add headless components: ChainProvider, ChainIcon & ChainName
+
+- [#5529](https://github.com/thirdweb-dev/js/pull/5529) [`7488102`](https://github.com/thirdweb-dev/js/commit/7488102d20604a1d8cfd4653a34aa9a975f5c7f1) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Adds LoyaltyCard extensions and support for ERC721 deployment.
+
+ ```ts
+ import { deployERC721Contract } from "thirdweb/deploys";
+
+ const loyaltyCardContractAddress = await deployERC721Contract({
+ chain: "your-chain-id", // replace with your chain ID
+ client: yourThirdwebClient, // replace with your Thirdweb client instance
+ account: yourAccount, // replace with your account details
+ type: "LoyaltyCard",
+ params: {
+ name: "MyLoyaltyCard",
+ symbol: "LOYAL",
+ description: "A loyalty card NFT contract",
+ image: "path/to/image.png", // replace with your image path
+ defaultAdmin: "0xYourAdminAddress", // replace with your admin address
+ royaltyRecipient: "0xYourRoyaltyRecipient", // replace with your royalty recipient address
+ royaltyBps: 500n, // 5% royalty
+ trustedForwarders: ["0xTrustedForwarderAddress"], // replace with your trusted forwarder addresses
+ saleRecipient: "0xYourSaleRecipient", // replace with your sale recipient address
+ platformFeeBps: 200n, // 2% platform fee
+ platformFeeRecipient: "0xYourPlatformFeeRecipient", // replace with your platform fee recipient address
+ },
+ });
+ ```
+
+### Patch Changes
+
+- [#5517](https://github.com/thirdweb-dev/js/pull/5517) [`480fb4e`](https://github.com/thirdweb-dev/js/commit/480fb4e8ec02b79fdb8b00d709994c50ef929a28) Thanks [@kien-ngo](https://github.com/kien-ngo)! - Fix UI issue when assetTabs is set to an empty array
+
+- [#5548](https://github.com/thirdweb-dev/js/pull/5548) [`9337925`](https://github.com/thirdweb-dev/js/commit/93379251b79375784c1aac292dcaa209a1580b5e) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix tx cost estimation for pay transaction modal
+
## 5.71.0
### Minor Changes
diff --git a/packages/thirdweb/package.json b/packages/thirdweb/package.json
index b0321ef20a1..c99b0208de7 100644
--- a/packages/thirdweb/package.json
+++ b/packages/thirdweb/package.json
@@ -1,6 +1,6 @@
{
"name": "thirdweb",
- "version": "5.71.0",
+ "version": "5.72.0",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
diff --git a/packages/thirdweb/src/adapters/ethers5.test.ts b/packages/thirdweb/src/adapters/ethers5.test.ts
index c85da535acd..4239e5bc4f7 100644
--- a/packages/thirdweb/src/adapters/ethers5.test.ts
+++ b/packages/thirdweb/src/adapters/ethers5.test.ts
@@ -1,4 +1,5 @@
import * as ethers5 from "ethers5";
+import * as ethers6 from "ethers6";
import { describe, expect, it, test } from "vitest";
import { USDT_CONTRACT } from "~test/test-contracts.js";
import { ANVIL_CHAIN, FORKED_ETHEREUM_CHAIN } from "../../test/src/chains.js";
@@ -10,6 +11,7 @@ import { randomBytesBuffer } from "../utils/random.js";
import { privateKeyToAccount } from "../wallets/private-key.js";
import {
fromEthersContract,
+ fromEthersSigner,
toEthersContract,
toEthersProvider,
toEthersSigner,
@@ -149,4 +151,94 @@ describe("ethers5 adapter", () => {
const _decimals = await decimals({ contract: thirdwebContract });
expect(_decimals).toBe(6);
});
+
+ test("toEthersProvider should return a valid provider", async () => {
+ const provider = toEthersProvider(ethers5, TEST_CLIENT, ANVIL_CHAIN);
+ expect(provider).toBeDefined();
+ expect(provider.getBlockNumber).toBeDefined();
+
+ // Test if the provider can fetch the block number
+ const blockNumber = await provider.getBlockNumber();
+ expect(typeof blockNumber).toBe("number");
+ });
+
+ test("toEthersProvider should throw error with invalid ethers version", () => {
+ const invalidEthers = ethers6;
+ expect(() =>
+ // biome-ignore lint/suspicious/noExplicitAny: Testing invalid data
+ toEthersProvider(invalidEthers as any, TEST_CLIENT, ANVIL_CHAIN),
+ ).toThrow(
+ "You seem to be using ethers@6, please use the `ethers6Adapter()`",
+ );
+ });
+});
+
+describe("fromEthersSigner", () => {
+ it("should convert an ethers5 Signer to an Account", async () => {
+ const wallet = new ethers5.Wallet(ANVIL_PKEY_A);
+ const account = await fromEthersSigner(wallet);
+
+ expect(account).toBeDefined();
+ expect(account.address).toBe(await wallet.getAddress());
+ });
+
+ it("should sign a message", async () => {
+ const wallet = new ethers5.Wallet(ANVIL_PKEY_A);
+ const account = await fromEthersSigner(wallet);
+
+ const message = "Hello, world!";
+ const signature = await account.signMessage({ message });
+
+ expect(signature).toBe(await wallet.signMessage(message));
+ });
+
+ it("should sign a transaction", async () => {
+ const wallet = new ethers5.Wallet(
+ ANVIL_PKEY_A,
+ ethers5.getDefaultProvider(),
+ );
+ const account = await fromEthersSigner(wallet);
+
+ const transaction = {
+ to: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
+ value: 1n,
+ };
+
+ const signedTransaction = await account.signTransaction?.(transaction);
+
+ expect(signedTransaction).toBe(await wallet.signTransaction(transaction));
+ });
+
+ it("should sign typed data", async () => {
+ const wallet = new ethers5.Wallet(ANVIL_PKEY_A);
+ const account = await fromEthersSigner(wallet);
+
+ const domain = {
+ name: "Ether Mail",
+ version: "1",
+ chainId: 1,
+ verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
+ };
+
+ const types = {
+ Person: [
+ { name: "name", type: "string" },
+ { name: "wallet", type: "address" },
+ ],
+ };
+
+ const value = {
+ name: "Alice",
+ wallet: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
+ };
+
+ const signature = await account.signTypedData({
+ primaryType: "Person",
+ domain,
+ types,
+ message: value,
+ });
+
+ expect(signature).toBeDefined();
+ });
});
diff --git a/packages/thirdweb/src/adapters/ethers5.ts b/packages/thirdweb/src/adapters/ethers5.ts
index c4ca2d6e5cf..f3b84cbecdc 100644
--- a/packages/thirdweb/src/adapters/ethers5.ts
+++ b/packages/thirdweb/src/adapters/ethers5.ts
@@ -2,7 +2,7 @@ import type { Abi } from "abitype";
import * as universalethers from "ethers";
import type * as ethers5 from "ethers5";
import type * as ethers6 from "ethers6";
-import type { AccessList, Hex, TransactionSerializable } from "viem";
+import type { AccessList, Hex } from "viem";
import type { Chain } from "../chains/types.js";
import { getRpcUrlForChain } from "../chains/utils.js";
import type { ThirdwebClient } from "../client/client.js";
@@ -10,6 +10,7 @@ import { type ThirdwebContract, getContract } from "../contract/contract.js";
import { toSerializableTransaction } from "../transaction/actions/to-serializable-transaction.js";
import { waitForReceipt } from "../transaction/actions/wait-for-tx-receipt.js";
import type { PreparedTransaction } from "../transaction/prepare-transaction.js";
+import type { SerializableTransaction } from "../transaction/serialize-transaction.js";
import { toHex } from "../utils/encoding/hex.js";
import type { Account } from "../wallets/interfaces/wallet.js";
@@ -39,7 +40,7 @@ function assertEthers5(
): asserts ethers is typeof ethers5 {
if (!isEthers5(ethers)) {
throw new Error(
- "You seem to be using ethers@6, please use the `ethers6Adapter()",
+ "You seem to be using ethers@6, please use the `ethers6Adapter()`",
);
}
}
@@ -279,6 +280,7 @@ export function toEthersProvider(
client: ThirdwebClient,
chain: Chain,
): ethers5.providers.Provider {
+ assertEthers5(ethers);
const url = getRpcUrlForChain({ chain, client });
const headers: HeadersInit = {
"Content-Type": "application/json",
@@ -588,7 +590,7 @@ export async function toEthersSigner(
* @internal
*/
function alignTxToEthers(
- tx: TransactionSerializable,
+ tx: SerializableTransaction,
): ethers5.ethers.utils.Deferrable {
const { to: viemTo, type: viemType, gas, ...rest } = tx;
// massage "to" to fit ethers
@@ -623,8 +625,7 @@ function alignTxToEthers(
gasLimit: gas,
to,
type,
- accessList: tx.accessList as ethers5.utils.AccessListish | undefined,
- };
+ } as ethers5.ethers.utils.Deferrable;
}
async function alignTxFromEthers(
diff --git a/packages/thirdweb/src/adapters/ethers6.ts b/packages/thirdweb/src/adapters/ethers6.ts
index d552805ab60..de90941730f 100644
--- a/packages/thirdweb/src/adapters/ethers6.ts
+++ b/packages/thirdweb/src/adapters/ethers6.ts
@@ -2,13 +2,14 @@ import type { Abi } from "abitype";
import * as universalethers from "ethers";
import type * as ethers5 from "ethers5";
import type * as ethers6 from "ethers6";
-import type { AccessList, Hex, TransactionSerializable } from "viem";
+import type { AccessList, Hex } from "viem";
import type { Chain } from "../chains/types.js";
import { getRpcUrlForChain } from "../chains/utils.js";
import type { ThirdwebClient } from "../client/client.js";
import { type ThirdwebContract, getContract } from "../contract/contract.js";
import { toSerializableTransaction } from "../transaction/actions/to-serializable-transaction.js";
import type { PreparedTransaction } from "../transaction/prepare-transaction.js";
+import type { SerializableTransaction } from "../transaction/serialize-transaction.js";
import { toHex } from "../utils/encoding/hex.js";
import { resolvePromisedValue } from "../utils/promise/resolve-promised-value.js";
import type { Account } from "../wallets/interfaces/wallet.js";
@@ -493,7 +494,7 @@ export function toEthersSigner(
* @returns The aligned transaction object.
* @internal
*/
-function alignTxToEthers(tx: TransactionSerializable) {
+function alignTxToEthers(tx: SerializableTransaction) {
const { type: viemType, ...rest } = tx;
// massage "type" to fit ethers
diff --git a/packages/thirdweb/src/adapters/viem.test.ts b/packages/thirdweb/src/adapters/viem.test.ts
index 935fbdcc16d..03356d2cbe3 100644
--- a/packages/thirdweb/src/adapters/viem.test.ts
+++ b/packages/thirdweb/src/adapters/viem.test.ts
@@ -1,4 +1,4 @@
-import { type Account as ViemAccount, zeroAddress } from "viem";
+import type { Account as ViemAccount } from "viem";
import { privateKeyToAccount as viemPrivateKeyToAccount } from "viem/accounts";
import { beforeAll, describe, expect, test } from "vitest";
import { USDT_ABI } from "~test/abis/usdt.js";
@@ -6,7 +6,7 @@ import {
USDT_CONTRACT_ADDRESS,
USDT_CONTRACT_WITH_ABI,
} from "~test/test-contracts.js";
-import { ANVIL_PKEY_A } from "~test/test-wallets.js";
+import { ANVIL_PKEY_A, TEST_ACCOUNT_B } from "~test/test-wallets.js";
import { typedData } from "~test/typed-data.js";
import { ANVIL_CHAIN, FORKED_ETHEREUM_CHAIN } from "../../test/src/chains.js";
@@ -87,6 +87,7 @@ describe("walletClient.toViem", () => {
if (!walletClient.account) {
throw new Error("Account not found");
}
+
const txHash = await walletClient.sendTransaction({
account: walletClient.account,
chain: {
@@ -101,8 +102,8 @@ describe("walletClient.toViem", () => {
decimals: ANVIL_CHAIN.nativeCurrency?.decimals || 18,
},
},
- to: zeroAddress,
- value: 0n,
+ to: TEST_ACCOUNT_B.address,
+ value: 10n,
});
expect(txHash).toBeDefined();
expect(txHash.slice(0, 2)).toBe("0x");
diff --git a/packages/thirdweb/src/exports/pay.ts b/packages/thirdweb/src/exports/pay.ts
index 96c7b94fce2..f901ec55cd7 100644
--- a/packages/thirdweb/src/exports/pay.ts
+++ b/packages/thirdweb/src/exports/pay.ts
@@ -66,3 +66,13 @@ export type {
PayTokenInfo,
PayOnChainTransactionDetails,
} from "../pay/utils/commonTypes.js";
+
+export {
+ convertFiatToCrypto,
+ type ConvertFiatToCryptoParams,
+} from "../pay/convert/fiatToCrypto.js";
+
+export {
+ convertCryptoToFiat,
+ type ConvertCryptoToFiatParams,
+} from "../pay/convert/cryptoToFiat.js";
diff --git a/packages/thirdweb/src/pay/convert/cryptoToFiat.test.ts b/packages/thirdweb/src/pay/convert/cryptoToFiat.test.ts
new file mode 100644
index 00000000000..fed1b8e0aca
--- /dev/null
+++ b/packages/thirdweb/src/pay/convert/cryptoToFiat.test.ts
@@ -0,0 +1,95 @@
+import { describe, expect, it } from "vitest";
+import { TEST_CLIENT } from "~test/test-clients.js";
+import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
+import { base } from "../../chains/chain-definitions/base.js";
+import { ethereum } from "../../chains/chain-definitions/ethereum.js";
+import { sepolia } from "../../chains/chain-definitions/sepolia.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js";
+import { convertCryptoToFiat } from "./cryptoToFiat.js";
+
+describe.runIf(process.env.TW_SECRET_KEY)("Pay: crypto-to-fiat", () => {
+ it("should convert ETH price to USD on Ethereum mainnet", async () => {
+ const data = await convertCryptoToFiat({
+ chain: ethereum,
+ fromTokenAddress: NATIVE_TOKEN_ADDRESS,
+ fromAmount: 1,
+ to: "USD",
+ client: TEST_CLIENT,
+ });
+ expect(data.result).toBeDefined();
+ // Should be a number
+ expect(!Number.isNaN(data.result)).toBe(true);
+ // Since eth is around US$3000, we can add a test to check if the price is greater than $1500 (as a safe margin)
+ // let's hope that scenario does not happen :(
+ expect(Number(data.result) > 1500).toBe(true);
+ });
+
+ it("should convert ETH price to USD on Base mainnet", async () => {
+ const data = await convertCryptoToFiat({
+ chain: base,
+ fromTokenAddress: NATIVE_TOKEN_ADDRESS,
+ fromAmount: 1,
+ to: "USD",
+ client: TEST_CLIENT,
+ });
+ expect(data.result).toBeDefined();
+ // Should be a number
+ expect(!Number.isNaN(data.result)).toBe(true);
+ // Since eth is around US$3000, we can add a test to check if the price is greater than $1500 (as a safe margin)
+ // let's hope that scenario does not happen :(
+ expect(data.result > 1500).toBe(true);
+ });
+
+ it("should return zero if fromAmount is zero", async () => {
+ const data = await convertCryptoToFiat({
+ chain: base,
+ fromTokenAddress: NATIVE_TOKEN_ADDRESS,
+ fromAmount: 0,
+ to: "USD",
+ client: TEST_CLIENT,
+ });
+ expect(data.result).toBe(0);
+ });
+
+ it("should throw error for testnet chain (because testnets are not supported", async () => {
+ await expect(() =>
+ convertCryptoToFiat({
+ chain: sepolia,
+ fromTokenAddress: NATIVE_TOKEN_ADDRESS,
+ fromAmount: 1,
+ to: "USD",
+ client: TEST_CLIENT,
+ }),
+ ).rejects.toThrowError(
+ `Cannot fetch price for a testnet (chainId: ${sepolia.id})`,
+ );
+ });
+
+ it("should throw error if fromTokenAddress is set to an invalid EVM address", async () => {
+ await expect(() =>
+ convertCryptoToFiat({
+ chain: ethereum,
+ fromTokenAddress: "haha",
+ fromAmount: 1,
+ to: "USD",
+ client: TEST_CLIENT,
+ }),
+ ).rejects.toThrowError(
+ "Invalid fromTokenAddress. Expected a valid EVM contract address",
+ );
+ });
+
+ it("should throw error if fromTokenAddress is set to a wallet address", async () => {
+ await expect(() =>
+ convertCryptoToFiat({
+ chain: base,
+ fromTokenAddress: TEST_ACCOUNT_A.address,
+ fromAmount: 1,
+ to: "USD",
+ client: TEST_CLIENT,
+ }),
+ ).rejects.toThrowError(
+ `Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`,
+ );
+ });
+});
diff --git a/packages/thirdweb/src/pay/convert/cryptoToFiat.ts b/packages/thirdweb/src/pay/convert/cryptoToFiat.ts
new file mode 100644
index 00000000000..b046fedd3f7
--- /dev/null
+++ b/packages/thirdweb/src/pay/convert/cryptoToFiat.ts
@@ -0,0 +1,111 @@
+import type { Address } from "abitype";
+import type { Chain } from "../../chains/types.js";
+import type { ThirdwebClient } from "../../client/client.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js";
+import { getBytecode } from "../../contract/actions/get-bytecode.js";
+import { getContract } from "../../contract/contract.js";
+import { isAddress } from "../../utils/address.js";
+import { getClientFetch } from "../../utils/fetch.js";
+import { getPayConvertCryptoToFiatEndpoint } from "../utils/definitions.js";
+
+/**
+ * Props for the `convertCryptoToFiat` function
+ * @buyCrypto
+ */
+export type ConvertCryptoToFiatParams = {
+ client: ThirdwebClient;
+ /**
+ * The contract address of the token
+ * For native token, use NATIVE_TOKEN_ADDRESS
+ */
+ fromTokenAddress: Address;
+ /**
+ * The amount of token to convert to fiat value
+ */
+ fromAmount: number;
+ /**
+ * The chain that the token is deployed to
+ */
+ chain: Chain;
+ /**
+ * The fiat symbol. e.g "USD"
+ * Only USD is supported at the moment.
+ */
+ to: "USD";
+};
+
+/**
+ * Get a price of a token (using tokenAddress + chainId) in fiat.
+ * Only USD is supported at the moment.
+ * @example
+ * ### Basic usage
+ * For native token (non-ERC20), you should use NATIVE_TOKEN_ADDRESS as the value for `tokenAddress`
+ * ```ts
+ * import { convertCryptoToFiat } from "thirdweb/pay";
+ *
+ * // Get Ethereum price
+ * const result = convertCryptoToFiat({
+ * fromTokenAddress: NATIVE_TOKEN_ADDRESS,
+ * to: "USD",
+ * chain: ethereum,
+ * fromAmount: 1,
+ * });
+ *
+ * // Result: `{ result: 3404.11 }`
+ * ```
+ * @buyCrypto
+ * @returns a number representing the price (in selected fiat) of "x" token, with "x" being the `fromAmount`.
+ */
+export async function convertCryptoToFiat(
+ options: ConvertCryptoToFiatParams,
+): Promise<{ result: number }> {
+ const { client, fromTokenAddress, to, chain, fromAmount } = options;
+ if (Number(fromAmount) === 0) {
+ return { result: 0 };
+ }
+ // Testnets just don't work with our current provider(s)
+ if (chain.testnet === true) {
+ throw new Error(`Cannot fetch price for a testnet (chainId: ${chain.id})`);
+ }
+ // Some provider that we are using will return `0` for unsupported token
+ // so we should do some basic input validations before sending the request
+
+ // Make sure it's a valid EVM address
+ if (!isAddress(fromTokenAddress)) {
+ throw new Error(
+ "Invalid fromTokenAddress. Expected a valid EVM contract address",
+ );
+ }
+ // Make sure it's either a valid contract or a native token address
+ if (fromTokenAddress.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase()) {
+ const bytecode = await getBytecode(
+ getContract({
+ address: fromTokenAddress,
+ chain,
+ client,
+ }),
+ ).catch(() => undefined);
+ if (!bytecode || bytecode === "0x") {
+ throw new Error(
+ `Error: ${fromTokenAddress} on chainId: ${chain.id} is not a valid contract address.`,
+ );
+ }
+ }
+ const params = {
+ fromTokenAddress,
+ to,
+ chainId: String(chain.id),
+ fromAmount: String(fromAmount),
+ };
+ const queryString = new URLSearchParams(params).toString();
+ const url = `${getPayConvertCryptoToFiatEndpoint()}?${queryString}`;
+ const response = await getClientFetch(client)(url);
+ if (!response.ok) {
+ throw new Error(
+ `Failed to fetch ${to} value for token (${fromTokenAddress}) on chainId: ${chain.id}`,
+ );
+ }
+
+ const data: { result: number } = await response.json();
+ return data;
+}
diff --git a/packages/thirdweb/src/pay/convert/fiatToCrypto.test.ts b/packages/thirdweb/src/pay/convert/fiatToCrypto.test.ts
new file mode 100644
index 00000000000..3a74b4ef0d5
--- /dev/null
+++ b/packages/thirdweb/src/pay/convert/fiatToCrypto.test.ts
@@ -0,0 +1,96 @@
+import { describe, expect, it } from "vitest";
+import { TEST_CLIENT } from "~test/test-clients.js";
+import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
+import { base } from "../../chains/chain-definitions/base.js";
+import { ethereum } from "../../chains/chain-definitions/ethereum.js";
+import { sepolia } from "../../chains/chain-definitions/sepolia.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js";
+import { convertFiatToCrypto } from "./fiatToCrypto.js";
+
+describe.runIf(process.env.TW_SECRET_KEY)("Pay: fiatToCrypto", () => {
+ it("should convert fiat price to token on Ethereum mainnet", async () => {
+ const data = await convertFiatToCrypto({
+ chain: ethereum,
+ from: "USD",
+ fromAmount: 1,
+ to: NATIVE_TOKEN_ADDRESS,
+ client: TEST_CLIENT,
+ });
+ expect(data.result).toBeDefined();
+ // Should be a number
+ expect(!Number.isNaN(data.result)).toBe(true);
+ // Since eth is around US$3000, 1 USD should be around 0.0003
+ // we give it some safe margin so the test won't be flaky
+ expect(data.result < 0.001).toBe(true);
+ });
+
+ it("should convert fiat price to token on Base mainnet", async () => {
+ const data = await convertFiatToCrypto({
+ chain: base,
+ from: "USD",
+ fromAmount: 1,
+ to: NATIVE_TOKEN_ADDRESS,
+ client: TEST_CLIENT,
+ });
+
+ expect(data.result).toBeDefined();
+ // Should be a number
+ expect(!Number.isNaN(data.result)).toBe(true);
+ // Since eth is around US$3000, 1 USD should be around 0.0003
+ // we give it some safe margin so the test won't be flaky
+ expect(data.result < 0.001).toBe(true);
+ });
+
+ it("should return zero if the fromAmount is zero", async () => {
+ const data = await convertFiatToCrypto({
+ chain: base,
+ from: "USD",
+ fromAmount: 0,
+ to: NATIVE_TOKEN_ADDRESS,
+ client: TEST_CLIENT,
+ });
+ expect(data.result).toBe(0);
+ });
+
+ it("should throw error for testnet chain (because testnets are not supported", async () => {
+ await expect(() =>
+ convertFiatToCrypto({
+ chain: sepolia,
+ to: NATIVE_TOKEN_ADDRESS,
+ fromAmount: 1,
+ from: "USD",
+ client: TEST_CLIENT,
+ }),
+ ).rejects.toThrowError(
+ `Cannot fetch price for a testnet (chainId: ${sepolia.id})`,
+ );
+ });
+
+ it("should throw error if `to` is set to an invalid EVM address", async () => {
+ await expect(() =>
+ convertFiatToCrypto({
+ chain: ethereum,
+ to: "haha",
+ fromAmount: 1,
+ from: "USD",
+ client: TEST_CLIENT,
+ }),
+ ).rejects.toThrowError(
+ "Invalid `to`. Expected a valid EVM contract address",
+ );
+ });
+
+ it("should throw error if `to` is set to a wallet address", async () => {
+ await expect(() =>
+ convertFiatToCrypto({
+ chain: base,
+ to: TEST_ACCOUNT_A.address,
+ fromAmount: 1,
+ from: "USD",
+ client: TEST_CLIENT,
+ }),
+ ).rejects.toThrowError(
+ `Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`,
+ );
+ });
+});
diff --git a/packages/thirdweb/src/pay/convert/fiatToCrypto.ts b/packages/thirdweb/src/pay/convert/fiatToCrypto.ts
new file mode 100644
index 00000000000..7d6e6083c7f
--- /dev/null
+++ b/packages/thirdweb/src/pay/convert/fiatToCrypto.ts
@@ -0,0 +1,110 @@
+import type { Address } from "abitype";
+import type { Chain } from "../../chains/types.js";
+import type { ThirdwebClient } from "../../client/client.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js";
+import { getBytecode } from "../../contract/actions/get-bytecode.js";
+import { getContract } from "../../contract/contract.js";
+import { isAddress } from "../../utils/address.js";
+import { getClientFetch } from "../../utils/fetch.js";
+import { getPayConvertFiatToCryptoEndpoint } from "../utils/definitions.js";
+
+/**
+ * Props for the `convertFiatToCrypto` function
+ * @buyCrypto
+ */
+export type ConvertFiatToCryptoParams = {
+ client: ThirdwebClient;
+ /**
+ * The fiat symbol. e.g: "USD"
+ * Currently only USD is supported.
+ */
+ from: "USD";
+ /**
+ * The total amount of fiat to convert
+ * e.g: If you want to convert 2 cents to USD, enter `0.02`
+ */
+ fromAmount: number;
+ /**
+ * The token address
+ * For native token, use NATIVE_TOKEN_ADDRESS
+ */
+ to: Address;
+ /**
+ * The chain that the token is deployed to
+ */
+ chain: Chain;
+};
+
+/**
+ * Convert a fiat value to a token.
+ * Currently only USD is supported.
+ * @example
+ * ### Basic usage
+ * ```ts
+ * import { convertFiatToCrypto } from "thirdweb/pay";
+ *
+ * // Convert 2 cents to ETH
+ * const result = await convertFiatToCrypto({
+ * from: "USD",
+ * // the token address. For native token, use NATIVE_TOKEN_ADDRESS
+ * to: "0x...",
+ * // the chain (of the chain where the token belong to)
+ * chain: ethereum,
+ * // 2 cents
+ * fromAmount: 0.02,
+ * });
+ * ```
+ * Result: `{ result: 0.0000057 }`
+ * @buyCrypto
+ */
+export async function convertFiatToCrypto(
+ options: ConvertFiatToCryptoParams,
+): Promise<{ result: number }> {
+ const { client, from, to, chain, fromAmount } = options;
+ if (Number(fromAmount) === 0) {
+ return { result: 0 };
+ }
+ // Testnets just don't work with our current provider(s)
+ if (chain.testnet === true) {
+ throw new Error(`Cannot fetch price for a testnet (chainId: ${chain.id})`);
+ }
+ // Some provider that we are using will return `0` for unsupported token
+ // so we should do some basic input validations before sending the request
+
+ // Make sure it's a valid EVM address
+ if (!isAddress(to)) {
+ throw new Error("Invalid `to`. Expected a valid EVM contract address");
+ }
+ // Make sure it's either a valid contract or a native token
+ if (to.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase()) {
+ const bytecode = await getBytecode(
+ getContract({
+ address: to,
+ chain,
+ client,
+ }),
+ ).catch(() => undefined);
+ if (!bytecode || bytecode === "0x") {
+ throw new Error(
+ `Error: ${to} on chainId: ${chain.id} is not a valid contract address.`,
+ );
+ }
+ }
+ const params = {
+ from,
+ to,
+ chainId: String(chain.id),
+ fromAmount: String(fromAmount),
+ };
+ const queryString = new URLSearchParams(params).toString();
+ const url = `${getPayConvertFiatToCryptoEndpoint()}?${queryString}`;
+ const response = await getClientFetch(client)(url);
+ if (!response.ok) {
+ throw new Error(
+ `Failed to convert ${to} value to token (${to}) on chainId: ${chain.id}`,
+ );
+ }
+
+ const data: { result: number } = await response.json();
+ return data;
+}
diff --git a/packages/thirdweb/src/pay/utils/definitions.ts b/packages/thirdweb/src/pay/utils/definitions.ts
index 313cf16094c..f1763343238 100644
--- a/packages/thirdweb/src/pay/utils/definitions.ts
+++ b/packages/thirdweb/src/pay/utils/definitions.ts
@@ -76,3 +76,9 @@ export const getPaySupportedSources = () =>
*/
export const getPayBuyHistoryEndpoint = () =>
`${getPayBaseUrl()}/wallet/history/v1`;
+
+export const getPayConvertFiatToCryptoEndpoint = () =>
+ `${getPayBaseUrl()}/convert/fiat-to-crypto/v1`;
+
+export const getPayConvertCryptoToFiatEndpoint = () =>
+ `${getPayBaseUrl()}/convert/crypto-to-fiat/v1`;
diff --git a/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts b/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts
index b8b281938cd..922a213eda5 100644
--- a/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts
+++ b/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts
@@ -1,14 +1,13 @@
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
import type { Chain } from "../../../../chains/types.js";
-import { getGasPrice } from "../../../../gas/get-gas-price.js";
import type { BuyWithCryptoStatus } from "../../../../pay/buyWithCrypto/getStatus.js";
import type { BuyWithFiatStatus } from "../../../../pay/buyWithFiat/getStatus.js";
import type { FiatProvider } from "../../../../pay/utils/commonTypes.js";
-import { estimateGasCost } from "../../../../transaction/actions/estimate-gas-cost.js";
import type { GaslessOptions } from "../../../../transaction/actions/gasless/types.js";
import { sendTransaction } from "../../../../transaction/actions/send-transaction.js";
import type { WaitForReceiptOptions } from "../../../../transaction/actions/wait-for-tx-receipt.js";
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
+import { getTransactionGasCost } from "../../../../transaction/utils.js";
import { resolvePromisedValue } from "../../../../utils/promise/resolve-promised-value.js";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
import { getTokenBalance } from "../../../../wallets/utils/getTokenBalance.js";
@@ -215,7 +214,7 @@ export function useSendTransactionCore(args: {
tokenAddress: _erc20Value.tokenAddress,
})
: undefined,
- getTotalTxCostForBuy(tx, account.address),
+ getTransactionGasCost(tx, account.address),
]);
const gasSponsored = hasSponsoredTransactionsEnabled(wallet);
@@ -248,39 +247,3 @@ export function useSendTransactionCore(args: {
},
});
}
-
-async function getTotalTxCostForBuy(tx: PreparedTransaction, from?: string) {
- try {
- const gasCost = await estimateGasCost({
- transaction: tx,
- from,
- });
-
- const bufferCost = gasCost.wei / 10n;
-
- // Note: get tx.value AFTER estimateGasCost
- const txValue = await resolvePromisedValue(tx.value);
-
- // add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
- return gasCost.wei + bufferCost + (txValue || 0n);
- } catch {
- if (from) {
- // try again without passing from
- return await getTotalTxCostForBuy(tx);
- }
- // fallback if both fail, use the tx value + 2M * gas price
- const value = await resolvePromisedValue(tx.value);
-
- const gasPrice = await getGasPrice({
- client: tx.client,
- chain: tx.chain,
- });
-
- const buffer = 2_000_000n * gasPrice;
-
- if (!value) {
- return 0n + buffer;
- }
- return value + buffer;
- }
-}
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useBuyTxStates.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useBuyTxStates.ts
index 919da47731f..f913825b356 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useBuyTxStates.ts
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/useBuyTxStates.ts
@@ -4,10 +4,9 @@ import { getChainMetadata } from "../../../../../../../chains/utils.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js";
import { getContract } from "../../../../../../../contract/contract.js";
import { getCurrencyMetadata } from "../../../../../../../extensions/erc20/read/getCurrencyMetadata.js";
-import { getGasPrice } from "../../../../../../../gas/get-gas-price.js";
import { encode } from "../../../../../../../transaction/actions/encode.js";
-import { estimateGasCost } from "../../../../../../../transaction/actions/estimate-gas-cost.js";
import type { PreparedTransaction } from "../../../../../../../transaction/prepare-transaction.js";
+import { getTransactionGasCost } from "../../../../../../../transaction/utils.js";
import type { Hex } from "../../../../../../../utils/encoding/hex.js";
import { resolvePromisedValue } from "../../../../../../../utils/promise/resolve-promised-value.js";
import type { Account } from "../../../../../../../wallets/interfaces/wallet.js";
@@ -138,30 +137,3 @@ export function useTransactionCostAndData(args: {
},
});
}
-
-async function getTransactionGasCost(tx: PreparedTransaction, from?: string) {
- try {
- const gasCost = await estimateGasCost({
- transaction: tx,
- from,
- });
-
- const bufferCost = gasCost.wei / 10n;
-
- // Note: get tx.value AFTER estimateGasCost
- // add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
- return gasCost.wei + bufferCost;
- } catch {
- if (from) {
- // try again without passing from
- return await getTransactionGasCost(tx);
- }
- // fallback if both fail, use the tx value + 2M * gas price
- const gasPrice = await getGasPrice({
- client: tx.client,
- chain: tx.chain,
- });
-
- return 2_000_000n * gasPrice;
- }
-}
diff --git a/packages/thirdweb/src/transaction/actions/gasless/providers/biconomy.ts b/packages/thirdweb/src/transaction/actions/gasless/providers/biconomy.ts
index adbf0e4a100..0c9442e4698 100644
--- a/packages/thirdweb/src/transaction/actions/gasless/providers/biconomy.ts
+++ b/packages/thirdweb/src/transaction/actions/gasless/providers/biconomy.ts
@@ -1,5 +1,5 @@
import type { Address } from "abitype";
-import { type TransactionSerializable, encodeAbiParameters } from "viem";
+import { encodeAbiParameters } from "viem";
import { ZERO_ADDRESS } from "../../../../constants/addresses.js";
import { getContract } from "../../../../contract/contract.js";
import { isHex } from "../../../../utils/encoding/helpers/is-hex.js";
@@ -8,6 +8,7 @@ import { stringify } from "../../../../utils/json.js";
import type { Account } from "../../../../wallets/interfaces/wallet.js";
import type { PreparedTransaction } from "../../../prepare-transaction.js";
import { readContract } from "../../../read-contract.js";
+import type { SerializableTransaction } from "../../../serialize-transaction.js";
import type { WaitForReceiptOptions } from "../../wait-for-tx-receipt.js";
/**
@@ -27,7 +28,7 @@ type SendBiconomyTransactionOptions = {
// TODO: update this to `Transaction<"prepared">` once the type is available to ensure only prepared transactions are accepted
// biome-ignore lint/suspicious/noExplicitAny: library function that accepts any prepared transaction type
transaction: PreparedTransaction;
- serializableTransaction: TransactionSerializable;
+ serializableTransaction: SerializableTransaction;
gasless: BiconomyOptions;
};
diff --git a/packages/thirdweb/src/transaction/actions/gasless/providers/engine.ts b/packages/thirdweb/src/transaction/actions/gasless/providers/engine.ts
index 56796a2e872..ab0f115741e 100644
--- a/packages/thirdweb/src/transaction/actions/gasless/providers/engine.ts
+++ b/packages/thirdweb/src/transaction/actions/gasless/providers/engine.ts
@@ -1,10 +1,10 @@
import type { Address } from "abitype";
-import type { TransactionSerializable } from "viem";
import { getContract } from "../../../../contract/contract.js";
import { stringify } from "../../../../utils/json.js";
import type { Account } from "../../../../wallets/interfaces/wallet.js";
import type { PreparedTransaction } from "../../../prepare-transaction.js";
import { readContract } from "../../../read-contract.js";
+import type { SerializableTransaction } from "../../../serialize-transaction.js";
import {
type WaitForReceiptOptions,
waitForReceipt,
@@ -28,7 +28,7 @@ type SendengineTransactionOptions = {
// TODO: update this to `Transaction<"prepared">` once the type is available to ensure only prepared transactions are accepted
// biome-ignore lint/suspicious/noExplicitAny: library function that accepts any prepared transaction type
transaction: PreparedTransaction;
- serializableTransaction: TransactionSerializable;
+ serializableTransaction: SerializableTransaction;
gasless: EngineOptions;
};
diff --git a/packages/thirdweb/src/transaction/actions/gasless/providers/openzeppelin.ts b/packages/thirdweb/src/transaction/actions/gasless/providers/openzeppelin.ts
index 3143e80a1ad..d643fd4f1f2 100644
--- a/packages/thirdweb/src/transaction/actions/gasless/providers/openzeppelin.ts
+++ b/packages/thirdweb/src/transaction/actions/gasless/providers/openzeppelin.ts
@@ -1,11 +1,11 @@
import type { Address } from "abitype";
-import type { TransactionSerializable } from "viem";
import { getContract } from "../../../../contract/contract.js";
import { isHex } from "../../../../utils/encoding/helpers/is-hex.js";
import { stringify } from "../../../../utils/json.js";
import type { Account } from "../../../../wallets/interfaces/wallet.js";
import type { PreparedTransaction } from "../../../prepare-transaction.js";
import { readContract } from "../../../read-contract.js";
+import type { SerializableTransaction } from "../../../serialize-transaction.js";
import type { WaitForReceiptOptions } from "../../wait-for-tx-receipt.js";
/**
@@ -26,7 +26,7 @@ type SendOpenZeppelinTransactionOptions = {
// TODO: update this to `Transaction<"prepared">` once the type is available to ensure only prepared transactions are accepted
// biome-ignore lint/suspicious/noExplicitAny: library function that accepts any prepared transaction type
transaction: PreparedTransaction;
- serializableTransaction: TransactionSerializable;
+ serializableTransaction: SerializableTransaction;
gasless: OpenZeppelinOptions;
};
diff --git a/packages/thirdweb/src/transaction/actions/gasless/send-gasless-transaction.ts b/packages/thirdweb/src/transaction/actions/gasless/send-gasless-transaction.ts
index f4f33371648..e43187db346 100644
--- a/packages/thirdweb/src/transaction/actions/gasless/send-gasless-transaction.ts
+++ b/packages/thirdweb/src/transaction/actions/gasless/send-gasless-transaction.ts
@@ -1,6 +1,6 @@
-import type { TransactionSerializable } from "viem";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import type { PreparedTransaction } from "../../prepare-transaction.js";
+import type { SerializableTransaction } from "../../serialize-transaction.js";
import { addTransactionToStore } from "../../transaction-store.js";
import type { WaitForReceiptOptions } from "../wait-for-tx-receipt.js";
import type { GaslessOptions } from "./types.js";
@@ -10,7 +10,7 @@ type SendGaslessTransactionOptions = {
// TODO: update this to `Transaction<"prepared">` once the type is available to ensure only prepared transactions are accepted
// biome-ignore lint/suspicious/noExplicitAny: library function that accepts any prepared transaction type
transaction: PreparedTransaction;
- serializableTransaction: TransactionSerializable;
+ serializableTransaction: SerializableTransaction;
gasless: GaslessOptions;
};
diff --git a/packages/thirdweb/src/transaction/actions/sign-transaction.test.ts b/packages/thirdweb/src/transaction/actions/sign-transaction.test.ts
index af4cce65976..257d96aeb73 100644
--- a/packages/thirdweb/src/transaction/actions/sign-transaction.test.ts
+++ b/packages/thirdweb/src/transaction/actions/sign-transaction.test.ts
@@ -1,27 +1,20 @@
-import {
- type TransactionSerializable,
- type TransactionSerializableBase,
- type TransactionSerializableEIP1559,
- type TransactionSerializableEIP2930,
- type TransactionSerializableLegacy,
- zeroAddress,
-} from "viem";
import { describe, expect, test } from "vitest";
import { ANVIL_PKEY_A } from "~test/test-wallets.js";
+import { ZERO_ADDRESS } from "../../constants/addresses.js";
import { fromGwei } from "../../utils/units.js";
import { signTransaction } from "./sign-transaction.js";
const BASE_TRANSACTION = {
gas: 21000n,
nonce: 785,
-} satisfies TransactionSerializableBase;
+} as const;
describe("eip1559", () => {
const BASE_EIP1559_TRANSACTION = {
...BASE_TRANSACTION,
chainId: 1,
type: "eip1559",
- } as const satisfies TransactionSerializableEIP1559;
+ } as const;
test("default", () => {
const signature = signTransaction({
@@ -69,7 +62,7 @@ describe("eip1559", () => {
...BASE_EIP1559_TRANSACTION,
accessList: [
{
- address: zeroAddress,
+ address: ZERO_ADDRESS,
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000001",
"0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe",
@@ -105,7 +98,7 @@ describe("eip2930", () => {
...BASE_TRANSACTION,
chainId: 1,
type: "eip2930",
- } as const satisfies TransactionSerializable;
+ } as const;
test("default", () => {
const signature = signTransaction({
@@ -124,13 +117,13 @@ describe("eip2930", () => {
gasPrice: fromGwei("2"),
accessList: [
{
- address: zeroAddress,
+ address: ZERO_ADDRESS,
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000000",
],
},
],
- } as TransactionSerializableEIP2930,
+ },
privateKey: ANVIL_PKEY_A,
});
@@ -159,7 +152,7 @@ describe("legacy", () => {
...BASE_TRANSACTION,
gasPrice: fromGwei("2"),
type: "legacy",
- } as const satisfies TransactionSerializableLegacy;
+ } as const;
test("default", () => {
const signature = signTransaction({
diff --git a/packages/thirdweb/src/transaction/actions/sign-transaction.ts b/packages/thirdweb/src/transaction/actions/sign-transaction.ts
index 13605d02e33..efabb96d947 100644
--- a/packages/thirdweb/src/transaction/actions/sign-transaction.ts
+++ b/packages/thirdweb/src/transaction/actions/sign-transaction.ts
@@ -1,11 +1,13 @@
-import type { TransactionSerializable } from "viem";
+import * as ox__Hash from "ox/Hash";
+import * as ox__Secp256k1 from "ox/Secp256k1";
import type { Hex } from "../../utils/encoding/hex.js";
-import { keccak256 } from "../../utils/hashing/keccak256.js";
-import { sign } from "../../utils/signatures/sign.js";
-import { serializeTransaction } from "../serialize-transaction.js";
+import {
+ type SerializableTransaction,
+ serializeTransaction,
+} from "../serialize-transaction.js";
export type SignTransactionOptions = {
- transaction: TransactionSerializable;
+ transaction: SerializableTransaction;
privateKey: Hex;
// TODO: Add optional custom serializer here
};
@@ -32,14 +34,10 @@ export function signTransaction({
transaction,
privateKey,
}: SignTransactionOptions): Hex {
- if (transaction.type === "eip4844") {
- transaction = { ...transaction, sidecars: false };
- }
-
const serializedTransaction = serializeTransaction({ transaction });
- const signature = sign({
- hash: keccak256(serializedTransaction),
+ const signature = ox__Secp256k1.sign({
+ payload: ox__Hash.keccak256(serializedTransaction),
privateKey: privateKey,
});
return serializeTransaction({
diff --git a/packages/thirdweb/src/transaction/actions/to-serializable-transaction.ts b/packages/thirdweb/src/transaction/actions/to-serializable-transaction.ts
index 29e2ab5598a..073b11d28b4 100644
--- a/packages/thirdweb/src/transaction/actions/to-serializable-transaction.ts
+++ b/packages/thirdweb/src/transaction/actions/to-serializable-transaction.ts
@@ -1,10 +1,10 @@
-import type { TransactionSerializable } from "viem";
import { getGasOverridesForTransaction } from "../../gas/fee-data.js";
import { getRpcClient } from "../../rpc/rpc.js";
import { getAddress } from "../../utils/address.js";
import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js";
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
import type { PreparedTransaction } from "../prepare-transaction.js";
+import type { SerializableTransaction } from "../serialize-transaction.js";
import { encode } from "./encode.js";
import { estimateGas } from "./estimate-gas.js";
@@ -112,5 +112,5 @@ export async function toSerializableTransaction(
accessList,
value,
...feeData,
- } satisfies TransactionSerializable;
+ } satisfies SerializableTransaction;
}
diff --git a/packages/thirdweb/src/transaction/actions/zksync/getEip721Domain.ts b/packages/thirdweb/src/transaction/actions/zksync/getEip721Domain.ts
index 60b30c117de..bb548218315 100644
--- a/packages/thirdweb/src/transaction/actions/zksync/getEip721Domain.ts
+++ b/packages/thirdweb/src/transaction/actions/zksync/getEip721Domain.ts
@@ -1,4 +1,3 @@
-import type { TransactionSerializable } from "viem";
import { hashBytecode } from "viem/zksync";
import type { Address } from "../../../utils/address.js";
import { toHex } from "../../../utils/encoding/hex.js";
@@ -6,8 +5,9 @@ import type {
EIP712SerializedTransaction,
EIP712TransactionOptions,
} from "../../prepare-transaction.js";
+import type { SerializableTransaction } from "../../serialize-transaction.js";
-export type EIP721TransactionSerializable = TransactionSerializable & {
+export type EIP721TransactionSerializable = SerializableTransaction & {
from: Address;
} & EIP712TransactionOptions;
export const gasPerPubdataDefault = 50000n;
diff --git a/packages/thirdweb/src/transaction/actions/zksync/send-eip712-transaction.ts b/packages/thirdweb/src/transaction/actions/zksync/send-eip712-transaction.ts
index a1241e12b97..83cfc233883 100644
--- a/packages/thirdweb/src/transaction/actions/zksync/send-eip712-transaction.ts
+++ b/packages/thirdweb/src/transaction/actions/zksync/send-eip712-transaction.ts
@@ -173,7 +173,11 @@ export async function getZkGasFees(args: {
resolvePromisedValue(transaction.eip712),
]);
let gasPerPubdata = eip712?.gasPerPubdata;
- if (!gas || !maxFeePerGas || !maxPriorityFeePerGas) {
+ if (
+ gas === undefined ||
+ maxFeePerGas === undefined ||
+ maxPriorityFeePerGas === undefined
+ ) {
const rpc = getRpcClient(transaction);
const params = await formatTransaction({ transaction, from });
const result = (await rpc({
diff --git a/packages/thirdweb/src/transaction/serialize-transaction.test.ts b/packages/thirdweb/src/transaction/serialize-transaction.test.ts
index 4ce29c44c36..4090524e808 100644
--- a/packages/thirdweb/src/transaction/serialize-transaction.test.ts
+++ b/packages/thirdweb/src/transaction/serialize-transaction.test.ts
@@ -1,18 +1,10 @@
-import { assertType, describe, expect, test } from "vitest";
-
-import {
- type TransactionSerializable,
- type TransactionSerializableBase,
- type TransactionSerializableEIP1559,
- type TransactionSerializableEIP2930,
- type TransactionSerializableLegacy,
- type TransactionSerializedEIP1559,
- type TransactionSerializedEIP2930,
- type TransactionSerializedLegacy,
- parseTransaction,
-} from "viem";
-
-import { checksumAddress } from "../utils/address.js";
+import * as ox__Hex from "ox/Hex";
+import * as ox__TransactionEnvelopeEip1559 from "ox/TransactionEnvelopeEip1559";
+import * as ox__TransactionEnvelopeEip2930 from "ox/TransactionEnvelopeEip2930";
+import * as ox__TransactionEnvelopeLegacy from "ox/TransactionEnvelopeLegacy";
+import { describe, expect, test } from "vitest";
+
+import { type Address, checksumAddress } from "../utils/address.js";
import { fromGwei, toWei } from "../utils/units.js";
import { serializeTransaction } from "./serialize-transaction.js";
@@ -24,7 +16,7 @@ const BASE_TRANSACTION = {
to: checksumAddress(TEST_ACCOUNT_B.address),
nonce: 785,
value: toWei("1"),
-} as const satisfies TransactionSerializableBase;
+} as const;
describe.runIf(process.env.TW_SECRET_KEY)("eip1559", () => {
const baseEip1559 = {
@@ -32,21 +24,23 @@ describe.runIf(process.env.TW_SECRET_KEY)("eip1559", () => {
chainId: 1,
maxFeePerGas: fromGwei("2"),
maxPriorityFeePerGas: fromGwei("2"),
- } as const satisfies TransactionSerializableEIP1559;
+ } as const;
test("default", () => {
- const serialized = serializeTransaction({
+ const serialized = serializeTransaction({
transaction: baseEip1559,
});
- assertType(serialized);
expect(serialized).toEqual(
"0x02ef0182031184773594008477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0",
);
- const tx = parseTransaction(serialized);
+ const tx = ox__TransactionEnvelopeEip1559.deserialize(
+ serialized as ox__TransactionEnvelopeEip1559.Serialized,
+ );
// The parsed transaction to address is not guaranteed to be checksummed, but our input address is
expect({ ...tx, to: tx.to ? checksumAddress(tx.to) : undefined }).toEqual({
...baseEip1559,
+ nonce: BigInt(baseEip1559.nonce),
type: "eip1559",
});
});
@@ -58,18 +52,22 @@ describe.runIf(process.env.TW_SECRET_KEY)("eip1559", () => {
} as const;
const serialized = serializeTransaction({ transaction: args });
expect(serialized).toEqual("0x02c90180808080808080c0");
- expect(parseTransaction(serialized)).toEqual(args);
+
+ const tx = ox__TransactionEnvelopeEip1559.deserialize(
+ serialized as ox__TransactionEnvelopeEip1559.Serialized,
+ );
+ expect(tx).toEqual(args);
});
test("default (all zeros)", () => {
const baseEip1559Zero = {
- to: TEST_ACCOUNT_B.address,
+ to: TEST_ACCOUNT_B.address as Address,
nonce: 0,
chainId: 1,
maxFeePerGas: 0n,
maxPriorityFeePerGas: 0n,
value: 0n,
- } satisfies TransactionSerializableEIP1559;
+ };
const serialized = serializeTransaction({
transaction: baseEip1559Zero,
@@ -78,7 +76,9 @@ describe.runIf(process.env.TW_SECRET_KEY)("eip1559", () => {
expect(serialized).toEqual(
"0x02dd01808080809470997970c51812dc3a010c7d01b50e0d17dc79c88080c0",
);
- const tx = parseTransaction(serialized);
+ const tx = ox__TransactionEnvelopeEip1559.deserialize(
+ serialized as ox__TransactionEnvelopeEip1559.Serialized,
+ );
expect({ ...tx, to: tx.to ? checksumAddress(tx.to) : undefined }).toEqual({
chainId: 1,
to: TEST_ACCOUNT_B.address,
@@ -95,7 +95,14 @@ describe.runIf(process.env.TW_SECRET_KEY)("eip1559", () => {
transaction: args,
});
expect(serialized).toEqual("0x02c90180800180808080c0");
- expect(parseTransaction(serialized)).toEqual({ ...args, type: "eip1559" });
+ expect(
+ ox__TransactionEnvelopeEip1559.deserialize(
+ serialized as ox__TransactionEnvelopeEip1559.Serialized,
+ ),
+ ).toEqual({
+ ...args,
+ type: "eip1559",
+ });
});
test("args: gas", () => {
@@ -109,7 +116,11 @@ describe.runIf(process.env.TW_SECRET_KEY)("eip1559", () => {
expect(serialized).toEqual(
"0x02f101820311847735940084773594008252099470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0",
);
- expect(parseTransaction(serialized).gas).toEqual(args.gas);
+ expect(
+ ox__TransactionEnvelopeEip1559.deserialize(
+ serialized as ox__TransactionEnvelopeEip1559.Serialized,
+ ).gas,
+ ).toEqual(args.gas);
});
test("args: accessList", () => {
@@ -121,31 +132,39 @@ describe.runIf(process.env.TW_SECRET_KEY)("eip1559", () => {
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000001",
"0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe",
- ],
+ ] as const,
},
],
- } satisfies TransactionSerializableEIP1559;
- const serialized = serializeTransaction({
+ };
+ const serialized = serializeTransaction({
transaction: args,
});
expect(serialized).toEqual(
"0x02f88b0182031184773594008477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080f85bf859940000000000000000000000000000000000000000f842a00000000000000000000000000000000000000000000000000000000000000001a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe",
);
- expect(parseTransaction(serialized).accessList).toEqual(args.accessList);
+ expect(
+ ox__TransactionEnvelopeEip1559.deserialize(
+ serialized as ox__TransactionEnvelopeEip1559.Serialized,
+ ).accessList,
+ ).toEqual(args.accessList);
});
test("args: data", () => {
const args = {
...baseEip1559,
data: "0x1234",
- } satisfies TransactionSerializableEIP1559;
+ } as const;
const serialized = serializeTransaction({
transaction: args,
});
expect(serialized).toEqual(
"0x02f10182031184773594008477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a7640000821234c0",
);
- expect(parseTransaction(serialized).data).toEqual(args.data);
+ expect(
+ ox__TransactionEnvelopeEip1559.deserialize(
+ serialized as ox__TransactionEnvelopeEip1559.Serialized,
+ ).data,
+ ).toEqual(args.data);
});
test("signed", async () => {
@@ -162,7 +181,11 @@ describe.runIf(process.env.TW_SECRET_KEY)("eip1559", () => {
expect(serialized).toEqual(
"0x02f8720182031184773594008477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c001a0ce18214ff9d06ecaacb61811f9d6dc2be922e8cebddeaf6df0b30d5c498f6d33a05f0487c6dbbf2139f7c705d8054dbb16ecac8ae6256ce2c4c6f2e7ef35b3a496",
);
- expect(parseTransaction(serialized).yParity).toEqual(1);
+ expect(
+ ox__TransactionEnvelopeEip1559.deserialize(
+ serialized as ox__TransactionEnvelopeEip1559.Serialized,
+ ).yParity,
+ ).toEqual(1);
});
test("signature", () => {
@@ -270,19 +293,21 @@ describe("eip2930", () => {
},
],
gasPrice: fromGwei("2"),
- } as const satisfies TransactionSerializableEIP2930;
+ } as const;
test("default", () => {
- const serialized = serializeTransaction({
+ const serialized = serializeTransaction({
transaction: BASE_EIP2930_TRANSACTION,
});
- assertType(serialized);
expect(serialized).toEqual(
"0x01f863018203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080f838f7941234512345123451234512345123451234512345e1a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe",
);
- const tx = parseTransaction(serialized);
+ const tx = ox__TransactionEnvelopeEip2930.deserialize(
+ serialized as ox__TransactionEnvelopeEip2930.Serialized,
+ );
expect({ ...tx, to: tx.to ? checksumAddress(tx.to) : undefined }).toEqual({
...BASE_EIP2930_TRANSACTION,
+ nonce: BigInt(BASE_EIP2930_TRANSACTION.nonce),
type: "eip2930",
});
});
@@ -295,7 +320,7 @@ describe("eip2930", () => {
value: 0n,
gasPrice: 0n,
accessList: [],
- } satisfies TransactionSerializableEIP2930;
+ };
const serialized = serializeTransaction({
transaction: baseEip2930Zero,
@@ -305,7 +330,9 @@ describe("eip2930", () => {
"0x01dc018080809470997970c51812dc3a010c7d01b50e0d17dc79c88080c0",
);
- const tx = parseTransaction(serialized);
+ const tx = ox__TransactionEnvelopeEip2930.deserialize(
+ serialized as ox__TransactionEnvelopeEip2930.Serialized,
+ );
expect({ ...tx, to: tx.to ? checksumAddress(tx.to) : undefined }).toEqual({
chainId: 1,
to: checksumAddress(TEST_ACCOUNT_B.address),
@@ -325,26 +352,34 @@ describe("eip2930", () => {
},
],
gasPrice: fromGwei("2"),
- } satisfies TransactionSerializableEIP2930;
- const serialized = serializeTransaction({
+ } as const;
+ const serialized = serializeTransaction({
transaction: args,
});
expect(serialized).toEqual(
"0x01f8450180847735940080808080f838f7940000000000000000000000000000000000000000e1a00000000000000000000000000000000000000000000000000000000000000001",
);
- expect(parseTransaction(serialized).accessList).toEqual(args.accessList);
+ expect(
+ ox__TransactionEnvelopeEip2930.deserialize(
+ serialized as ox__TransactionEnvelopeEip2930.Serialized,
+ ).accessList,
+ ).toEqual(args.accessList);
});
test("minimal (w/ type)", () => {
const args = {
chainId: 1,
type: "eip2930",
- } satisfies TransactionSerializableEIP2930;
+ };
const serialized = serializeTransaction({
transaction: args,
});
expect(serialized).toEqual("0x01c801808080808080c0");
- expect(parseTransaction(serialized).type).toEqual("eip2930");
+ expect(
+ ox__TransactionEnvelopeEip2930.deserialize(
+ serialized as ox__TransactionEnvelopeEip2930.Serialized,
+ ).type,
+ ).toEqual("eip2930");
});
test("args: gas", () => {
@@ -356,19 +391,27 @@ describe("eip2930", () => {
expect(serialized).toEqual(
"0x01f8650182031184773594008252099470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080f838f7941234512345123451234512345123451234512345e1a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe",
);
- expect(parseTransaction(serialized).gas).toEqual(args.gas);
+ expect(
+ ox__TransactionEnvelopeEip2930.deserialize(
+ serialized as ox__TransactionEnvelopeEip2930.Serialized,
+ ).gas,
+ ).toEqual(args.gas);
});
test("args: data", () => {
const args = {
...BASE_EIP2930_TRANSACTION,
data: "0x1234",
- } satisfies TransactionSerializableEIP2930;
+ } as const;
const serialized = serializeTransaction({ transaction: args });
expect(serialized).toEqual(
"0x01f865018203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a7640000821234f838f7941234512345123451234512345123451234512345e1a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe",
);
- expect(parseTransaction(serialized).data).toEqual(args.data);
+ expect(
+ ox__TransactionEnvelopeEip2930.deserialize(
+ serialized as ox__TransactionEnvelopeEip2930.Serialized,
+ ).data,
+ ).toEqual(args.data);
});
test("signed", async () => {
@@ -384,10 +427,14 @@ describe("eip2930", () => {
expect(serialized).toEqual(
"0x01f8a6018203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080f838f7941234512345123451234512345123451234512345e1a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe01a0dc7b3483c0b183823f07d77247c60678d861080acdc4fb8b9fd131770b475c40a040f16567391132746735aff4d5a3fa5ae42ff3d5d538e341870e0259dc40741a",
);
- const tx = parseTransaction(serialized);
+ const tx = ox__TransactionEnvelopeEip2930.deserialize(
+ serialized as ox__TransactionEnvelopeEip2930.Serialized,
+ );
expect({ ...tx, to: tx.to ? checksumAddress(tx.to) : undefined }).toEqual({
...BASE_EIP2930_TRANSACTION,
- ...signature,
+ nonce: BigInt(BASE_EIP2930_TRANSACTION.nonce),
+ r: ox__Hex.toBigInt(signature.r),
+ s: ox__Hex.toBigInt(signature.s),
type: "eip2930",
yParity: 1,
});
@@ -490,16 +537,19 @@ describe("legacy", () => {
};
test("default", () => {
- const serialized = serializeTransaction({
+ const serialized = serializeTransaction({
transaction: BASE_LEGACY_TRANSACTION,
});
- assertType(serialized);
+
expect(serialized).toEqual(
"0xe88203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080",
);
- const tx = parseTransaction(serialized);
+ const tx = ox__TransactionEnvelopeLegacy.deserialize(
+ serialized as ox__TransactionEnvelopeLegacy.Serialized,
+ );
expect({ ...tx, to: tx.to ? checksumAddress(tx.to) : undefined }).toEqual({
...BASE_LEGACY_TRANSACTION,
+ nonce: BigInt(BASE_LEGACY_TRANSACTION.nonce),
type: "legacy",
});
});
@@ -510,7 +560,7 @@ describe("legacy", () => {
nonce: 0,
value: 0n,
gasPrice: 0n,
- } satisfies TransactionSerializableLegacy;
+ };
const serialized = serializeTransaction({
transaction: baseLegacyZero,
@@ -520,7 +570,9 @@ describe("legacy", () => {
"0xda8080809470997970c51812dc3a010c7d01b50e0d17dc79c88080",
);
- const tx = parseTransaction(serialized);
+ const tx = ox__TransactionEnvelopeLegacy.deserialize(
+ serialized as ox__TransactionEnvelopeLegacy.Serialized,
+ );
expect({ ...tx, to: tx.to ? checksumAddress(tx.to) : undefined }).toEqual({
to: checksumAddress(TEST_ACCOUNT_B.address),
type: "legacy",
@@ -530,23 +582,27 @@ describe("legacy", () => {
test("minimal (w/ gasPrice)", () => {
const args = {
gasPrice: fromGwei("2"),
- } satisfies TransactionSerializableLegacy;
+ };
const serialized = serializeTransaction({
transaction: args,
});
expect(serialized).toEqual("0xca80847735940080808080");
- expect(parseTransaction(serialized).gasPrice).toEqual(args.gasPrice);
+ expect(
+ ox__TransactionEnvelopeLegacy.deserialize(serialized).gasPrice,
+ ).toEqual(args.gasPrice);
});
test("minimal (w/ type)", () => {
const args = {
type: "legacy",
- } satisfies TransactionSerializableLegacy;
+ };
const serialized = serializeTransaction({
transaction: args,
});
expect(serialized).toEqual("0xc6808080808080");
- expect(parseTransaction(serialized).type).toEqual(args.type);
+ expect(ox__TransactionEnvelopeLegacy.deserialize(serialized).type).toEqual(
+ args.type,
+ );
});
test("args: gas", () => {
@@ -560,33 +616,39 @@ describe("legacy", () => {
expect(serialized).toEqual(
"0xea82031184773594008252099470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080",
);
- expect(parseTransaction(serialized).gas).toEqual(args.gas);
+ expect(ox__TransactionEnvelopeLegacy.deserialize(serialized).gas).toEqual(
+ args.gas,
+ );
});
test("args: data", () => {
const args = {
...BASE_LEGACY_TRANSACTION,
data: "0x1234",
- } satisfies TransactionSerializableLegacy;
+ } as const;
const serialized = serializeTransaction({
transaction: args,
});
expect(serialized).toEqual(
"0xea8203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a7640000821234",
);
- expect(parseTransaction(serialized).data).toEqual(args.data);
+ expect(ox__TransactionEnvelopeLegacy.deserialize(serialized).data).toEqual(
+ args.data,
+ );
});
test("args: chainId", () => {
const args = {
...BASE_LEGACY_TRANSACTION,
chainId: 69,
- } satisfies TransactionSerializableLegacy;
+ } as const;
const serialized = serializeTransaction({ transaction: args });
expect(serialized).toEqual(
"0xeb8203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080458080",
);
- expect(parseTransaction(serialized).chainId).toEqual(args.chainId);
+ expect(
+ ox__TransactionEnvelopeLegacy.deserialize(serialized).chainId,
+ ).toEqual(args.chainId);
});
test("signed", async () => {
@@ -602,10 +664,13 @@ describe("legacy", () => {
expect(serialized).toEqual(
"0xf86b8203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a7640000801ca06cb0e8d21e5baf998fb9a05f47acd83692dc148f90b81b332a152f020da0ae98a0344e49bacb1ef7af7c2ffed9e88d3f0ae0aa4945c9da0a660a03717dd5621f98",
);
- const tx = parseTransaction(serialized);
+ const tx = ox__TransactionEnvelopeLegacy.deserialize(serialized);
expect({ ...tx, to: tx.to ? checksumAddress(tx.to) : undefined }).toEqual({
...BASE_LEGACY_TRANSACTION,
- ...signature,
+ nonce: BigInt(BASE_LEGACY_TRANSACTION.nonce),
+ r: ox__Hex.toBigInt(signature.r),
+ s: ox__Hex.toBigInt(signature.s),
+ v: 28,
yParity: 1,
type: "legacy",
});
@@ -653,13 +718,15 @@ describe("legacy", () => {
expect(serialized).toEqual(
"0xf86c8203118477359400809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a76400008081ada02f43314322cf4c5dd645b028aa0b0dadff0fb73c41a6f0620ff1dfb11601ac30a066f37a65e139fa4b6df33a42ab5ccaeaa7a109382e7430caefd1deee63962626",
);
- const tx = parseTransaction(serialized);
+ const tx = ox__TransactionEnvelopeLegacy.deserialize(serialized);
expect({ ...tx, to: tx.to ? checksumAddress(tx.to) : undefined }).toEqual({
...args,
- ...signature,
+ nonce: BigInt(args.nonce),
+ r: ox__Hex.toBigInt(signature.r),
+ s: ox__Hex.toBigInt(signature.s),
yParity: 0,
type: "legacy",
- v: 173n,
+ v: 173,
});
});
@@ -686,164 +753,3 @@ test("cannot infer type from transaction object", () => {
}),
).toThrow();
});
-
-describe("miscellaneous", () => {
- test("https://github.com/wevm/viem/issues/1433", () => {
- expect(
- serializeTransaction({
- transaction: {
- blockHash: null,
- blockNumber: null,
- from: "0xc702b9950e44f7d321fa16ee88bf8e1a561249ba",
- gas: 51627n,
- gasPrice: 3000000000n,
- hash: "0x3eaa88a766e82cbe53c95218ab4c3cf316325802b5f75d086b5121007b918e92",
- input:
- "0xa9059cbb00000000000000000000000082fc882199816bcec06baf848eaec51c2f96c85b000000000000000000000000000000000000000000000000eccae3078bacd15d",
- nonce: 117,
- to: "0x55d398326f99059ff775485246999027b3197955",
- transactionIndex: null,
- value: 0n,
- type: "legacy",
- v: 84475n,
- r: "0x73b39769ff4a36515c8fca546550a3fdafebbf37fa9e22be2d92b44653ade7bf",
- s: "0x354c756a1aa3346e9b3ea5423ac99acfc005e9cce2cd698e14d792f43fa15a23",
- chainId: undefined,
- typeHex: "0x0",
- } as TransactionSerializable,
- }),
- ).toMatchInlineSnapshot(
- '"0xf8667584b2d05e0082c9ab9455d398326f99059ff775485246999027b31979558080830149fba073b39769ff4a36515c8fca546550a3fdafebbf37fa9e22be2d92b44653ade7bfa0354c756a1aa3346e9b3ea5423ac99acfc005e9cce2cd698e14d792f43fa15a23"',
- );
-
- expect(
- serializeTransaction({
- transaction: {
- blockHash: null,
- blockNumber: null,
- from: "0xc702b9950e44f7d321fa16ee88bf8e1a561249ba",
- gas: 51627n,
- gasPrice: 3000000000n,
- hash: "0x3eaa88a766e82cbe53c95218ab4c3cf316325802b5f75d086b5121007b918e92",
- input:
- "0xa9059cbb00000000000000000000000082fc882199816bcec06baf848eaec51c2f96c85b000000000000000000000000000000000000000000000000eccae3078bacd15d",
- nonce: 117,
- to: "0x55d398326f99059ff775485246999027b3197955",
- transactionIndex: null,
- value: 0n,
- type: "legacy",
- v: 84476n,
- r: "0x73b39769ff4a36515c8fca546550a3fdafebbf37fa9e22be2d92b44653ade7bf",
- s: "0x354c756a1aa3346e9b3ea5423ac99acfc005e9cce2cd698e14d792f43fa15a23",
- chainId: undefined,
- typeHex: "0x0",
- } as TransactionSerializable,
- }),
- ).toMatchInlineSnapshot(
- '"0xf8667584b2d05e0082c9ab9455d398326f99059ff775485246999027b31979558080830149fca073b39769ff4a36515c8fca546550a3fdafebbf37fa9e22be2d92b44653ade7bfa0354c756a1aa3346e9b3ea5423ac99acfc005e9cce2cd698e14d792f43fa15a23"',
- );
-
- expect(
- serializeTransaction({
- transaction: {
- blockHash: null,
- blockNumber: null,
- from: "0xc702b9950e44f7d321fa16ee88bf8e1a561249ba",
- gas: 51627n,
- gasPrice: 3000000000n,
- hash: "0x3eaa88a766e82cbe53c95218ab4c3cf316325802b5f75d086b5121007b918e92",
- input:
- "0xa9059cbb00000000000000000000000082fc882199816bcec06baf848eaec51c2f96c85b000000000000000000000000000000000000000000000000eccae3078bacd15d",
- nonce: 117,
- to: "0x55d398326f99059ff775485246999027b3197955",
- transactionIndex: null,
- value: 0n,
- type: "legacy",
- v: 35n,
- r: "0x73b39769ff4a36515c8fca546550a3fdafebbf37fa9e22be2d92b44653ade7bf",
- s: "0x354c756a1aa3346e9b3ea5423ac99acfc005e9cce2cd698e14d792f43fa15a23",
- chainId: undefined,
- typeHex: "0x0",
- } as TransactionSerializable,
- }),
- ).toMatchInlineSnapshot(
- '"0xf8637584b2d05e0082c9ab9455d398326f99059ff775485246999027b319795580801ba073b39769ff4a36515c8fca546550a3fdafebbf37fa9e22be2d92b44653ade7bfa0354c756a1aa3346e9b3ea5423ac99acfc005e9cce2cd698e14d792f43fa15a23"',
- );
-
- expect(
- serializeTransaction({
- transaction: {
- blockHash: null,
- blockNumber: null,
- from: "0xc702b9950e44f7d321fa16ee88bf8e1a561249ba",
- gas: 51627n,
- gasPrice: 3000000000n,
- hash: "0x3eaa88a766e82cbe53c95218ab4c3cf316325802b5f75d086b5121007b918e92",
- input:
- "0xa9059cbb00000000000000000000000082fc882199816bcec06baf848eaec51c2f96c85b000000000000000000000000000000000000000000000000eccae3078bacd15d",
- nonce: 117,
- to: "0x55d398326f99059ff775485246999027b3197955",
- transactionIndex: null,
- value: 0n,
- type: "legacy",
- v: 36n,
- r: "0x73b39769ff4a36515c8fca546550a3fdafebbf37fa9e22be2d92b44653ade7bf",
- s: "0x354c756a1aa3346e9b3ea5423ac99acfc005e9cce2cd698e14d792f43fa15a23",
- chainId: undefined,
- typeHex: "0x0",
- } as TransactionSerializable,
- }),
- ).toMatchInlineSnapshot(
- '"0xf8637584b2d05e0082c9ab9455d398326f99059ff775485246999027b319795580801ca073b39769ff4a36515c8fca546550a3fdafebbf37fa9e22be2d92b44653ade7bfa0354c756a1aa3346e9b3ea5423ac99acfc005e9cce2cd698e14d792f43fa15a23"',
- );
- });
-
- test("https://github.com/wevm/viem/issues/1849", async () => {
- const tx = {
- blockHash:
- "0xbfafd5da42c7e715149a1fbcc20c40bcf5f62e013f60af9cdf07c27142b6a0ff",
- blockNumber: 19295009n,
- gas: 421374n,
- gasPrice: 20701311233n,
- input:
- "0x3a2b7b980000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000726000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000065d96f1b00000000000000000000000000000000000000000000000000000000000000030b010c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000c23190af48df1c00000000000000000000000000000000000000000000000000000000000001000000000000000000000000002648f5592c09a260c601acde44e7f8f2944944fb0000000000000000000000000000000000000000000000000f43fc2c04ee000000000000000000000000000000000000000000000000000000c23190af48df1c00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bbe33f57f41a20b2f00dec91dcc1169597f36221f002710c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000023a03a3f85888a471f4effcafa03431511e388560000000000000000000000000000000000000000000000000000000000000000",
- nonce: 38,
- to: "0x2648f5592c09a260c601acde44e7f8f2944944fb",
- transactionIndex: 74,
- value: 57108134443873424n,
- type: "legacy",
- chainId: 1,
- v: 38n,
- r: "0x5abc283bf902f90884f800b2f810febd5e90f8d5077d89e150631bbcc547b7d1",
- s: "0x5acc7718573bcd268256c996f2ae1bdd2bd97019992f44d5806b1cc843cde2e7",
- typeHex: "0x0",
- } as const;
-
- const serialized = serializeTransaction({
- transaction: { ...tx, data: tx.input },
- });
-
- expect(keccak256(serialized)).toEqual(
- "0x6ed21df69b02678dfb290ef2a43d490303562eb387f70795766b37bfa9d09bd2",
- );
- });
-
- test("Successfully handles hex as bigint", () => {
- const transaction = {
- to: "0x0000000000000000000000000000000000000000",
- chainId: 31337,
- data: "0x",
- gas: 21001n,
- nonce: 0,
- accessList: undefined,
- value: "0x0",
- maxFeePerGas: 3100000000n,
- maxPriorityFeePerGas: 1100000000n,
- } as unknown as TransactionSerializable;
-
- const signed = serializeTransaction({ transaction });
- expect(signed).toMatchInlineSnapshot(
- `"0x02ec827a6980844190ab0084b8c63f008252099400000000000000000000000000000000000000008330783080c0"`,
- );
- });
-});
diff --git a/packages/thirdweb/src/transaction/serialize-transaction.ts b/packages/thirdweb/src/transaction/serialize-transaction.ts
index 3f596440af3..93a2e17b2a0 100644
--- a/packages/thirdweb/src/transaction/serialize-transaction.ts
+++ b/packages/thirdweb/src/transaction/serialize-transaction.ts
@@ -1,15 +1,37 @@
-import {
- type GetTransactionType,
- type SerializedTransactionReturnType,
- type Signature,
- type TransactionSerializable,
- type TransactionType,
- serializeTransaction as _serializeTransaction,
-} from "viem";
+import * as ox__Hex from "ox/Hex";
+import * as ox__Signature from "ox/Signature";
+import * as ox__TransactionEnvelopeEip1559 from "ox/TransactionEnvelopeEip1559";
+import * as ox__TransactionEnvelopeEip2930 from "ox/TransactionEnvelopeEip2930";
+import * as ox__TransactionEnvelopeLegacy from "ox/TransactionEnvelopeLegacy";
+import type { Hex } from "../utils/encoding/hex.js";
+
+export type SerializableTransaction = {
+ type?: string | undefined;
+ r?: Hex | bigint;
+ s?: Hex | bigint;
+ v?: bigint | number;
+ yParity?: bigint | number;
+ accessList?:
+ | ox__TransactionEnvelopeEip2930.TransactionEnvelopeEip2930["accessList"]
+ | undefined;
+ chainId?: number | undefined;
+ gasPrice?: bigint | undefined;
+ maxFeePerGas?: bigint | undefined;
+ maxPriorityFeePerGas?: bigint | undefined;
+ data?: Hex | undefined;
+ to?: string | null | undefined; // Must allow null for backwards compatibility
+ nonce?: number | bigint | undefined;
+ value?: bigint | undefined;
+ gas?: bigint | undefined;
+ gasLimit?: bigint | undefined;
+};
export type SerializeTransactionOptions = {
- transaction: TransactionSerializable;
- signature?: Signature | undefined;
+ transaction: SerializableTransaction;
+ signature?:
+ | ox__Signature.Signature
+ | ox__Signature.Legacy
+ | undefined;
};
/**
@@ -32,18 +54,39 @@ export type SerializeTransactionOptions = {
* });
* ```
*/
-export function serializeTransaction<
- const transaction extends TransactionSerializable,
- _transactionType extends TransactionType = GetTransactionType,
->(
+export function serializeTransaction(
options: SerializeTransactionOptions,
-): SerializedTransactionReturnType {
+): Hex {
const { transaction } = options;
+ const type = getTransactionEnvelopeType(transaction);
+
// This is to maintain compatibility with our old interface (including the signature in the transaction object)
const signature = (() => {
- if (options.signature) return options.signature;
- if (transaction.v === undefined && transaction.yParity === undefined) {
+ if (options.signature) {
+ if (
+ "v" in options.signature &&
+ typeof options.signature.v !== "undefined"
+ ) {
+ return ox__Signature.fromLegacy({
+ r: ox__Hex.toBigInt(options.signature.r),
+ s: ox__Hex.toBigInt(options.signature.s),
+ v: Number(options.signature.v),
+ });
+ }
+
+ return {
+ r: ox__Hex.toBigInt(options.signature.r),
+ s: ox__Hex.toBigInt(options.signature.s),
+ // We force the Signature type here because we filter for legacy type above
+ yParity: (options.signature as unknown as ox__Signature.Signature)
+ .yParity,
+ };
+ }
+ if (
+ typeof transaction.v === "undefined" &&
+ typeof transaction.yParity === "undefined"
+ ) {
return undefined;
}
@@ -52,12 +95,78 @@ export function serializeTransaction<
}
return {
- v: transaction.v,
- r: transaction.r,
- s: transaction.s,
- yParity: transaction.yParity,
+ r:
+ typeof transaction.r === "bigint"
+ ? transaction.r
+ : ox__Hex.toBigInt(transaction.r),
+ s:
+ typeof transaction.s === "bigint"
+ ? transaction.s
+ : ox__Hex.toBigInt(transaction.s),
+ yParity:
+ typeof transaction.v !== "undefined" &&
+ typeof transaction.yParity === "undefined"
+ ? ox__Signature.vToYParity(Number(transaction.v))
+ : Number(transaction.yParity),
};
})();
- return _serializeTransaction(transaction, signature as Signature | undefined); // Trust the options type-checking did its job and that the converted signature mirrors that type
+ if (type === "eip1559") {
+ const typedTransaction =
+ transaction as ox__TransactionEnvelopeEip1559.TransactionEnvelopeEip1559;
+ ox__TransactionEnvelopeEip1559.assert(typedTransaction);
+
+ return ox__TransactionEnvelopeEip1559.serialize(typedTransaction, {
+ signature,
+ });
+ }
+
+ if (type === "legacy") {
+ const typedTransaction =
+ transaction as ox__TransactionEnvelopeLegacy.TransactionEnvelopeLegacy;
+ ox__TransactionEnvelopeLegacy.assert(typedTransaction);
+
+ return ox__TransactionEnvelopeLegacy.serialize(typedTransaction, {
+ signature,
+ });
+ }
+
+ if (type === "eip2930") {
+ const typedTransaction =
+ transaction as ox__TransactionEnvelopeEip2930.TransactionEnvelopeEip2930;
+ ox__TransactionEnvelopeEip2930.assert(typedTransaction);
+
+ return ox__TransactionEnvelopeEip2930.serialize(typedTransaction, {
+ signature,
+ });
+ }
+
+ throw new Error("Invalid transaction type");
+}
+
+/**
+ * @internal
+ */
+function getTransactionEnvelopeType(
+ transactionEnvelope: SerializableTransaction,
+) {
+ if (typeof transactionEnvelope.type !== "undefined") {
+ return transactionEnvelope.type;
+ }
+
+ if (
+ typeof transactionEnvelope.maxFeePerGas !== "undefined" ||
+ typeof transactionEnvelope.maxPriorityFeePerGas !== "undefined"
+ ) {
+ return "eip1559";
+ }
+
+ if (typeof transactionEnvelope.gasPrice !== "undefined") {
+ if (typeof transactionEnvelope.accessList !== "undefined") {
+ return "eip2930";
+ }
+ return "legacy";
+ }
+
+ throw new Error("Invalid transaction type");
}
diff --git a/packages/thirdweb/src/transaction/utils.ts b/packages/thirdweb/src/transaction/utils.ts
index ca52bd5edd8..de8df87c4cd 100644
--- a/packages/thirdweb/src/transaction/utils.ts
+++ b/packages/thirdweb/src/transaction/utils.ts
@@ -1,4 +1,7 @@
import type { AbiFunction } from "abitype";
+import { getGasPrice } from "../gas/get-gas-price.js";
+import { estimateGasCost } from "./actions/estimate-gas-cost.js";
+import type { PreparedTransaction } from "./prepare-transaction.js";
/**
* @internal
@@ -11,3 +14,33 @@ export function isAbiFunction(item: unknown): item is AbiFunction {
item.type === "function"
);
}
+
+export async function getTransactionGasCost(
+ tx: PreparedTransaction,
+ from?: string,
+) {
+ try {
+ const gasCost = await estimateGasCost({
+ transaction: tx,
+ from,
+ });
+
+ const bufferCost = gasCost.wei / 10n;
+
+ // Note: get tx.value AFTER estimateGasCost
+ // add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
+ return gasCost.wei + bufferCost;
+ } catch {
+ if (from) {
+ // try again without passing from
+ return await getTransactionGasCost(tx);
+ }
+ // fallback if both fail, use the tx value + 1M * gas price
+ const gasPrice = await getGasPrice({
+ client: tx.client,
+ chain: tx.chain,
+ });
+
+ return 1_000_000n * gasPrice;
+ }
+}
diff --git a/packages/thirdweb/src/utils/any-evm/keyless-transaction.test.ts b/packages/thirdweb/src/utils/any-evm/keyless-transaction.test.ts
new file mode 100644
index 00000000000..c2fbf759196
--- /dev/null
+++ b/packages/thirdweb/src/utils/any-evm/keyless-transaction.test.ts
@@ -0,0 +1,91 @@
+import * as ox__Hash from "ox/Hash";
+import * as ox__Hex from "ox/Hex";
+import * as ox__Signature from "ox/Signature";
+import { recoverAddress } from "viem";
+import { describe, expect, it } from "vitest";
+import { serializeTransaction } from "../../transaction/serialize-transaction.js";
+import { getKeylessTransaction } from "./keyless-transaction.js";
+
+describe("getKeylessTransaction", () => {
+ const mockTransaction = {
+ to: "0x1234567890123456789012345678901234567890",
+ value: 1000n,
+ chainId: 1,
+ gasPrice: 10n,
+ };
+
+ const mockSignature = {
+ r: "0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe",
+ s: "0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe",
+ v: 27n,
+ } as const;
+
+ it("should return the correct signer address and serialized transaction", async () => {
+ const serializedTransaction = serializeTransaction({
+ transaction: mockTransaction,
+ });
+
+ const hash = ox__Hash.keccak256(serializedTransaction);
+ const expectedAddress = await recoverAddress({
+ hash,
+ signature: ox__Signature.toHex({
+ r: ox__Hex.toBigInt(mockSignature.r),
+ s: ox__Hex.toBigInt(mockSignature.s),
+ yParity: ox__Signature.vToYParity(Number(mockSignature.v)),
+ }),
+ });
+
+ const result = await getKeylessTransaction({
+ transaction: mockTransaction,
+ signature: mockSignature,
+ });
+
+ expect(result.signerAddress).toBe(expectedAddress);
+ expect(result.transaction).toBe(
+ serializeTransaction({
+ transaction: mockTransaction,
+ signature: mockSignature,
+ }),
+ );
+ });
+
+ it("should throw if yParity is explicitly undefined", async () => {
+ const invalidSignature = {
+ r: mockSignature.r,
+ s: mockSignature.s,
+ yParity: undefined,
+ };
+
+ await expect(
+ getKeylessTransaction({
+ transaction: mockTransaction,
+ // biome-ignore lint/suspicious/noExplicitAny: Testing invalid data
+ signature: invalidSignature as any,
+ }),
+ ).rejects.toThrow();
+ });
+
+ it("should throw if a signature is not recoverable", async () => {
+ const invalidSignature = { ...mockSignature, v: undefined };
+
+ await expect(
+ getKeylessTransaction({
+ transaction: mockTransaction,
+ // biome-ignore lint/suspicious/noExplicitAny: Testing invalid data
+ signature: invalidSignature as any,
+ }),
+ ).rejects.toThrow();
+ });
+
+ it("should throw an error if the transaction is invalid", async () => {
+ const invalidTransaction = { ...mockTransaction, value: "invalid" };
+
+ await expect(
+ getKeylessTransaction({
+ // biome-ignore lint/suspicious/noExplicitAny: Testing invalid data
+ transaction: invalidTransaction as any,
+ signature: mockSignature,
+ }),
+ ).rejects.toThrow();
+ });
+});
diff --git a/packages/thirdweb/src/utils/any-evm/keyless-transaction.ts b/packages/thirdweb/src/utils/any-evm/keyless-transaction.ts
index fb94a48650b..717adbfd13f 100644
--- a/packages/thirdweb/src/utils/any-evm/keyless-transaction.ts
+++ b/packages/thirdweb/src/utils/any-evm/keyless-transaction.ts
@@ -1,15 +1,18 @@
+import * as ox__Hash from "ox/Hash";
+import * as ox__Hex from "ox/Hex";
+import * as ox__Signature from "ox/Signature";
+import { recoverAddress } from "viem";
import {
- type Signature,
- type TransactionSerializable,
- recoverAddress,
+ type SerializableTransaction,
serializeTransaction,
- signatureToHex,
-} from "viem";
-import { keccak256 } from "../hashing/keccak256.js";
+} from "../../transaction/serialize-transaction.js";
+import type { Hex } from "../encoding/hex.js";
type GetKeylessTransactionOptions = {
- transaction: TransactionSerializable;
- signature: Signature;
+ transaction: SerializableTransaction;
+ signature:
+ | ox__Signature.Signature
+ | ox__Signature.Legacy;
};
/**
@@ -23,20 +26,46 @@ export async function getKeylessTransaction(
options: GetKeylessTransactionOptions,
) {
// 1. Create serialized txn string
- const hash = keccak256(serializeTransaction(options.transaction));
+ const hash = ox__Hash.keccak256(
+ serializeTransaction({ transaction: options.transaction }),
+ );
+
+ const yParity = (() => {
+ if (
+ "yParity" in options.signature &&
+ typeof options.signature.yParity !== "undefined"
+ ) {
+ return options.signature.yParity;
+ }
+
+ if (
+ "v" in options.signature &&
+ typeof options.signature.v !== "undefined"
+ ) {
+ return ox__Signature.vToYParity(Number(options.signature.v));
+ }
+
+ throw new Error(
+ "Invalid recovered signature provided with transaction, missing v or yParity",
+ );
+ })();
// 2. Determine signer address from custom signature + txn
const address = await recoverAddress({
hash,
- signature: signatureToHex(options.signature),
+ signature: ox__Signature.toHex({
+ r: ox__Hex.toBigInt(options.signature.r),
+ s: ox__Hex.toBigInt(options.signature.s),
+ yParity,
+ }),
});
// 3. Create the signed serialized txn string.
// To be sent directly to the chain using a provider.
- const transaction = serializeTransaction(
- options.transaction,
- options.signature,
- );
+ const transaction = serializeTransaction({
+ transaction: options.transaction,
+ signature: options.signature,
+ });
return {
signerAddress: address,
diff --git a/packages/thirdweb/src/wallets/interfaces/wallet.ts b/packages/thirdweb/src/wallets/interfaces/wallet.ts
index 76d17e30be9..a5ac2b8bf11 100644
--- a/packages/thirdweb/src/wallets/interfaces/wallet.ts
+++ b/packages/thirdweb/src/wallets/interfaces/wallet.ts
@@ -1,11 +1,12 @@
import type { Address } from "abitype";
import type * as ox__TypedData from "ox/TypedData";
-import type { Hex, SignableMessage, TransactionSerializable } from "viem";
+import type { Hex, SignableMessage } from "viem";
import type { Chain } from "../../chains/types.js";
import type {
EIP712TransactionOptions,
PreparedTransaction,
} from "../../transaction/prepare-transaction.js";
+import type { SerializableTransaction } from "../../transaction/serialize-transaction.js";
import type { SendTransactionResult } from "../../transaction/types.js";
import type { WalletEmitter } from "../wallet-emitter.js";
import type {
@@ -15,7 +16,7 @@ import type {
WalletId,
} from "../wallet-types.js";
-export type SendTransactionOption = TransactionSerializable & {
+export type SendTransactionOption = SerializableTransaction & {
chainId: number;
eip712?: EIP712TransactionOptions;
};
@@ -217,7 +218,7 @@ export type Account = {
* }
* ```
*/
- signTransaction?: (tx: TransactionSerializable) => Promise;
+ signTransaction?: (tx: SerializableTransaction) => Promise;
/**
* Send the given array of transactions to the blockchain in a single batch
*
diff --git a/packages/thirdweb/src/wallets/private-key.ts b/packages/thirdweb/src/wallets/private-key.ts
index 101c7326150..05e0ecc6e61 100644
--- a/packages/thirdweb/src/wallets/private-key.ts
+++ b/packages/thirdweb/src/wallets/private-key.ts
@@ -1,15 +1,16 @@
import { secp256k1 } from "@noble/curves/secp256k1";
import type * as ox__TypedData from "ox/TypedData";
-import type { SignableMessage, TransactionSerializable } from "viem";
import { publicKeyToAddress } from "viem/utils";
import { getCachedChain } from "../chains/utils.js";
import type { ThirdwebClient } from "../client/client.js";
import { eth_sendRawTransaction } from "../rpc/actions/eth_sendRawTransaction.js";
import { getRpcClient } from "../rpc/rpc.js";
import { signTransaction } from "../transaction/actions/sign-transaction.js";
+import type { SerializableTransaction } from "../transaction/serialize-transaction.js";
import { type Hex, toHex } from "../utils/encoding/hex.js";
import { signMessage } from "../utils/signatures/sign-message.js";
import { signTypedData } from "../utils/signatures/sign-typed-data.js";
+import type { Prettify } from "../utils/type-utils.js";
import type { Account } from "./interfaces/wallet.js";
export type PrivateKeyToAccountOptions = {
@@ -42,6 +43,13 @@ export type PrivateKeyToAccountOptions = {
privateKey: string;
};
+type Message = Prettify<
+ | string
+ | {
+ raw: Hex | Uint8Array;
+ }
+>;
+
/**
* Get an `Account` object from a private key.
* @param options - The options for `privateKeyToAccount`
@@ -70,7 +78,7 @@ export function privateKeyToAccount(
const account = {
address,
sendTransaction: async (
- tx: TransactionSerializable & { chainId: number },
+ tx: SerializableTransaction & { chainId: number },
) => {
const rpcRequest = getRpcClient({
client: client,
@@ -88,7 +96,7 @@ export function privateKeyToAccount(
transactionHash,
};
},
- signMessage: async ({ message }: { message: SignableMessage }) => {
+ signMessage: async ({ message }: { message: Message }) => {
return signMessage({
message,
privateKey,
@@ -105,7 +113,7 @@ export function privateKeyToAccount(
privateKey,
});
},
- signTransaction: async (tx: TransactionSerializable) => {
+ signTransaction: async (tx: SerializableTransaction) => {
return signTransaction({
transaction: tx,
privateKey,
diff --git a/packages/thirdweb/src/wallets/smart/lib/bundler.ts b/packages/thirdweb/src/wallets/smart/lib/bundler.ts
index a60f0bf895e..ff5e045c0ce 100644
--- a/packages/thirdweb/src/wallets/smart/lib/bundler.ts
+++ b/packages/thirdweb/src/wallets/smart/lib/bundler.ts
@@ -1,7 +1,8 @@
-import { type TransactionSerializable, decodeErrorResult } from "viem";
+import { decodeErrorResult } from "viem";
import { parseEventLogs } from "../../../event/actions/parse-logs.js";
import { userOperationRevertReasonEvent } from "../../../extensions/erc4337/__generated__/IEntryPoint/events/UserOperationRevertReason.js";
import { postOpRevertReasonEvent } from "../../../extensions/erc4337/__generated__/IEntryPoint_v07/events/PostOpRevertReason.js";
+import type { SerializableTransaction } from "../../../transaction/serialize-transaction.js";
import type { TransactionReceipt } from "../../../transaction/types.js";
import { type Hex, hexToBigInt } from "../../../utils/encoding/hex.js";
import { getClientFetch } from "../../../utils/fetch.js";
@@ -217,7 +218,7 @@ export async function getUserOpReceiptRaw(
*/
export async function getZkPaymasterData(args: {
options: BundlerOptions;
- transaction: TransactionSerializable;
+ transaction: SerializableTransaction;
}): Promise {
const res = await sendBundlerRequest({
options: args.options,
@@ -233,7 +234,7 @@ export async function getZkPaymasterData(args: {
export async function broadcastZkTransaction(args: {
options: BundlerOptions;
- transaction: TransactionSerializable;
+ transaction: SerializableTransaction;
signedTransaction: Hex;
}): Promise<{ transactionHash: Hex }> {
const res = await sendBundlerRequest({