diff --git a/.changeset/thin-rockets-walk.md b/.changeset/thin-rockets-walk.md new file mode 100644 index 00000000000..bde5e20d8f2 --- /dev/null +++ b/.changeset/thin-rockets-walk.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Default to in-memory storage when creating inapp wallets outside the browser diff --git a/packages/thirdweb/src/wallets/in-app/web/in-app.ts b/packages/thirdweb/src/wallets/in-app/web/in-app.ts index 11597f42ff8..1e60d716543 100644 --- a/packages/thirdweb/src/wallets/in-app/web/in-app.ts +++ b/packages/thirdweb/src/wallets/in-app/web/in-app.ts @@ -23,7 +23,7 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js"; * const account = await wallet.connect({ * client, * chain, - * strategy: "google", + * strategy: "google", // or "apple", "facebook","discord", "github", "twitch", "x", "telegram", "line", "coinbase", etc * }); * ``` * @@ -31,15 +31,21 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js"; * * ### Enable smart accounts and sponsor gas for your users: * + * With the `executionMode` option, you can enable smart accounts and sponsor gas for your users. + * + * **Using EIP-7702** (recommended): + * + * On chains with EIP-7702 enabled, you can upgrade the inapp wallet to a smart account, keeping the same address and performance as the regular EOA. + * * ```ts * import { inAppWallet } from "thirdweb/wallets"; * import { sepolia } from "thirdweb/chains"; * * const wallet = inAppWallet({ - * smartAccount: { - * chain: sepolia, + * executionMode: { + * mode: "EIP7702", * sponsorGas: true, - * }, + * }, * }); * * // account will be a smart account with sponsored gas enabled @@ -49,8 +55,28 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js"; * }); * ``` * + * **Using EIP-4337**: + * + * On chains without EIP-7702 enabled, you can still use smart accounts using EIP-4337, this will return a different address (the smart contract address) than the regular EOA. + * + * ```ts + * import { inAppWallet } from "thirdweb/wallets/in-app"; + * + * const wallet = inAppWallet({ + * executionMode: { + * mode: "EIP4337", + * smartAccount: { + * chain: sepolia, // chain required for EIP-4337 + * sponsorGas: true, + * } + * }, + * }); + * ``` + * * ### Login with email * + * To login with email, you can use the `preAuthenticate` function to first send a verification code to the user's email, then login with the verification code. + * * ```ts * import { inAppWallet, preAuthenticate } from "thirdweb/wallets/in-app"; * @@ -73,22 +99,10 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js"; * }); * ``` * - * ### Login with SIWE - * ```ts - * import { inAppWallet, createWallet } from "thirdweb/wallets"; - * - * const rabby = createWallet("io.rabby"); - * const inAppWallet = inAppWallet(); + * ### Login with phone number * - * const account = await inAppWallet.connect({ - * strategy: "wallet", - * chain: mainnet, - * wallet: rabby, - * client: MY_CLIENT - * }); - * ``` + * Similar to email, you can login with a phone number by first sending a verification code to the user's phone number, then login with the verification code. * - * ### Login with phone number * ```ts * import { inAppWallet, preAuthenticate } from "thirdweb/wallets/in-app"; * @@ -111,8 +125,28 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js"; * }); * ``` * + * ### Login with another wallet (SIWE) + * + * You can also login to the in-app wallet with another existing wallet by signing a standard Sign in with Ethereum (SIWE) message. + * + * ```ts + * import { inAppWallet, createWallet } from "thirdweb/wallets"; + * + * const rabby = createWallet("io.rabby"); + * const inAppWallet = inAppWallet(); + * + * const account = await inAppWallet.connect({ + * strategy: "wallet", + * chain: mainnet, + * wallet: rabby, + * client: MY_CLIENT + * }); + * ``` + * * ### Login with passkey * + * You can also login with a passkey. This mode requires specifying whether it should create a new passkey, or sign in with an existing passkey. We recommend checking if the user has a passkey stored in their browser to automatically login with it. + * * ```ts * import { inAppWallet, hasStoredPasskey } from "thirdweb/wallets/in-app"; * @@ -128,6 +162,11 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js"; * ``` * * ### Connect to a guest account + * + * You can also connect to a guest account, this will create a new account for the user instantly and store it in the browser's local storage. + * + * You can later "upgrade" this account by linking another auth method, like email or phone for example. This will preserve the account's address and history. + * * ```ts * import { inAppWallet } from "thirdweb/wallets"; * @@ -141,19 +180,19 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js"; * * ### Connect to a backend account * - * for usage in backends, you might also need to provide a 'storage' to store auth tokens. In-memory usually works for most purposes. + * For usage in backends, you can create wallets with the `backend` strategy and a stable walletSecret. + * + * Make sure to keep that walletSecret safe as it is the key to access that wallet, never expose it to the client. * * ```ts * import { inAppWallet } from "thirdweb/wallets"; * - * const wallet = inAppWallet({ - * storage: inMemoryStorage, // for usage in backends/scripts - * }); + * const wallet = inAppWallet(); * * const account = await wallet.connect({ * client, * strategy: "backend", - * walletSecret: "...", // Provided by your app + * walletSecret: "...", // Your own secret, keep it safe * }); * ``` * @@ -189,16 +228,21 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js"; * }); * ``` * - * ### Specify a logo for your login page (Connect UI) + * ### Specify a logo, icon and name for your login page (Connect UI) + * + * You can specify a logo, icon and name for your login page to customize how in-app wallets are displayed in the Connect UI components (ConnectButton and ConnectEmbed). + * * ```ts * import { inAppWallet } from "thirdweb/wallets"; * const wallet = inAppWallet({ * metadata: { - * image: { - * src: "https://example.com/logo.png", - * alt: "My logo", - * width: 100, - * height: 100, + * name: "My App", + * icon: "https://example.com/icon.png", + * image: { + * src: "https://example.com/logo.png", + * alt: "My logo", + * width: 100, + * height: 100, * }, * }, * }); @@ -206,6 +250,8 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js"; * * ### Hide the ability to export the private key within the Connect Modal UI * + * By default, the Connect Modal will show a button to export the private key of the wallet. You can hide this button by setting the `hidePrivateKeyExport` option to `true`. + * * ```ts * import { inAppWallet } from "thirdweb/wallets"; * const wallet = inAppWallet({ @@ -228,7 +274,7 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js"; * * ### Override storage for the wallet state * - * By default, wallet state is stored in the browser's local storage. You can override this behavior by providing a custom storage object, useful for server side integrations. + * By default, wallet state is stored in the browser's local storage if in the browser, or in-memory storage if not in the browser. You can override this behavior by providing a custom storage object, useful for server side and CLI integrations. * * ```ts * import { inAppWallet } from "thirdweb/wallets"; diff --git a/packages/thirdweb/src/wallets/in-app/web/lib/in-app-backend.test.ts b/packages/thirdweb/src/wallets/in-app/web/lib/in-app-backend.test.ts deleted file mode 100644 index 8da74d7c2eb..00000000000 --- a/packages/thirdweb/src/wallets/in-app/web/lib/in-app-backend.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { TEST_CLIENT } from "~test/test-clients.js"; -import { sepolia } from "../../../../chains/chain-definitions/sepolia.js"; -import { inMemoryStorage } from "../../../../utils/storage/inMemoryStorage.js"; -import { inAppWallet } from "../in-app.js"; - -describe("InAppWallet", () => { - it("should sign a message with backend strategy", async () => { - const wallet = inAppWallet({ - smartAccount: { - chain: sepolia, - sponsorGas: true, - }, - storage: inMemoryStorage, - }); - const account = await wallet.connect({ - client: TEST_CLIENT, - strategy: "backend", - walletSecret: "test-secret", - }); - expect(account.address).toBeDefined(); - const message = await account.signMessage({ - message: "Hello, world!", - }); - expect(message).toBeDefined(); - }); -}); diff --git a/packages/thirdweb/src/wallets/in-app/web/lib/in-app-integration.test.ts b/packages/thirdweb/src/wallets/in-app/web/lib/in-app-integration.test.ts new file mode 100644 index 00000000000..97fd4eea3f5 --- /dev/null +++ b/packages/thirdweb/src/wallets/in-app/web/lib/in-app-integration.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from "vitest"; +import { TEST_CLIENT } from "~test/test-clients.js"; +import { sepolia } from "../../../../chains/chain-definitions/sepolia.js"; +import { parseEventLogs } from "../../../../event/actions/parse-logs.js"; +import { userOperationEventEvent } from "../../../../extensions/erc4337/__generated__/IEntryPoint/events/UserOperationEvent.js"; +import { executedEvent } from "../../../../extensions/erc7702/__generated__/MinimalAccount/events/Executed.js"; +import { sendAndConfirmTransaction } from "../../../../transaction/actions/send-and-confirm-transaction.js"; +import { prepareTransaction } from "../../../../transaction/prepare-transaction.js"; +import { inAppWallet } from "../in-app.js"; +describe("InAppWallet Integration Tests", () => { + it("should sign a message with backend strategy", async () => { + const wallet = inAppWallet(); + const account = await wallet.connect({ + client: TEST_CLIENT, + strategy: "backend", + walletSecret: "test-secret", + }); + expect(account.address).toBeDefined(); + const message = await account.signMessage({ + message: "Hello, world!", + }); + expect(message).toBeDefined(); + }); + + it("should sign a message with guest strategy", async () => { + const wallet = inAppWallet(); + const account = await wallet.connect({ + client: TEST_CLIENT, + strategy: "guest", + }); + expect(account.address).toBeDefined(); + const message = await account.signMessage({ + message: "Hello, world!", + }); + expect(message).toBeDefined(); + }); + + it("should sponsor gas for a 7702 smart account", async () => { + const chain = sepolia; + const wallet = inAppWallet({ + executionMode: { + mode: "EIP7702", + sponsorGas: true, + }, + }); + const account = await wallet.connect({ + client: TEST_CLIENT, + strategy: "guest", + chain, + }); + expect(account.address).toBeDefined(); + const tx = await sendAndConfirmTransaction({ + transaction: prepareTransaction({ + chain, + client: TEST_CLIENT, + to: account.address, + value: 0n, + }), + account, + }); + expect(tx.transactionHash).toBeDefined(); + const logs = parseEventLogs({ + logs: tx.logs, + events: [executedEvent()], + }); + const executedLog = logs[0]; + if (!executedLog) { + throw new Error("No executed log found"); + } + expect(executedLog.args.to).toBe(account.address); + expect(executedLog.args.value).toBe(0n); + }); + + it("should sponsor gas for a 4337 smart account", async () => { + const chain = sepolia; + const wallet = inAppWallet({ + executionMode: { + mode: "EIP4337", + smartAccount: { + chain, + sponsorGas: true, + }, + }, + }); + const account = await wallet.connect({ + client: TEST_CLIENT, + strategy: "guest", + chain, + }); + expect(account.address).toBeDefined(); + const tx = await sendAndConfirmTransaction({ + transaction: prepareTransaction({ + chain, + client: TEST_CLIENT, + to: account.address, + value: 0n, + }), + account, + }); + expect(tx.transactionHash).toBeDefined(); + const logs = parseEventLogs({ + logs: tx.logs, + events: [userOperationEventEvent()], + }); + const executedLog = logs[0]; + if (!executedLog) { + throw new Error("No executed log found"); + } + expect(executedLog.args.sender).toBe(account.address); + expect(executedLog.args.success).toBe(true); + }); +}); diff --git a/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts b/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts index 2b8b1b07028..db7ab3124b8 100644 --- a/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts +++ b/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts @@ -1,5 +1,7 @@ import type { ThirdwebClient } from "../../../../client/client.js"; import { getThirdwebBaseUrl } from "../../../../utils/domains.js"; +import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js"; +import { inMemoryStorage } from "../../../../utils/storage/inMemoryStorage.js"; import { webLocalStorage } from "../../../../utils/storage/webStorage.js"; import type { SocialAuthOption } from "../../../../wallets/types.js"; import type { Account } from "../../../interfaces/wallet.js"; @@ -86,7 +88,7 @@ export class InAppWebConnector implements InAppConnector { this.ecosystem = ecosystem; this.passkeyDomain = passkeyDomain; this.storage = new ClientScopedStorage({ - storage: storage ?? webLocalStorage, + storage: storage ?? getDefaultStorage(), clientId: client.clientId, ecosystem: ecosystem, }); @@ -489,3 +491,11 @@ export class InAppWebConnector implements InAppConnector { function assertUnreachable(x: never, message?: string): never { throw new Error(message ?? `Invalid param: ${x}`); } + +function getDefaultStorage(): AsyncStorage { + if (typeof window !== "undefined" && window.localStorage) { + return webLocalStorage; + } + // default to in-memory storage if we're not in the browser + return inMemoryStorage; +}