diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCardUI.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCardUI.tsx index 1d8d70ffc04..27c9026f3af 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCardUI.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCardUI.tsx @@ -68,7 +68,7 @@ export function EngineCloudBarChartCardUI({ if (data.length === 0 || isAllEmpty) { return ( ); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/layout.tsx index e7967088f5e..0fe85ba945f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/layout.tsx @@ -104,7 +104,7 @@ function EngineLegacyBannerUI(props: { } + + ); +} + +function Example() { + return ( + + + + ); +} +``` + +## Backend: Call the transactions API + +`POST /api/claim` calls the transactions API to send tokens to the user's wallet. + +```tsx +export async function POST(request: Request) { + const { userWalletAddress } = await request.json(); + + await fetch( + "https://engine.thirdweb.com/v1/contract/write", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-secret-key": "", + }, + body: JSON.stringify({ + executionOptions: { + from: "", + chainId: "", + }, + params: [ + { + contractAddress: "", + method: "function transfer(address to, uint256 amount)", + params: [userWalletAddress, 1000000000000000000], // 1 token + }, + ], + }), + }, + ); + + return NextResponse.json({ message: "Success!" }); +} +``` + +## Try it out! + +Here’s what the user flow looks like. + +The app prompts the user to connect their wallet. + + + + + +A user presses claim. + + + +They'll receive the tokens in their wallet shortly! + + + diff --git a/apps/portal/src/app/transactions/layout.tsx b/apps/portal/src/app/transactions/layout.tsx new file mode 100644 index 00000000000..c6846187293 --- /dev/null +++ b/apps/portal/src/app/transactions/layout.tsx @@ -0,0 +1,20 @@ +import { DocLayout } from "@/components/Layouts/DocLayout"; +import { sidebar } from "./sidebar"; + +export default async function Layout(props: { children: React.ReactNode }) { + return ( + +

+ Transactions +

+ + } + > + {props.children} +
+ ); +} diff --git a/apps/portal/src/app/transactions/monitor/page.mdx b/apps/portal/src/app/transactions/monitor/page.mdx new file mode 100644 index 00000000000..f7b0a7c1657 --- /dev/null +++ b/apps/portal/src/app/transactions/monitor/page.mdx @@ -0,0 +1,82 @@ +import { + Grid, + Callout, + OpenSourceCard, + ArticleIconCard, + createMetadata, + Steps, + Step, +} from "@doc"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { WalletsSmartIcon } from "@/icons"; +import { TypeScriptIcon, DotNetIcon, EngineIcon } from "@/icons"; + +export const metadata = createMetadata({ + image: { + title: "Monitor transactions", + icon: "transactions", + }, + title: "Monitor Transactions", + description: "Monitor and get notified about transactions in your application.", +}); + +# Monitor Transactions + +Monitor and get notified about transactions in your application, both on your project dashboard and programmatically. + +--- + + + + + + HTTP + + + + TypeScript / React + + + + + + You can track the status of transactions sent via the [transactions API](https://engine.thirdweb.com/reference) using its transaction id. + +```http +GET /v1/transactions?id= +Content-Type: application/json +x-secret-key: +``` + +You can also search transactions by `from` or `status`. + + + + + + You can track the status of transactions sent via the [transactions API](https://engine.thirdweb.com/reference) using its transaction id. + + ```typescript + import { Engine } from "thirdweb"; + + const executionResult = await Engine.getTransactionStatus({ + client, + transactionId, // the transaction id returned from enqueueTransaction + }); + ``` + + You can also poll for the transaction hash with this convenience function: + + ```typescript + import { Engine } from "thirdweb"; + + const { transactionHash } = await Engine.waitForTransactionHash({ + client, + transactionId, // the transaction id returned from enqueueTransaction + }); + ``` + + + + + diff --git a/apps/portal/src/app/transactions/page.mdx b/apps/portal/src/app/transactions/page.mdx new file mode 100644 index 00000000000..74c2bbe9870 --- /dev/null +++ b/apps/portal/src/app/transactions/page.mdx @@ -0,0 +1,556 @@ +import { + Callout, + OpenSourceCard, + createMetadata, + InstallTabs, + SDKCard, + Grid, + ArticleIconCard, + Tabs, + TabsList, + TabsTrigger, + TabsContent, +} from "@doc"; +import { + ReactIcon, + TypeScriptIcon, + UnityIcon, + DotNetIcon, + UnrealEngineIcon, + EngineIcon, +} from "@/icons"; +import { ExternalLink } from "lucide-react"; + +export const metadata = createMetadata({ + image: { + title: "Transactions", + icon: "transactions", + }, + title: "Transactions", + description: "Send, monitor, and manage transactions.", +}); + +# Transactions + +Send, monitor, and manage transactions. Send transactions from user or server wallets, sponsor gas, monitor transaction status, and more. + +--- + + + + + + HTTP + + + + TypeScript + + + + React + + + + React Native + + + + Unity + + + + .NET + + + + Unreal Engine + + + + + ### Send a Transaction + + You can send transactions with your [server wallets](/connect/wallet/server) using the [transactions API](https://engine.thirdweb.com/reference). + + ```http + POST /v1/contract/write + Content-Type: application/json + x-secret-key: + + { + "executionOptions": { + "from": "0x...", // your server wallet address + "chainId": "1" // your chain id + }, + "params": [{ + "contractAddress": "0x...", + "method": "function transfer(address to, uint256 amount)", + "params": ["0x...", "1000000000000000000"], + }], + } + ``` + + + + + ### Installation + + Install the thirdweb SDK in your TypeScript project: + + + + ### Set up the Client + + First, create a client instance to connect with thirdweb services: + + ```typescript + import { createThirdwebClient } from "thirdweb"; + + // - clientId for client-side applications + // - secretKey for server-side applications + const client = createThirdwebClient({ + clientId: "YOUR_CLIENT_ID", // Get from your thirdweb dashboard + }); + ``` + + ### Prepare a Contract Call + + You can prepare a contract call using the [`prepareContractCall`](/references/typescript/v5/prepareContractCall) function. This function will return a transaction object that you can then send using the `sendTransaction` function. + + You can also use the [catalog of pre-built contract calls](/references/typescript/v5/functions#extensions) to easily prepare a transaction for standards like ERC20, ERC721, ERC1155, and more. + + ```typescript + import { prepareContractCall, getContract } from "thirdweb"; + import { defineChain } from "thirdweb/chains"; + + const contract = await getContract({ + client, + address: "0x...", + chain: defineChain(8453), + }); + + const transaction = prepareContractCall({ + contract, + method: "function transfer(address to, uint256 amount)", + params: ["0x...", "1000000000000000000"], + }); + ``` + + For raw transactions, you can use the [`prepareTransaction`](/references/typescript/v5/prepareTransaction) function. + + ### Send a Transaction + + Send a transaction from a [user wallet](/connect/wallet/sign-in-methods/configure) or [server wallet](/connect/wallet/server) using the [`sendTransaction`](/references/typescript/v5/sendTransaction) function. + + The `account` parameter is the wallet that will be used to send the transaction. You can get an account object from a user wallet or a server wallet. + + ```typescript + import { sendTransaction } from "thirdweb"; + + const { transactionHash } = await sendTransaction({ + account: wallet.getAccount(), + transaction, + }); + + console.log("transaction sent", transactionHash); + ``` + + + + + ### Installation + + Install the thirdweb SDK in your React project: + + + + + ### Create a Client + + First, create a client file (e.g., `thirdwebClient.ts`) for reuse throughout your app: + + ```typescript + // thirdwebClient.ts + import { createThirdwebClient } from "thirdweb"; + + export const client = createThirdwebClient({ + clientId: "YOUR_CLIENT_ID", // Get from your thirdweb dashboard + }); + ``` + + ### Setup the Provider + + Wrap your application with the ThirdwebProvider: + + ```tsx + // app.tsx / _app.tsx + import { ThirdwebProvider } from "thirdweb/react"; + + function App() { + return ( + + + + ); + } + ``` + + ### Transaction Button Component + + Use the pre-built [TransactionButton](/references/typescript/v5/TransactionButton) for a complete transaction UI: + + ```tsx + import { TransactionButton } from "thirdweb/react"; + import { getContract, defineChain, prepareContractCall } from "thirdweb"; + + const contract = await getContract({ + client, + address: "0x...", + chain: defineChain(8453), + }); + + function YourApp() { + return ( + { + const transaction = prepareContractCall({ + contract, + method: "function transfer(address to, uint256 amount)", + params: ["0x...", "1000000000000000000"], + }); + return transaction; + }} + onTransactionConfirmed={handleSuccess} + onError={handleError} + > + Send Transaction + + ); + } + ``` + + ### Transaction Hooks + + For more custom UI, use the [transaction hooks](/references/typescript/v5/hooks#transactions): + + ```tsx + import { useSendTransaction, useActiveAccount } from "thirdweb/react"; + + function TransactionButton() { + const { mutate: sendTransaction, isPending, data } = useSendTransaction(); + + console.log("transaction hash", data?.transactionHash); + + const onClick = () => { + const transaction = prepareContractCall({ + contract, + method: "function transfer(address to, uint256 amount)", + params: ["0x...", "1000000000000000000"], + }); + + // automatically uses the connected wallet as the sender + sendTransaction(transaction); + }; + + return ( + + ); + } + ``` + + You can also use the [catalog of pre-built contract calls](/references/typescript/v5/functions#extensions) to easily prepare a transaction for standards like ERC20, ERC721, ERC1155, and more. + + + + + ### Installation + + Install the thirdweb SDK in your React Native project: + + + + + + You cannot use Expo Go with thirdweb because native modules are required. + Use an Expo development build (`npx expo prebuild`) or React Native CLI app. + + + ### Create a Client + + Create a client once and reuse it throughout your app: + + ```typescript + import { createThirdwebClient } from "thirdweb"; + + export const client = createThirdwebClient({ + clientId: "YOUR_CLIENT_ID", // Get from your thirdweb dashboard + }); + ``` + + + When creating your client ID on the thirdweb dashboard, allowlist your mobile app's bundle ID (e.g., com.yourcompany.app) for security. + + + ### Setup the Provider + + Wrap your application with the ThirdwebProvider: + + ```tsx + import { ThirdwebProvider } from "thirdweb/react"; + + function App() { + return ( + + + + ); + } + ``` + + ### Transaction Button Component + + Use the pre-built [TransactionButton](/references/typescript/v5/TransactionButton) for a complete transaction UI: + + ```tsx + import { TransactionButton } from "thirdweb/react"; + import { getContract, defineChain, prepareContractCall } from "thirdweb"; + + const contract = await getContract({ + client, + address: "0x...", + chain: defineChain(8453), + }); + + function YourApp() { + return ( + { + const transaction = prepareContractCall({ + contract, + method: "function transfer(address to, uint256 amount)", + params: ["0x...", "1000000000000000000"], + }); + return transaction; + }} + onTransactionConfirmed={handleSuccess} + onError={handleError} + > + Send Transaction + + ); + } + ``` + + ### Transaction Hooks + + For more custom UI, use the [transaction hooks](/references/typescript/v5/hooks#transactions): + + ```tsx + import { useSendTransaction, useActiveAccount } from "thirdweb/react"; + + function TransactionButton() { + const { mutate: sendTransaction, isPending, data } = useSendTransaction(); + + console.log("transaction hash", data?.transactionHash); + + const onClick = () => { + const transaction = prepareContractCall({ + contract, + method: "function transfer(address to, uint256 amount)", + params: ["0x...", "1000000000000000000"], + }); + + // automatically uses the connected wallet as the sender + sendTransaction(transaction); + }; + + return ( + + ); + } + ``` + + You can also use the [catalog of pre-built contract calls](/references/typescript/v5/functions#extensions) to easily prepare a transaction for standards like ERC20, ERC721, ERC1155, and more. + + + + + ### Installation + + 1. Download the latest [thirdweb Unity SDK](https://github.com/thirdweb-dev/unity-sdk/releases) (.unitypackage file) + 2. Import the package into your Unity project via Assets > Import Package > Custom Package + + ### Configure Client ID + + After importing the SDK: + + 1. Go to Project Settings > Thirdweb + 2. Enter your Client ID from the thirdweb dashboard + 3. Allowlist your game's Bundle ID on the thirdweb dashboard for security + + ### Send a Transaction + + Create a new script to manage transactions: + + ```csharp + using Thirdweb; + using UnityEngine; + using UnityEngine.UI; + + public class TransactionManager : MonoBehaviour + { + private ThirdwebSDK sdk; + public Text walletAddressText; + public Button sendButton; + + void Start() + { + // Client ID is set in Project Settings > Thirdweb + sdk = new ThirdwebSDK("ethereum"); // Or any supported chain + sendButton.onClick.AddListener(SendTransaction); + } + + public async void SendTransaction() + { + try { + var contract = await ThirdwebManager.Instance.GetContract( + address: "contract-address", + chainId: 1, + abi: "optional-abi" + ); + + // Send a transaction + var receipt = await contract.Write(wallet, contract, "transfer", weiValue, toAddress, amount); + Debug.Log($"Transaction receipt: {receipt}"); + } + catch (System.Exception e) { + Debug.LogError("Error sending transaction: " + e.Message); + } + } + } + ``` + + + + + ### Installation + + Install the thirdweb .NET SDK using NuGet: + + ```bash + dotnet add package Thirdweb + ``` + + ### Initialize the SDK + + Create a client instance to connect with thirdweb services: + + ```csharp + using Thirdweb; + + // For client-side applications: + var sdk = new ThirdwebSDK("ethereum", new ThirdwebSDK.Options + { + ClientId = "YOUR_CLIENT_ID" // From thirdweb dashboard + }); + + // For server-side applications: + // var sdk = new ThirdwebSDK("ethereum", new ThirdwebSDK.Options + // { + // SecretKey = Environment.GetEnvironmentVariable("THIRDWEB_SECRET_KEY") + // }); + ``` + + ### Send a Transaction + + ```csharp + BigInteger chainId = 1; // Ethereum mainnet + string contractAddress = "0x..."; // Your contract address + var contract = await ThirdwebContract.Create(client, contractAddress, chainId); + + // The wallet that signs and sends the transaction + var wallet = await PrivateKeyWallet.Create(client, "yourPrivateKeyHex"); + + // Assuming transfer takes an address and an amount as parameters + string toAddress = "0x..."; + BigInteger amount = new BigInteger(1000); // The amount to transfer + + // No ether is being sent in this non-payable transaction, so weiValue is 0 + BigInteger weiValue = BigInteger.Zero; + + // Executing the transfer + var receipt = await contract.Write(wallet, contract, "transfer", weiValue, toAddress, amount); + Console.WriteLine($"Transaction receipt: {receipt}"); + ``` + + + + + ### Installation + + 1. Download the thirdweb Unreal Engine plugin from the [Unreal Engine Marketplace](https://www.unrealengine.com/marketplace/en-US/product/thirdweb) + 2. Add the plugin to your Unreal project + 3. Enable the plugin in your project settings + + ### Configure Client ID + + 1. Go to Edit > Project Settings > Thirdweb + 2. Enter your Client ID from the thirdweb dashboard + 3. Enter your Bundle ID (must match what was allowlisted on the thirdweb dashboard) + + ### Send a Transaction + + Use the [Engine Blueprint](/unreal-engine/blueprints/engine) to send a transaction. + + + + +## Going further + +- [Sponsor Gas](/transactions/sponsor) +- [Monitor Transactions](/transactions/monitor) + +## Explore Full API References + +For comprehensive guides on implementing the full thirdweb SDK, explore our language-specific documentation: + + + + + + + + + diff --git a/apps/portal/src/app/transactions/session-keys/page.mdx b/apps/portal/src/app/transactions/session-keys/page.mdx new file mode 100644 index 00000000000..5e3862d68fb --- /dev/null +++ b/apps/portal/src/app/transactions/session-keys/page.mdx @@ -0,0 +1,223 @@ +# Session Keys + +Session keys enable secure transaction execution on behalf of smart accounts without requiring direct access to the main account's private key. This guide will walk you through creating and using session keys with the thirdweb TypeScript SDK. + +## Prerequisites + +Before you begin, ensure you have: +- A thirdweb client configured +- A frontend application +- A [server wallet](/connect/wallet/server) + +## Frontend Setup + +First, let's set up the necessary imports and configuration: + +```typescript +import { + generateAccount, + smartWallet, + sendTransaction, + getContract, + createThirdwebClient +} from "thirdweb"; +import { sepolia } from "thirdweb/chains"; +import { getAllActiveSigners } from "thirdweb/extensions/erc4337"; +import { Engine } from "thirdweb/engine"; + +// Configure your client +const client = createThirdwebClient({ + clientId: "your-client-id", +}); + +// Your session key account address +const sessionKeyServerWalletAddress = "0x..."; // Replace with your server wallet address + +// Target address for transactions +const targetAddress = "0x..."; // Replace with your target address +``` + +## Step 1: Configure User Smart Wallet with Session Key + +The first step is to add our session key address as a signer to the user's smart account. This is typically done on the client side since it needs explicit user approval. This can be done by configuring the smart wallet with the session key address and permissions. + +In a React application, this can be done by using the `ConnectButton` or `ConnectEmbed` component. This will automatically configure the smart wallet with the session key address and permissions. + +```tsx + +``` + +This can also be done in pure TypeScript by using the `smartWallet` function and connecting it to a personal account. + +For this guide, we'll generate a random personal account that will be used to create the smart wallet: + +```typescript +// this would be the user's personal account +const personalAccount = await generateAccount({ + client: client, +}); + +// wrapped in a smart wallet with session key permissions +const smart = smartWallet({ + chain: sepolia, + sessionKey: { + address: sessionKeyServerWalletAddress, + permissions: { + // "*" allows all targets, or specify specific contract addresses + approvedTargets: "*", + }, + }, + sponsorGas: true, // Enable gas sponsorship +}); + +console.log("Personal account created:", personalAccount.address); +``` + +### Session Key Permissions + +The `permissions` object allows you to control what the session key can do: + +- `approvedTargets`: Specify which contract addresses the session key can interact with + - Use `"*"` for all targets + - Use an array of addresses for specific contracts: `["0x123...", "0x456..."]` + +## Step 2: Connect Smart Account + +Connect the smart wallet using the personal account: + +```typescript +const smartAccount = await smart.connect({ + client: client, + personalAccount: personalAccount, +}); + +console.log("Smart account address:", smartAccount.address); +``` + +Note that in a React application, this would be done automatically by the `ConnectButton` or `ConnectEmbed` component. + +## Step 3 (Optional): Verify Session Key Registration + +Check that the session key is properly registered as an active signer: + +```typescript +const signers = await getAllActiveSigners({ + contract: getContract({ + address: smartAccount.address, + chain: sepolia, + client: client, + }), +}); + +// Verify the session key is in the list of active signers +const isSessionKeyActive = signers + .map((s) => s.signer) + .includes(sessionKeyServerWalletAddress); + +console.log("Session key is active:", isSessionKeyActive); +console.log("All active signers:", signers.map((s) => s.signer)); +``` + +## Step 4: Create Engine Server Wallet + +Set up an Engine server wallet using the session key for transaction execution: + +```typescript +const serverWallet = Engine.serverWallet({ + address: sessionKeyServerWalletAddress, + chain: sepolia, + client: client, + executionOptions: { + entrypointVersion: "0.6", // ERC-4337 entrypoint version + signerAddress: sessionKeyServerWalletAddress, + smartAccountAddress: smartAccount.address, + type: "ERC4337", + }, + vaultAccessToken: process.env.VAULT_TOKEN as string, // Your vault access token +}); +``` + +### Execution Options + +- `entrypointVersion`: The ERC-4337 entrypoint version to use +- `signerAddress`: The session key address that will sign transactions +- `smartAccountAddress`: The smart account address that will execute transactions +- `type`: The account abstraction type (ERC4337) + +## Step 5: Execute Transactions + +Now you can execute transactions using the session key: + +```typescript +const tx = await sendTransaction({ + account: serverWallet, + transaction: { + chain: sepolia, + client: client, + to: targetAddress, + value: 0n, // Amount in wei (0 for no ETH transfer) + // data: "0x...", // Optional: contract call data + }, +}); + +console.log("Transaction sent:", tx.transactionHash); +``` + +## Security Considerations + +- **Session Key Storage**: Store session keys securely, preferably in a vault system +- **Permission Scope**: Limit session key permissions to only necessary targets +- **Key Rotation**: Regularly rotate session keys for enhanced security +- **Monitoring**: Monitor session key usage for suspicious activity + +## Troubleshooting + +### Common Issues + +1. **Session key not active**: Ensure the session key is properly registered with the smart account +2. **Permission denied**: Check that the target address is included in `approvedTargets` +3. **Gas estimation failed**: Verify that gas sponsorship is properly configured +4. **Vault token invalid**: Ensure your vault access token is valid and has proper permissions + +### Error Handling + +Always wrap your session key operations in try-catch blocks: + +```typescript +try { + const tx = await sendTransaction({ + account: serverWallet, + transaction: { + chain: sepolia, + client, + to: targetAddress, + value: 0n, + }, + }); +} catch (error) { + if (error.message.includes("permission")) { + console.error("Session key lacks permission for this operation"); + } else if (error.message.includes("gas")) { + console.error("Gas estimation or sponsorship failed"); + } else { + console.error("Transaction failed:", error); + } +} +``` + +## Next Steps + +- Explore [Engine API Reference](https://engine.thirdweb.com/reference) +- Check out the [TypeScript SDK](/references/typescript/v5/serverWallet) documentation \ No newline at end of file diff --git a/apps/portal/src/app/transactions/sidebar.tsx b/apps/portal/src/app/transactions/sidebar.tsx new file mode 100644 index 00000000000..111f928a7dc --- /dev/null +++ b/apps/portal/src/app/transactions/sidebar.tsx @@ -0,0 +1,100 @@ +import { ZapIcon } from "lucide-react"; +import type { SideBar } from "@/components/Layouts/DocLayout"; +import { DotNetIcon, ReactIcon, TypeScriptIcon, UnityIcon } from "@/icons"; +import { UnrealEngineIcon } from "../../icons/sdks/UnrealEngineIcon"; + +const transactionsSlug = "/transactions"; + +export const sidebar: SideBar = { + links: [ + { + href: transactionsSlug, + name: "Get Started", + icon: , + }, + { + href: `${transactionsSlug}/sponsor`, + name: "Sponsor Gas", + }, + { + href: `${transactionsSlug}/monitor`, + name: "Monitor Transactions", + }, + { separator: true }, + { + name: "Guides", + isCollapsible: false, + links: [ + { + href: `${transactionsSlug}/distribute-tokens`, + name: "Distribute Tokens", + }, + { + href: `${transactionsSlug}/stripe-payments`, + name: "Sell NFTs with Stripe", + }, + { + href: `${transactionsSlug}/session-keys`, + name: "Session Keys", + }, + ], + }, + { separator: true }, + { + isCollapsible: false, + links: [ + { + href: "/references/typescript/v5/functions#transactions", + icon: , + name: "TypeScript", + }, + { + href: "/references/typescript/v5/hooks#transactions", + icon: , + name: "React", + }, + { + href: "/references/typescript/v5/hooks#transactions", + icon: , + name: "React Native", + }, + { + href: "/dotnet", + icon: , + name: "Dotnet", + }, + { + href: "/unity", + icon: , + name: "Unity", + }, + { + href: "/unreal-engine", + icon: , + name: "Unreal Engine", + }, + ], + name: "API References", + }, + { separator: true }, + { + name: "Resources", + isCollapsible: false, + links: [ + { + href: `/engine/v3`, + name: "Engine v3", + }, + { + href: `/engine/v2`, + name: "Engine v2", + }, + { + href: `/vault`, + name: "Vault", + }, + ], + }, + ], + name: "Transactions", +}; diff --git a/apps/portal/src/app/transactions/sponsor/page.mdx b/apps/portal/src/app/transactions/sponsor/page.mdx new file mode 100644 index 00000000000..88118dd8f21 --- /dev/null +++ b/apps/portal/src/app/transactions/sponsor/page.mdx @@ -0,0 +1,208 @@ +import { + Grid, + Callout, + OpenSourceCard, + ArticleIconCard, + createMetadata, + Steps, + Step, +} from "@doc"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { WalletsSmartIcon } from "@/icons"; +import { TypeScriptIcon, DotNetIcon, EngineIcon } from "@/icons"; + +export const metadata = createMetadata({ + image: { + title: "Sponsor gas", + icon: "transactions", + }, + title: "Sponsor Gas", + description: "Sponsor Gas for your user or server wallets.", +}); + +# Sponsor gas + +Sponsor gas fees for transactions using EIP-7702 or ERC-4337. Thirdweb will handle the gas fees for you. + +--- + + + + EIP-7702 + ERC-4337 + + + + +## EIP-7702 (recommended) + +Sponsor gas fees using [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702), enabling gasless transactions and improving user experience. + + + + + + HTTP + + + + TypeScript / React + + + + .NET / Unity + + + + + + You can enable EIP-7702 execution for your server wallet and the [transactions API](https://engine.thirdweb.com/reference). This is the default execution mode for server wallets. + +```http +POST /v1/contract/write +Content-Type: application/json +x-secret-key: + +{ + "executionOptions": { + "from": "0x...", // your server wallet **signer (EOA) address** address + "chainId": "1" // your chain id + }, + "params": [{ + "contractAddress": "0x...", + "method": "function transfer(address to, uint256 amount)", + "params": ["0x...", "1000000000000000000"], + }], +} +``` + +You can also explicitly set `type: "EIP7702"` in the `executionOptions` to enable EIP-7702 execution. + + + + + + You can enable EIP-7702 execution for in-app wallets by passing the `executionMode` option to the `inAppWallet` function. + + ```typescript + const wallet = inAppWallet({ + // enable gasless transactions for the wallet + executionMode: { + mode: "EIP7702", + sponsorGas: true, + }, + }); + ``` + + For server wallets, EIP-7702 execution is the default execution mode when initializing a server wallet with a signer (EOA) address. + + ```typescript + const wallet = await Engine.serverWallet({ + client, + address: "0x...", // your server wallet signer (EOA) address + }); + ``` + + + + + + You can enable EIP-7702 execution for in-app wallets by passing the `executionMode` option. + + ```csharp + var smartEoa = await InAppWallet.Create( + client: thirdwebClient, + authProvider: AuthProvider.Google, // or other auth providers + executionMode: ExecutionMode.EIP7702Sponsored // enable gas sponsorship + ); + ``` + + + + +That's it! All transactions executed by the user will be sponsored via the thirdweb infrastructure. + + + + + +## ERC-4337 + +For chains that don't support EIP-7702, you can use EIP-4337 smart contract wallets to sponsor gas fees. Note that with EIP-4337, it will create a smart contract wallet with a different address than the admin wallet (EOA) that controls it. + + + + + + HTTP + + + + TypeScript / React + + + + .NET / Unity + + + + + +You can enable ERC-4337 execution for your server wallet and the [transactions API](https://engine.thirdweb.com/reference). + +```http +POST /v1/contract/write +Content-Type: application/json +x-secret-key: + +{ + "executionOptions": { + "from": "0x...", // your server wallet **smart wallet** address + "chainId": "1" // your chain id + }, + "params": [{ + "contractAddress": "0x...", + "method": "function transfer(address to, uint256 amount)", + "params": ["0x...", "1000000000000000000"], + }], +} +``` + +You can also explicitly set `type: "ERC4337"` in the `executionOptions` to enable ERC-4337 execution. + + + + For user wallets, you can enable ERC-4337 execution by passing the `smartAccount` option to the `inAppWallet` function. + + + ```typescript + const wallet = inAppWallet({ + // will create a smart contract wallet for the user + executionMode: { + mode: "EIP4337", + smartAccount: { + chain: sepolia, + sponsorGas: true, + }, + }, + }); + ``` + + + + + You can enable ERC-4337 execution for any wallet by creating a smart wallet with the `SmartWallet.Create` function. + + ```csharp + var adminWallet = await InAppWallet.Create( + client: thirdwebClient, + authProvider: AuthProvider.Google, // or other auth providers + ); + var smartWallet = await SmartWallet.Create(adminWallet, chainId, gasless: true); + ``` + + + + + + diff --git a/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-1.png b/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-1.png new file mode 100644 index 00000000000..0d2bafcc96a Binary files /dev/null and b/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-1.png differ diff --git a/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-2.png b/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-2.png new file mode 100644 index 00000000000..5d9861dd369 Binary files /dev/null and b/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-2.png differ diff --git a/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-3.png b/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-3.png new file mode 100644 index 00000000000..e90603d12a4 Binary files /dev/null and b/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-3.png differ diff --git a/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-overview.png b/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-overview.png new file mode 100644 index 00000000000..a638500afa5 Binary files /dev/null and b/apps/portal/src/app/transactions/stripe-payments/assets/nft-checkout-overview.png differ diff --git a/apps/portal/src/app/transactions/stripe-payments/page.mdx b/apps/portal/src/app/transactions/stripe-payments/page.mdx new file mode 100644 index 00000000000..b2da9a94b75 --- /dev/null +++ b/apps/portal/src/app/transactions/stripe-payments/page.mdx @@ -0,0 +1,242 @@ +import { DocImage } from "@doc"; +import OverviewImage from "./assets/nft-checkout-overview.png"; +import NftCheckout1 from "./assets/nft-checkout-1.png"; +import NftCheckout2 from "./assets/nft-checkout-2.png"; +import NftCheckout3 from "./assets/nft-checkout-3.png"; +import { createMetadata } from "@doc"; + +export const metadata = createMetadata({ + title: "Sell NFTs with Stripe", + description: "This guide uses thirdweb to sell NFTs with credit card payments via Stripe", +}); + +# Sell NFTs with Stripe + +This guide uses thirdweb the transactions API to sell NFTs with credit card: + +1. A buyer pays with credit card. +1. Upon payment, your backend calls the transactions API. +1. the transactions API mints an NFT to the buyer's wallet. + +The buyer receives the NFT without requiring wallet signatures or gas funds. + + + +## Prerequisites + +- A thirdweb client ID and secret key from your Team > Project > Settings page. +- A [server wallet](/connect/wallet/server) +- A deployed NFT contract that can be used by the server wallet +- A [Stripe account](https://dashboard.stripe.com/register) on test mode + +## Frontend: Add Connect Wallet and credit card form + +Use [``](/references/typescript/v5/ConnectButton) to prompt the buyer for their wallet address. The buyer provides their credit card details and selects **Pay now** to send payment details directly to Stripe. + +```tsx +import { ThirdwebProvider } from "thirdweb/react"; + +function Home() { + return ( + + + + ); +} +``` + +```tsx +import { createThirdwebClient } from "thirdweb"; +import { ThirdwebProvider, ConnectButton, useActiveAccount } from "thirdweb/react"; + +const client = createThirdwebClient({ + clientId: "your-client-id", +}); + +// src/app/page.tsx +function PurchasePage() { + const buyerWalletAddress = useActiveAccount()?.address; + const [clientSecret, setClientSecret] = useState(""); + + // Retrieve a Stripe client secret to display the credit card form. + const onClick = async () => { + const resp = await fetch("/api/stripe-intent", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ buyerWalletAddress }), + }); + const json = await resp.json(); + setClientSecret(json.clientSecret); + }; + + const stripe = loadStripe(""); + + return ( +
+ + + {!clientSecret ? ( + + ) : ( + + + + )} +
+ ); +} +``` + +```tsx +const CreditCardForm = () => { + const elements = useElements(); + const stripe = useStripe(); + + const onClick = async () => { + // Submit payment to Stripe. The NFT is minted later in the webhook. + await stripe.confirmPayment({ + elements, + confirmParams: { return_url: "http://localhost:3000" }, + redirect: "if_required", + }); + alert("Payment success. The NFT will be delivered to your wallet shortly."); + }; + + return ( + <> + + + + ); +}; +``` + +## Backend: Get a Stripe client secret + +`POST /api/stripe-intent` returns a client secret which is needed to display the credit card form. + +```tsx +// src/app/api/stripe-intent/route.ts +import { NextResponse } from "next/server"; +import { Stripe } from "stripe"; + +export async function POST(req: Request) { + const { buyerWalletAddress } = await req.json(); + + const stripe = new Stripe("", { + apiVersion: "2023-10-16", + }); + const paymentIntent = await stripe.paymentIntents.create({ + amount: 100_00, + currency: "usd", + payment_method_types: ["card"], + // buyerWalletAddress is needed in the webhook. + metadata: { buyerWalletAddress }, + }); + + return NextResponse.json({ + clientSecret: paymentIntent.client_secret, + }); +} +``` + +## Backend: Configure the Stripe webhook + +`POST /api/stripe-webhook` calls the transactions API to mint an NFT when a buyer is successfully charged. + +```tsx +// src/app/api/stripe-webhook/route.ts + +import { createThirdwebClient, getContract, Engine } from "thirdweb"; +import * as ERC1155 from "thirdweb/extensions/erc1155"; + +const client = createThirdwebClient({ + secretKey: "", +}); + + +export const config = { + api: { bodyParser: false }, +}; + +export async function POST(req: NextRequest) { + // Validate the webhook signature + // Source: https://stripe.com/docs/webhooks#secure-webhook + const body = await req.text(); + const signature = headers().get("stripe-signature"); + const stripe = new Stripe("", { + apiVersion: "2023-10-16", + }); + + // Validate and parse the payload. + const event = stripe.webhooks.constructEvent( + body, + signature, + "", + ); + + if (event.type === "charge.succeeded") { + const { buyerWalletAddress } = event.data.object.metadata; + + // Mint an NFT to the buyer with your server wallet. + + // 1. get the contract, here we're using a TokenERC1155 contract (Edition) + // we also already created an NFT with token id 0 + const contract = await getContract({ + client, + address: "", // the address of the NFT contract + chain: defineChain(1), // the chain id of the NFT contract + }); + + // 2. prepare the transaction, here we're using the ERC1155 claimTo extension + const transaction = ERC1155.mintAdditionalSupplyTo({ + contract, + to: buyerWalletAddress, // the recipient address + tokenId: 0n, // the tokenId of the NFT to mint + supply: 1n, // minting 1 copy of the NFT + }); + + // 3. get the server wallet + const serverWallet = Engine.serverWallet({ + client, + address: "", + }); + + // 4. send the transaction + const { transactionId } = await serverWallet.enqueueTransaction({ + transaction, + }); + + // 5. return the transaction id to the frontend for status polling + return NextResponse.json({ transactionId }); + } + + return NextResponse.json({ message: "OK" }); +} +``` + +## Configure Stripe webhooks + +Navigate to the [Stripe webhooks dashboard (test mode)](https://dashboard.stripe.com/test/webhooks) and add the `/api/stripe-webhook` endpoint and send the `charge.succeeded` event. + +## Try it out! + +Here’s what the user flow looks like. + +The buyer is prompted to provide their credit card. + + + +They provide their card details. + +> _Tip: Stripe testmode accepts `4242 4242 4242 4242` as a valid credit card._ + + + +They are informed when their payment is submitted. + + diff --git a/apps/portal/src/app/vault/get-started/page.mdx b/apps/portal/src/app/vault/get-started/page.mdx index 0762bda870c..b3bbfb2489f 100644 --- a/apps/portal/src/app/vault/get-started/page.mdx +++ b/apps/portal/src/app/vault/get-started/page.mdx @@ -4,12 +4,12 @@ import { createMetadata } from "@/components/Document"; # Get Started with Vault -Vault is currently in beta as a part of Engine Cloud and will expand into more products. [Learn how to setup and manage your Vault with thirdweb Engine.](/engine/v3/get-started) +Vault is currently powering server wallets for the transactions API and will expand into more products. ## Manage Vault -To manage your Vault, navigate to **Engine > Vault** in your project dashboard. +To manage your Vault, navigate to **Project > Vault** in your project dashboard. Here you can create and manage access tokens and rotate your admin key. \ No newline at end of file diff --git a/apps/portal/src/icons/index.ts b/apps/portal/src/icons/index.ts index e5d69021b09..8917fa5772b 100644 --- a/apps/portal/src/icons/index.ts +++ b/apps/portal/src/icons/index.ts @@ -8,6 +8,7 @@ export { ContractInteractIcon } from "./products/contracts/ContractInteractIcon" // contracts export { ContractModularContractIcon } from "./products/contracts/ContractModularContractIcon"; export { ContractPublishIcon } from "./products/contracts/ContractPublishIcon"; +export { EngineIcon } from "./products/EngineIcon"; // infra export { InfraRPCIcon } from "./products/infra/InfraRPCIcon"; export { InfraStorageIcon } from "./products/infra/InfraStorageIcon"; diff --git a/packages/thirdweb/src/engine/server-wallet.ts b/packages/thirdweb/src/engine/server-wallet.ts index c5454873634..8e97f6b818c 100644 --- a/packages/thirdweb/src/engine/server-wallet.ts +++ b/packages/thirdweb/src/engine/server-wallet.ts @@ -80,7 +80,6 @@ export type ServerWallet = Account & { * const myServerWallet = Engine.serverWallet({ * client, * address: "", - * vaultAccessToken: "", * }); * ``` *