diff --git a/.changeset/odd-rockets-sink.md b/.changeset/odd-rockets-sink.md new file mode 100644 index 00000000000..59da1d2952e --- /dev/null +++ b/.changeset/odd-rockets-sink.md @@ -0,0 +1,5 @@ +--- +"@thirdweb-dev/api": patch +--- + +Update to latest API diff --git a/apps/playground-web/src/app/data/pages-metadata.ts b/apps/playground-web/src/app/data/pages-metadata.ts index 06630eeb74d..f3dde86eda2 100644 --- a/apps/playground-web/src/app/data/pages-metadata.ts +++ b/apps/playground-web/src/app/data/pages-metadata.ts @@ -106,6 +106,12 @@ export const headlessComponentsFeatureCards: FeatureCardMetadata[] = [ ]; export const transactionsFeatureCards: FeatureCardMetadata[] = [ + { + icon: UserIcon, + title: "From User Wallets", + link: "/transactions/users", + description: "Transactions from user wallets with monitoring and retries.", + }, { icon: PlaneIcon, title: "Airdrop Tokens", diff --git a/apps/playground-web/src/app/navLinks.ts b/apps/playground-web/src/app/navLinks.ts index 9428ab71cfd..7467bf77cb7 100644 --- a/apps/playground-web/src/app/navLinks.ts +++ b/apps/playground-web/src/app/navLinks.ts @@ -219,6 +219,10 @@ const transactions: ShadcnSidebarLink = { href: "/transactions", exactMatch: true, }, + { + href: "/transactions/users", + label: "From User Wallets", + }, { href: "/transactions/airdrop-tokens", label: "Airdrop Tokens", diff --git a/apps/playground-web/src/app/transactions/users/page.tsx b/apps/playground-web/src/app/transactions/users/page.tsx new file mode 100644 index 00000000000..3a5d0202ff0 --- /dev/null +++ b/apps/playground-web/src/app/transactions/users/page.tsx @@ -0,0 +1,101 @@ +import { User2Icon } from "lucide-react"; +import type { Metadata } from "next"; +import { GatewayPreview } from "@/components/account-abstraction/gateway"; +import { PageLayout } from "@/components/blocks/APIHeader"; +import { CodeExample } from "@/components/code/code-example"; +import ThirdwebProvider from "@/components/thirdweb-provider"; +import { metadataBase } from "@/lib/constants"; + +export const metadata: Metadata = { + description: "Transactions from user wallets with monitoring and retries", + metadataBase, + title: "User Transactions | thirdweb", +}; + +export default function Page() { + return ( + + Transactions from user wallets with monitoring and retries. + } + docsLink="https://portal.thirdweb.com/transactions?utm_source=playground" + title="User Transactions" + > + + + + ); +} + +function UserTransactions() { + return ( + <> + { + const walletAddress = activeWallet?.getAccount()?.address; + const authToken = activeWallet?.getAuthToken?.(); + // transactions are a simple POST request to the thirdweb API + // or use the @thirdweb-dev/api type-safe JS SDK + const response = await fetch( + "https://api.thirdweb.com/v1/contract/write", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-client-id": "", + // uses the in-app wallet's auth token to authenticate the request + "Authorization": "Bearer " + authToken, + }, + body: JSON.stringify({ + from: walletAddress, + chainId: "84532", + calls: [ + { + contractAddress: "0x...", + method: "function claim(address to, uint256 amount)", + params: [walletAddress, "1"], + }, + ], + }), + }); + }; + + return ( + <> + + + + ); +}`} + header={{ + description: + "Queue, monitor, and retry transactions from your users in-app wallets. All transactions and analytics will be displayed in your developer dashboard.", + title: "Transactions from User Wallets", + }} + lang="tsx" + preview={} + /> + + ); +} diff --git a/apps/playground-web/src/components/account-abstraction/gateway.tsx b/apps/playground-web/src/components/account-abstraction/gateway.tsx new file mode 100644 index 00000000000..c1ca8349b00 --- /dev/null +++ b/apps/playground-web/src/components/account-abstraction/gateway.tsx @@ -0,0 +1,292 @@ +"use client"; + +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useState } from "react"; +import { encode, getContract } from "thirdweb"; +import { baseSepolia } from "thirdweb/chains"; +import { claimTo, getNFT, getOwnedNFTs } from "thirdweb/extensions/erc1155"; +import { + ConnectButton, + MediaRenderer, + useActiveAccount, + useActiveWallet, + useDisconnect, + useReadContract, +} from "thirdweb/react"; +import { stringify } from "thirdweb/utils"; +import { inAppWallet } from "thirdweb/wallets/in-app"; +import { THIRDWEB_CLIENT } from "../../lib/client"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "../ui/table"; + +const url = `http://${process.env.NEXT_PUBLIC_API_URL}`; + +const chain = baseSepolia; +const editionDropAddress = "0x638263e3eAa3917a53630e61B1fBa685308024fa"; +const editionDropTokenId = 2n; + +const editionDropContract = getContract({ + address: editionDropAddress, + chain, + client: THIRDWEB_CLIENT, +}); + +const iaw = inAppWallet(); + +function TransactionRow({ transactionId }: { transactionId: string }) { + const { data: txStatus, isLoading } = useQuery({ + enabled: !!transactionId, + queryFn: async () => { + const response = await fetch(`${url}/v1/transactions/${transactionId}`, { + headers: { + "Content-type": "application/json", + "x-client-id": THIRDWEB_CLIENT.clientId, + }, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error( + `Failed to send transaction: ${response.statusText} - ${text}`, + ); + } + + const results = await response.json(); + const transaction = results.result; + + return transaction; + }, + queryKey: ["txStatus", transactionId], + refetchInterval: 2000, + }); + + const getStatusBadge = (status: string) => { + switch (status) { + case undefined: + case "QUEUED": + return Queued; + case "SUBMITTED": + return Submitted; + case "CONFIRMED": + return Confirmed; + case "FAILED": + return Failed; + default: + return {"Unknown"}; + } + }; + + const renderTransactionHash = () => { + if (!txStatus) return "-"; + + const execStatus = txStatus?.status; + + let txHash: string | undefined; + if (execStatus === "CONFIRMED") { + txHash = txStatus.transactionHash; + } + + if (txHash && chain.blockExplorers?.[0]?.url) { + return ( + + {txHash.slice(0, 6)}...{txHash.slice(-4)} + + ); + } + + return txHash ? ( + + {txHash.slice(0, 6)}...{txHash.slice(-4)} + + ) : ( + "-" + ); + }; + + return ( + + + {transactionId.slice(0, 8)}...{transactionId.slice(-4)} + + + {isLoading || !txStatus.executionResult?.status ? ( + Queued + ) : ( + getStatusBadge(txStatus.executionResult?.status) + )} + + {renderTransactionHash()} + + ); +} + +export function GatewayPreview() { + const [txIds, setTxIds] = useState([]); + const activeEOA = useActiveAccount(); + const activeWallet = useActiveWallet(); + const { disconnect } = useDisconnect(); + const { data: nft, isLoading: isNftLoading } = useReadContract(getNFT, { + contract: editionDropContract, + tokenId: editionDropTokenId, + }); + const { data: ownedNfts } = useReadContract(getOwnedNFTs, { + // biome-ignore lint/style/noNonNullAssertion: handled by queryOptions + address: activeEOA?.address!, + contract: editionDropContract, + queryOptions: { enabled: !!activeEOA, refetchInterval: 2000 }, + useIndexer: false, + }); + + const { data: preparedTx } = useQuery({ + enabled: !!activeEOA, + queryFn: async () => { + if (!activeEOA) { + throw new Error("No active EOA"); + } + const tx = claimTo({ + contract: editionDropContract, + quantity: 1n, + to: activeEOA.address, + tokenId: editionDropTokenId, + }); + return { + data: await encode(tx), + to: editionDropContract.address, + }; + }, + queryKey: ["tx", activeEOA?.address], + }); + + const sendTransactionMutation = useMutation({ + mutationFn: async () => { + if (!preparedTx || !activeEOA) { + throw new Error("Missing required data"); + } + + const response = await fetch(`${url}/v1/transactions`, { + body: stringify({ + from: activeEOA.address, + chainId: baseSepolia.id, + transactions: [preparedTx], + }), + headers: { + Authorization: `Bearer ${activeWallet?.getAuthToken?.()}`, + "Content-type": "application/json", + "x-client-id": THIRDWEB_CLIENT.clientId, + }, + method: "POST", + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error( + `Failed to send transaction: ${response.statusText} - ${text}`, + ); + } + + const results = await response.json(); + const txId = results.result?.transactionIds?.[0]; + if (!txId) { + throw new Error("No transaction ID"); + } + + return txId; + }, + onSuccess: (txId) => { + setTxIds((prev) => [...prev, txId]); + }, + }); + + if (activeEOA && activeWallet && activeWallet?.id !== iaw.id) { + return ( +
+ Please connect with an in-app wallet for this example + +
+ ); + } + + return ( +
+ {isNftLoading ? ( +
Loading...
+ ) : ( + <> +
+ +
+ {nft ? ( + + ) : null} + {activeEOA ? ( +
+

+ You own {ownedNfts?.[0]?.quantityOwned.toString() || "0"}{" "} + {nft?.metadata?.name} +

+ +
+ ) : null} + {txIds.length > 0 && ( +
+ + + + + Tx ID + Status + TX Hash + + + + {txIds.map((txId) => ( + + ))} + +
+
+
+ )} + + )} +
+ ); +} diff --git a/packages/thirdweb/src/wallets/in-app/web/lib/in-app-gateway.test.ts b/packages/thirdweb/src/wallets/in-app/web/lib/in-app-gateway.test.ts index 54b22a490c6..de239b75aad 100644 --- a/packages/thirdweb/src/wallets/in-app/web/lib/in-app-gateway.test.ts +++ b/packages/thirdweb/src/wallets/in-app/web/lib/in-app-gateway.test.ts @@ -1,126 +1,110 @@ -import { sendTransaction, signMessage } from "@thirdweb-dev/engine"; +import { + configure, + isSuccessResponse, + sendTransaction, + signMessage, +} from "@thirdweb-dev/engine"; import { beforeAll, describe, expect, it } from "vitest"; import { TEST_CLIENT } from "~test/test-clients.js"; import { sepolia } from "../../../../chains/chain-definitions/sepolia.js"; -import { createThirdwebClient } from "../../../../client/client.js"; import { waitForTransactionHash } from "../../../../engine/wait-for-tx-hash.js"; -import { - getThirdwebBaseUrl, - setThirdwebDomains, -} from "../../../../utils/domains.js"; -import { getClientFetch } from "../../../../utils/fetch.js"; import { stringify } from "../../../../utils/json.js"; import type { Account } from "../../../interfaces/wallet.js"; import { inAppWallet } from "../in-app.js"; -// TODO: productionize this test -describe - .runIf(process.env.TW_SECRET_KEY) - .skip("InAppWallet Gateway Tests", () => { - let account: Account; - let authToken: string | null | undefined; - const clientIdFetch = getClientFetch( - createThirdwebClient({ - clientId: TEST_CLIENT.clientId, - }), - ); +describe.runIf(process.env.TW_SECRET_KEY)("InAppWallet Gateway Tests", () => { + let account: Account; + let authToken: string | null | undefined; - beforeAll(async () => { - setThirdwebDomains({ - bundler: "bundler.thirdweb-dev.com", - engineCloud: "engine.thirdweb-dev.com", - inAppWallet: "embedded-wallet.thirdweb-dev.com", - rpc: "rpc.thirdweb-dev.com", - }); - const wallet = inAppWallet(); - account = await wallet.connect({ - client: TEST_CLIENT, - strategy: "backend", - walletSecret: "test-secret", - }); - authToken = wallet.getAuthToken(); - expect(authToken).toBeDefined(); + beforeAll(async () => { + configure({ + clientId: TEST_CLIENT.clientId, + secretKey: TEST_CLIENT.secretKey, }); + const wallet = inAppWallet(); + account = await wallet.connect({ + client: TEST_CLIENT, + strategy: "backend", + walletSecret: "test-secret", + }); + authToken = wallet.getAuthToken?.(); + expect(authToken).toBeDefined(); + }); - it("should sign a message with backend strategy", async () => { - const rawSignature = await account.signMessage({ - message: "Hello, world!", - }); - - // sign via api - const signResult = await signMessage({ - baseUrl: getThirdwebBaseUrl("engineCloud"), - body: { - params: [ - { - format: "text", - message: "Hello, world!", - }, - ], - signingOptions: { - from: account.address, - type: "EOA", - }, - }, - bodySerializer: stringify, - fetch: clientIdFetch, - headers: { - "x-wallet-access-token": authToken, - }, - }); - - const signatureResult = signResult.data?.result[0]; - if (signatureResult && "result" in signatureResult) { - expect(signatureResult.result.signature).toEqual(rawSignature); - } else { - throw new Error( - `Failed to sign message: ${stringify(signatureResult?.error) || "Unknown error"}`, - ); - } + it("should sign a message with backend strategy", async () => { + const rawSignature = await account.signMessage({ + message: "Hello, world!", }); - it("should queue a 7702 transaction", async () => { - const body = { - executionOptions: { - chainId: sepolia.id, - from: account.address, - type: "auto" as const, - }, + // sign via api + const signResult = await signMessage({ + body: { params: [ { - data: "0x", - to: account.address, - value: "0", + format: "text", + message: "Hello, world!", }, ], - }; - const result = await sendTransaction({ - baseUrl: getThirdwebBaseUrl("engineCloud"), - body, - bodySerializer: stringify, - fetch: clientIdFetch, - headers: { - "x-wallet-access-token": authToken, + signingOptions: { + from: account.address, + type: "EOA", }, - }); - if (result.error) { - throw new Error( - `Error sending transaction: ${stringify(result.error)}`, - ); - } + }, + headers: { + "x-wallet-access-token": authToken, + }, + }); - const txId = result.data?.result.transactions[0]?.id; - console.log(txId); - if (!txId) { - throw new Error("No transaction ID found"); - } + if (signResult.error) { + throw new Error(`Error signing message: ${stringify(signResult.error)}`); + } - const tx = await waitForTransactionHash({ - client: TEST_CLIENT, - transactionId: txId, - }); + const signatureResult = signResult.data?.result?.[0]; + if (signatureResult && isSuccessResponse(signatureResult)) { + expect(signatureResult.result.signature).toEqual(rawSignature); + } else { + throw new Error( + `Failed to sign message: ${stringify(signatureResult?.error) || "Unknown error"}`, + ); + } + }); - console.log(tx.transactionHash); - expect(tx.transactionHash).toBeDefined(); + it("should queue a 4337 transaction", async () => { + const body = { + executionOptions: { + chainId: sepolia.id, + from: account.address, + type: "auto" as const, + }, + params: [ + { + data: "0x", + to: account.address, + value: "0", + }, + ], + }; + const result = await sendTransaction({ + body, + headers: { + "x-wallet-access-token": authToken, + }, }); + if (result.error) { + throw new Error(`Error sending transaction: ${stringify(result.error)}`); + } + + const txId = result.data?.result.transactions[0]?.id; + if (!txId) { + throw new Error("No transaction ID found"); + } + + const tx = await waitForTransactionHash({ + client: TEST_CLIENT, + transactionId: txId, + }); + + console.log(tx.transactionHash); + expect(tx.transactionHash).toBeDefined(); }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cec5a27ed3..924ce4d9252 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7581,7 +7581,6 @@ packages: '@walletconnect/modal@2.7.0': resolution: {integrity: sha512-RQVt58oJ+rwqnPcIvRFeMGKuXb9qkgSmwz4noF8JZGUym3gUAzVs+uW2NQ1Owm9XOJAV+sANrtJ+VoVq1ftElw==} - deprecated: Please follow the migration guide on https://docs.reown.com/appkit/upgrade/wcm '@walletconnect/react-native-compat@2.17.3': resolution: {integrity: sha512-lHKwXKoB0rdDH1ukxUx7o86xosWbttWIHYMZ8tgAQC1k9VH3CZZCoBcHOAAX8iBzyb0n0UP3/9zRrOcJE5nz7Q==} @@ -16330,10 +16329,10 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0 + '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -16376,10 +16375,10 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0 + '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -16422,10 +16421,10 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0 + '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) '@aws-sdk/client-sts': 3.592.0 '@aws-sdk/core': 3.592.0 - '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -16469,7 +16468,7 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.592.0': + '@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 @@ -16512,6 +16511,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: + - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso-oidc@3.840.0': @@ -16648,7 +16648,7 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.592.0 + '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16767,6 +16767,24 @@ snapshots: '@smithy/util-stream': 4.2.3 tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0)': + dependencies: + '@aws-sdk/client-sts': 3.592.0 + '@aws-sdk/credential-provider-env': 3.587.0 + '@aws-sdk/credential-provider-http': 3.587.0 + '@aws-sdk/credential-provider-process': 3.587.0 + '@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0) + '@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/types': 3.577.0 + '@smithy/credential-provider-imds': 3.2.8 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + '@aws-sdk/credential-provider-ini@3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0)': dependencies: '@aws-sdk/client-sts': 3.592.0 @@ -16821,6 +16839,25 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-node@3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0)': + dependencies: + '@aws-sdk/credential-provider-env': 3.587.0 + '@aws-sdk/credential-provider-http': 3.587.0 + '@aws-sdk/credential-provider-ini': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0(@aws-sdk/client-sts@3.592.0))(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/credential-provider-process': 3.587.0 + '@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/client-sso-oidc@3.592.0) + '@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/client-sts@3.592.0) + '@aws-sdk/types': 3.577.0 + '@smithy/credential-provider-imds': 3.2.8 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - '@aws-sdk/client-sts' + - aws-crt + '@aws-sdk/credential-provider-node@3.592.0(@aws-sdk/client-sso-oidc@3.592.0)(@aws-sdk/client-sts@3.592.0)': dependencies: '@aws-sdk/credential-provider-env': 3.587.0 @@ -17094,7 +17131,7 @@ snapshots: '@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.592.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.592.0 + '@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/client-sts@3.592.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.11 '@smithy/shared-ini-file-loader': 3.1.12