diff --git a/.changeset/tender-dolls-tan.md b/.changeset/tender-dolls-tan.md new file mode 100644 index 00000000000..a63018a2ade --- /dev/null +++ b/.changeset/tender-dolls-tan.md @@ -0,0 +1,63 @@ +--- +"thirdweb": minor +--- + +Added session keys to smart wallet options + +You can now pass a `sessionKey` to the `smartWallet` options function to immediately add a session key to the smart wallet upon connection. + +This is great in combination with an engine backend wallet! Let's you act on behalf of the user from your backend, making executing transactions as easy as a REST API call. Also unblocks automations, like renewing a subscription, or paying for a service. + +```ts +const wallet = smartWallet({ + sessionKey: { + address: "0x...", // the session key address (ex: engine backend wallet) + permissions: { + approvedTargets: ["0x..."], // allowed contract addresses (or * for all) + nativeTokenLimitPerTransaction: 0.1, // max spend per transaction in ETH + permissionEndTimestamp: new Date(Date.now() + 1000 * 60 * 60), // expiration date + }, + }, +}); + +// this will connect the user wallet and add the session key if not already added +await wallet.connect({ + client: TEST_CLIENT, + personalAccount, +}); +``` + +You can also pass the `sessionKey` to the `ConnectButton`, `ConnectEmbed` components and `useConnect` hook. + +```tsx + +``` + +Also works for the `inAppWallet` `smartAccount` option! + +```ts +const wallet = inAppWallet({ + smartAccount: { + chain, + sponsorGas: true, + sessionKey: { + address: "0x...", + permissions: { + approvedTargets: "*", + }, + }, + }, +}); +``` diff --git a/packages/thirdweb/src/wallets/smart/index.ts b/packages/thirdweb/src/wallets/smart/index.ts index cd4ba39f284..a9da9d2c478 100644 --- a/packages/thirdweb/src/wallets/smart/index.ts +++ b/packages/thirdweb/src/wallets/smart/index.ts @@ -6,6 +6,9 @@ import type { ThirdwebClient } from "../../client/client.js"; import { type ThirdwebContract, getContract } from "../../contract/contract.js"; import { allowance } from "../../extensions/erc20/__generated__/IERC20/read/allowance.js"; import { approve } from "../../extensions/erc20/write/approve.js"; +import { isActiveSigner } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/isActiveSigner.js"; +import { addSessionKey } from "../../extensions/erc4337/account/addSessionKey.js"; +import { sendTransaction } from "../../transaction/actions/send-transaction.js"; import { toSerializableTransaction } from "../../transaction/actions/to-serializable-transaction.js"; import type { WaitForReceiptOptions } from "../../transaction/actions/wait-for-tx-receipt.js"; import { @@ -16,6 +19,7 @@ import type { PreparedTransaction } from "../../transaction/prepare-transaction. import { readContract } from "../../transaction/read-contract.js"; import { getAddress } from "../../utils/address.js"; import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js"; +import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js"; import type { Hex } from "../../utils/encoding/hex.js"; import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js"; import { parseTypedData } from "../../utils/signatures/helpers/parse-typed-data.js"; @@ -164,6 +168,30 @@ export async function connectSmartAccount( adminAccountToSmartAccountMap.set(personalAccount, account); smartAccountToAdminAccountMap.set(account, personalAccount); + if (options.sessionKey) { + let hasSessionKey = false; + // check if already added + const accountDeployed = await isContractDeployed(accountContract); + if (accountDeployed) { + hasSessionKey = await isActiveSigner({ + contract: accountContract, + signer: options.sessionKey.address, + }); + } + if (!hasSessionKey) { + const transaction = addSessionKey({ + account: personalAccount, + contract: accountContract, + permissions: options.sessionKey.permissions, + sessionKeyAddress: options.sessionKey.address, + }); + await sendTransaction({ + account: account, + transaction, + }); + } + } + return [account, chain] as const; } diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts b/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts index f7bef6f9999..67e15e3022c 100644 --- a/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts +++ b/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts @@ -15,6 +15,7 @@ import { } from "../../exports/extensions/erc4337.js"; import { balanceOf } from "../../extensions/erc1155/__generated__/IERC1155/read/balanceOf.js"; import { claimTo } from "../../extensions/erc1155/drops/write/claimTo.js"; +import { isActiveSigner } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/isActiveSigner.js"; import { setContractURI } from "../../extensions/marketplace/__generated__/IMarketplace/write/setContractURI.js"; import { estimateGasCost } from "../../transaction/actions/estimate-gas-cost.js"; import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js"; @@ -458,5 +459,29 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential( }), ).rejects.toThrowError(/AA21 didn't pay prefund/); }); + + it("can use a session key right after connecting", async () => { + const sessionKey = await generateAccount({ client }); + const wallet = smartWallet({ + chain, + gasless: true, + sessionKey: { + address: sessionKey.address, + permissions: { + approvedTargets: "*", + }, + }, + }); + await wallet.connect({ + client: TEST_CLIENT, + personalAccount, + }); + + const isSigner = await isActiveSigner({ + contract: accountContract, + signer: sessionKey.address, + }); + expect(isSigner).toEqual(true); + }); }, ); diff --git a/packages/thirdweb/src/wallets/smart/types.ts b/packages/thirdweb/src/wallets/smart/types.ts index 6be9384a4f7..78498a07987 100644 --- a/packages/thirdweb/src/wallets/smart/types.ts +++ b/packages/thirdweb/src/wallets/smart/types.ts @@ -3,6 +3,7 @@ import type * as ox__TypedData from "ox/TypedData"; import type { Chain } from "../../chains/types.js"; import type { ThirdwebClient } from "../../client/client.js"; import type { ThirdwebContract } from "../../contract/contract.js"; +import type { AccountPermissions } from "../../extensions/erc4337/account/types.js"; import type { PreparedTransaction } from "../../transaction/prepare-transaction.js"; import type { TransactionReceipt } from "../../transaction/types.js"; import type { Hex } from "../../utils/encoding/hex.js"; @@ -21,6 +22,10 @@ export type SmartWalletOptions = Prettify< { chain: Chain; // TODO consider making default chain optional factoryAddress?: string; + sessionKey?: { + address: string; + permissions: AccountPermissions; + }; overrides?: { bundlerUrl?: string; accountAddress?: string;