diff --git a/.changeset/yellow-snails-exist.md b/.changeset/yellow-snails-exist.md new file mode 100644 index 000000000..916b7bcc0 --- /dev/null +++ b/.changeset/yellow-snails-exist.md @@ -0,0 +1,7 @@ +--- +"@biconomy/account": minor +--- + +Sessions DevEx + +- Improved DevEx related to the creating and using of sessions [chore: sessions dx](https://github.com/bcnmy/biconomy-client-sdk/pull/486) diff --git a/.size-limit.json b/.size-limit.json index a2bdf369c..7fa63ece7 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,18 +2,18 @@ { "name": "core (esm)", "path": "./dist/_esm/index.js", - "limit": "60 kB", + "limit": "65 kB", "import": "*" }, { "name": "core (cjs)", "path": "./dist/_cjs/index.js", - "limit": "60 kB" + "limit": "65 kB" }, { "name": "account (tree-shaking)", "path": "./dist/_esm/index.js", - "limit": "60 kB", + "limit": "65 kB", "import": "{ createSmartAccountClient }" }, { diff --git a/CHANGELOG.md b/CHANGELOG.md index 96cc7c818..2f0425a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # @biconomy/account +## 4.4.0 + +### Minor Changes + +- Sessions DevEx + + - Improved DevEx related to the creating and using of sessions + +## 4.3.0 + +### Minor Changes + +- Added transferOwnership and gasOffsets + + - added transferOwnerhsip() method on the Smart Account + - added gasOffsets parameter to increase gas values + ## 4.2.0 ### Minor Changes diff --git a/README.md b/README.md index 8d8bd5bca..db77d8f2d 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,6 @@ The Biconomy SDK is your all-in-one toolkit for building decentralized applicati - [📄 Documentation and Resources](#-documentation-and-resources) - [💼 Examples](#-examples) - - - [🛠️ Initialise a smartAccount](#-initialise-a-smartAccount) - - [📨 send some eth with sponsorship](#-send-some-eth-with-sponsorship) - - [🔢 send a multi tx and pay gas with a token](#️-send-a-multi-tx-and-pay-gas-with-a-token) - - [License](#license) - [Connect with Biconomy 🍊](#connect-with-biconomy-🍊) @@ -73,91 +68,10 @@ For a comprehensive understanding of our project and to contribute effectively, ## 💼 Examples -### [Initialise a smartAccount](https://bcnmy.github.io/biconomy-client-sdk/functions/createSmartAccountClient.html) - -| Key | Description | -| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| [signer](https://bcnmy.github.io/biconomy-client-sdk/packages/account/docs/interfaces/SmartAccountSigner.html) | This signer will be used for signing userOps for any transactions you build. Will accept ethers.JsonRpcSigner as well as a viemWallet | -| [paymasterUrl](https://dashboard.biconomy.io) | You can pass in a paymasterUrl necessary for sponsoring transactions (retrieved from the biconomy dashboard) | -| [bundlerUrl](https://dashboard.biconomy.io) | You can pass in a bundlerUrl (retrieved from the biconomy dashboard) for sending transactions | - -```typescript -import { createSmartAccountClient } from "@biconomy/account"; -import { createWalletClient, http, createPublicClient } from "viem"; -import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; -import { mainnet as chain } from "viem/chains"; - -const account = privateKeyToAccount(generatePrivateKey()); -const signer = createWalletClient({ account, chain, transport: http() }); - -const smartAccount = await createSmartAccountClient({ - signer, - bundlerUrl, - paymasterUrl, -}); -``` - -### [Send some eth with sponsorship](https://bcnmy.github.io/biconomy-client-sdk/classes/BiconomySmartAccountV2.html#sendTransaction) - -| Key | Description | -| --------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [oneOrManyTx](https://bcnmy.github.io/biconomy-client-sdk/types/Transaction.html) | Submit multiple or one transactions | -| [userOpReceipt](https://bcnmy.github.io/biconomy-client-sdk/types/UserOpReceipt) | Returned information about your tx, receipts, userOpHashes etc | - -```typescript -const oneOrManyTx = { to: "0x...", value: 1 }; - -const { wait } = await smartAccount.sendTransaction(oneOrManyTx, { - paymasterServiceData: { - mode: PaymasterMode.SPONSORED, - }, -}); - -const { - receipt: { transactionHash }, - userOpHash, - success, -} = await wait(); -``` - -### [Send a multi tx and pay gas with a token](https://bcnmy.github.io/biconomy-client-sdk/classes/BiconomySmartAccountV2.html#getTokenFees) - -| Key | Description | -| -------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -| [buildUseropDto](https://bcnmy.github.io/biconomy-client-sdk/types/BuildUserOpOptions.html) | Options for building a userOp | -| [paymasterServiceData](https://bcnmy.github.io/biconomy-client-sdk/types/PaymasterUserOperationDto.html) | PaymasterOptions set in the buildUseropDto | - -```typescript -import { encodeFunctionData, parseAbi } from "viem"; -const preferredToken = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a"; // USDC - -const tx = { - to: nftAddress, - data: encodeFunctionData({ - abi: parseAbi(["function safeMint(address to) public"]), - functionName: "safeMint", - args: ["0x..."], - }), -}; - -const buildUseropDto = { - paymasterServiceData: { - mode: PaymasterMode.ERC20, - preferredToken, - }, -}; - -const { wait } = await smartAccount.sendTransaction( - [tx, tx] /* Mint twice */, - buildUseropDto -); - -const { - receipt: { transactionHash }, - userOpHash, - success, -} = await wait(); -``` +- [Initialise a smartAccount](examples/INITIALISE_A_SMART_CONTRACT.md) +- [send some eth with sponsorship](examples/SEND_SOME_ETH_WITH_SPONSORSHIP.md) +- [send a multi tx and pay gas with an erc20 token](examples/SEND_A_MULTI_TX_AND_PAY_GAS_WITH_TOKEN.md) +- [create and use a session](examples/CREATE_AND_USE_A_SESSION.md) ## License diff --git a/bun.lockb b/bun.lockb index 1784ac93e..0136a98d9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/examples/CREATE_AND_USE_A_BATCH_SESSION.md b/examples/CREATE_AND_USE_A_BATCH_SESSION.md new file mode 100644 index 000000000..584203304 --- /dev/null +++ b/examples/CREATE_AND_USE_A_BATCH_SESSION.md @@ -0,0 +1,138 @@ +### Create and Use a Batch Session + +| Key | Description | +| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| [sessionConfigs](https://bcnmy.github.io/biconomy-client-sdk/interfaces/CreateSessionDataParams.html) | svm criteria | +| [createERC20SessionDatum](https://bcnmy.github.io/biconomy-client-sdk/functions/createERC20SessionDatum.html) | helper that returns erc20 svm data (not recommended) | +| [createABISessionDatum](https://bcnmy.github.io/biconomy-client-sdk/types/createABISessionDatum.html) | helper that returns abi svm data (recommended) | + +```typescript +import { + createSmartAccountClient, + createSession, + createSessionSmartAccountClient, + Rule, + PaymasterMode, + Policy, +} from "@biconomy/account"; +import { createWalletClient, http, createPublicClient } from "viem"; +import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; +import { mainnet as chain } from "viem/chains"; + +const withSponsorship = { + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, +}; + +const account = privateKeyToAccount(generatePrivateKey()); +const signer = createWalletClient({ account, chain, transport: http() }); +const smartAccount = await createSmartAccountClient({ + signer, + bundlerUrl, + paymasterUrl, +}); + +// Retrieve bundler and pymaster urls from dashboard +const smartAccountAddress = await smartAccount.getAccountAddress(); + +// creates a store for the session, and saves the keys to it to be later retrieved +const { sessionKeyAddress, sessionStorageClient } = await createSessionKeyEOA( + smartAccount, + chain +); + +const leaves: CreateSessionParams[] = [ + createERC20SessionDatum({ + interval: { + validUntil: 0, + validAfter: 0, + }, + sessionKeyAddress, + sessionKeyData: encodeAbiParameters( + [ + { type: "address" }, + { type: "address" }, + { type: "address" }, + { type: "uint256" }, + ], + [sessionKeyAddress, token, recipient, amount] + ), + }), + createABISessionDatum({ + interval: { + validUntil: 0, + validAfter: 0, + }, + sessionKeyAddress, + contractAddress: nftAddress, + functionSelector: "safeMint(address)", + rules: [ + { + offset: 0, + condition: 0, + referenceValue: smartAccountAddress, + }, + ], + valueLimit: 0n, + }), +]; + +const { wait, session } = await createBatchSession( + smartAccount, + sessionKeyAddress, + sessionStorageClient, + leaves, + withSponsorship +); + +const { + receipt: { transactionHash }, + success, +} = await wait(); + +const smartAccountWithSession = await createSessionSmartAccountClient( + { + accountAddress: smartAccountAddress, // Set the account address on behalf of the user + bundlerUrl, + paymasterUrl, + chainId, + }, + session, + true // if batching +); + +const transferTx: Transaction = { + to: token, + data: encodeFunctionData({ + abi: parseAbi(["function transfer(address _to, uint256 _value)"]), + functionName: "transfer", + args: [recipient, amount], + }), +}; +const nftMintTx: Transaction = { + to: nftAddress, + data: encodeFunctionData({ + abi: parseAbi(["function safeMint(address _to)"]), + functionName: "safeMint", + args: [smartAccountAddress], + }), +}; + +const txs = [transferTx, nftMintTx]; + +const batchSessionParams = await getBatchSessionTxParams( + ["ERC20", "ABI"], + txs, + session, + chain +); + +const { wait: sessionWait } = await smartAccountWithSession.sendTransaction( + txs, + { + ...batchSessionParams, + ...withSponsorship, + } +); + +const { success } = await sessionWait(); +``` diff --git a/examples/CREATE_AND_USE_A_SESSION.md b/examples/CREATE_AND_USE_A_SESSION.md new file mode 100644 index 000000000..124b039b7 --- /dev/null +++ b/examples/CREATE_AND_USE_A_SESSION.md @@ -0,0 +1,116 @@ +### Create and Use a Session + +| Key | Description | +| ------------------------------------------------------------------------------- | ----------- | +| [sessionConfigs](https://bcnmy.github.io/biconomy-client-sdk/types/Policy.html) | Policy[] | +| [rules](https://bcnmy.github.io/biconomy-client-sdk/types/Policy.html) | Rule[] | + +```typescript +import { + createSmartAccountClient, + createSession, + createSessionSmartAccountClient, + Rule, + Policy, +} from "@biconomy/account"; +import { createWalletClient, http, createPublicClient } from "viem"; +import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; +import { mainnet as chain } from "viem/chains"; + +const account = privateKeyToAccount(generatePrivateKey()); +const signer = createWalletClient({ account, chain, transport: http() }); +const smartAccount = await createSmartAccountClient({ + signer, + bundlerUrl, + paymasterUrl, +}); // Retrieve bundler and pymaster urls from dashboard +const smartAccountAddress = await smartAccount.getAccountAddress(); + +// creates a store for the session, and saves the keys to it to be later retrieved +const { sessionKeyAddress, sessionStorageClient } = await createSessionKeyEOA( + smartAccount, + chain +); + +// The rules that govern the method from the whitelisted contract +const rules: Rule = [ + { + /** The index of the param from the selected contract function upon which the condition will be applied */ + offset: 0, + /** + * Conditions: + * + * 0 - Equal + * 1 - Less than or equal + * 2 - Less than + * 3 - Greater than or equal + * 4 - Greater than + * 5 - Not equal + */ + condition: 0, + /** The value to compare against */ + referenceValue: smartAccountAddress, + }, +]; + +/** The policy is made up of a list of rules applied to the contract method with and interval */ +const policy: Policy[] = [ + { + /** The address of the sessionKey upon which the policy is to be imparted */ + sessionKeyAddress, + /** The address of the contract to be included in the policy */ + contractAddress: nftAddress, + /** The specific function selector from the contract to be included in the policy */ + functionSelector: "safeMint(address)", + /** The list of rules which make up the policy */ + rules, + /** The time interval within which the session is valid */ + interval: { + validUntil: 0, + validAfter: 0, + }, + /** The maximum value that can be transferred in a single transaction */ + valueLimit: 0n, + }, +]; + +const { wait, session } = await createSession( + smartAccount, + policy, + sessionKeyAddress, + sessionStorageClient, + { + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + } +); + +const { + receipt: { transactionHash }, +} = await wait(); + +const smartAccountWithSession = await createSessionSmartAccountClient( + { + accountAddress: smartAccountAddress, // Set the account address on behalf of the user + bundlerUrl, + paymasterUrl, + chainId, + }, + session +); + +const { wait: mintWait } = await smartAccountWithSession.sendTransaction( + { + to: nftAddress, + data: encodeFunctionData({ + abi: parseAbi(["function safeMint(address _to)"]), + functionName: "safeMint", + args: [smartAccountAddress], + }), + }, + { + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + } +); + +const { success } = await mintWait(); +``` diff --git a/examples/INITIALISE_A_SMART_CONTRACT.md b/examples/INITIALISE_A_SMART_CONTRACT.md new file mode 100644 index 000000000..6b036e308 --- /dev/null +++ b/examples/INITIALISE_A_SMART_CONTRACT.md @@ -0,0 +1,23 @@ +### [Initialise a smartAccount](https://bcnmy.github.io/biconomy-client-sdk/functions/createSmartAccountClient.html) + +| Key | Description | +| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| [signer](https://bcnmy.github.io/biconomy-client-sdk/packages/account/docs/interfaces/SmartAccountSigner.html) | This signer will be used for signing userOps for any transactions you build. Will accept ethers.JsonRpcSigner as well as a viemWallet | +| [paymasterUrl](https://dashboard.biconomy.io) | You can pass in a paymasterUrl necessary for sponsoring transactions (retrieved from the biconomy dashboard) | +| [bundlerUrl](https://dashboard.biconomy.io) | You can pass in a bundlerUrl (retrieved from the biconomy dashboard) for sending transactions | + +```typescript +import { createSmartAccountClient } from "@biconomy/account"; +import { createWalletClient, http, createPublicClient } from "viem"; +import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; +import { mainnet as chain } from "viem/chains"; + +const account = privateKeyToAccount(generatePrivateKey()); +const signer = createWalletClient({ account, chain, transport: http() }); + +const smartAccount = await createSmartAccountClient({ + signer, + bundlerUrl, + paymasterUrl, +}); // Retrieve bundler and pymaster urls from dashboard +``` diff --git a/examples/SEND_A_MULTI_TX_AND_PAY_GAS_WITH_TOKEN.md b/examples/SEND_A_MULTI_TX_AND_PAY_GAS_WITH_TOKEN.md new file mode 100644 index 000000000..911f80fc4 --- /dev/null +++ b/examples/SEND_A_MULTI_TX_AND_PAY_GAS_WITH_TOKEN.md @@ -0,0 +1,56 @@ +### [Send a multi tx and pay gas with a token](https://bcnmy.github.io/biconomy-client-sdk/classes/BiconomySmartAccountV2.html#getTokenFees) + +| Key | Description | +| -------------------------------------------------------------------------------------------------------- | ------------------------------------------ | +| [buildUseropDto](https://bcnmy.github.io/biconomy-client-sdk/types/BuildUserOpOptions.html) | Options for building a userOp | +| [paymasterServiceData](https://bcnmy.github.io/biconomy-client-sdk/types/PaymasterUserOperationDto.html) | PaymasterOptions set in the buildUseropDto | + +```typescript +import { + encodeFunctionData, + parseAbi, + createWalletClient, + http, + createPublicClient, +} from "viem"; +import { createSmartAccountClient } from "@biconomy/account"; +import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; +import { mainnet as chain } from "viem/chains"; + +const account = privateKeyToAccount(generatePrivateKey()); +const signer = createWalletClient({ account, chain, transport: http() }); +const smartAccount = await createSmartAccountClient({ + signer, + bundlerUrl, + paymasterUrl, +}); // Retrieve bundler and pymaster urls from dashboard + +const preferredToken = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a"; // USDC + +const tx = { + to: nftAddress, + data: encodeFunctionData({ + abi: parseAbi(["function safeMint(address to) public"]), + functionName: "safeMint", + args: ["0x..."], + }), +}; + +const buildUseropDto = { + paymasterServiceData: { + mode: PaymasterMode.ERC20, + preferredToken, + }, +}; + +const { wait } = await smartAccount.sendTransaction( + [tx, tx] /* Mint twice */, + buildUseropDto +); + +const { + receipt: { transactionHash }, + userOpHash, + success, +} = await wait(); +``` diff --git a/examples/SEND_SOME_ETH_WITH_SPONSORSHIP.md b/examples/SEND_SOME_ETH_WITH_SPONSORSHIP.md new file mode 100644 index 000000000..33d35d323 --- /dev/null +++ b/examples/SEND_SOME_ETH_WITH_SPONSORSHIP.md @@ -0,0 +1,35 @@ +### [Send some eth with sponsorship](https://bcnmy.github.io/biconomy-client-sdk/classes/BiconomySmartAccountV2.html#sendTransaction) + +| Key | Description | +| --------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [oneOrManyTx](https://bcnmy.github.io/biconomy-client-sdk/types/Transaction.html) | Submit multiple or one transactions | +| [userOpReceipt](https://bcnmy.github.io/biconomy-client-sdk/types/UserOpReceipt) | Returned information about your tx, receipts, userOpHashes etc | + +```typescript +import { createSmartAccountClient } from "@biconomy/account"; +import { createWalletClient, http, createPublicClient } from "viem"; +import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; +import { mainnet as chain } from "viem/chains"; + +const account = privateKeyToAccount(generatePrivateKey()); +const signer = createWalletClient({ account, chain, transport: http() }); +const smartAccount = await createSmartAccountClient({ + signer, + bundlerUrl, + paymasterUrl, +}); // Retrieve bundler and paymaster urls from dashboard + +const oneOrManyTx = { to: "0x...", value: 1 }; + +const { wait } = await smartAccount.sendTransaction(oneOrManyTx, { + paymasterServiceData: { + mode: PaymasterMode.SPONSORED, + }, +}); + +const { + receipt: { transactionHash }, + userOpHash, + success, +} = await wait(); +``` diff --git a/package.json b/package.json index f8e03da04..8749ac31d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "sideEffects": false, "name": "@biconomy/account", "author": "Biconomy", - "version": "4.2.0", + "version": "4.4.0", "description": "SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.", "keywords": [ "erc-7579", @@ -51,7 +51,7 @@ "format": "biome format . --write", "lint": "biome check .", "lint:fix": "bun run lint --apply", - "dev": "concurrently \"bun run esm:watch\" \"bun run cjs:watch\" \"bun run esm:watch:aliases\" \"bun run cjs:watch:aliases\"", + "dev": "bun link && concurrently \"bun run esm:watch\" \"bun run cjs:watch\" \"bun run esm:watch:aliases\" \"bun run cjs:watch:aliases\"", "build": "bun run clean && bun run build:cjs && bun run build:esm && bun run build:types", "clean": "rimraf ./dist/_esm ./dist/_cjs ./dist/_types ./dist/tsconfig", "test": "vitest dev -c ./tests/vitest.config.ts", @@ -86,7 +86,9 @@ "@size-limit/preset-small-lib": "^11", "@types/bun": "latest", "@vitest/coverage-v8": "^1.3.1", + "buffer": "^6.0.3", "concurrently": "^8.2.2", + "ethers": "^6.12.0", "gh-pages": "^6.1.1", "rimraf": "^5.0.5", "simple-git-hooks": "^2.9.0", @@ -94,8 +96,7 @@ "tsc-alias": "^1.8.8", "tslib": "^2.6.2", "typedoc": "^0.25.9", - "vitest": "^1.3.1", - "buffer": "^6.0.3" + "vitest": "^1.3.1" }, "peerDependencies": { "typescript": "^5", diff --git a/src/account/BiconomySmartAccountV2.ts b/src/account/BiconomySmartAccountV2.ts index 048a05e68..7ce77c339 100644 --- a/src/account/BiconomySmartAccountV2.ts +++ b/src/account/BiconomySmartAccountV2.ts @@ -97,6 +97,8 @@ import { type UserOperationKey = keyof UserOperationStruct export class BiconomySmartAccountV2 extends BaseSmartContractAccount { + private sessionData?: ModuleInfo + private SENTINEL_MODULE = "0x0000000000000000000000000000000000000001" private index: number @@ -149,6 +151,8 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { DEFAULT_BICONOMY_FACTORY_ADDRESS }) + this.sessionData = biconomySmartAccountConfig.sessionData + this.defaultValidationModule = biconomySmartAccountConfig.defaultValidationModule this.activeValidationModule = @@ -215,7 +219,7 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * * - Docs: https://docs.biconomy.io/Account/integration#integration-1 * - * @param biconomySmartAccountConfig - Configuration for initializing the BiconomySmartAccountV2 instance. + * @param biconomySmartAccountConfig - Configuration for initializing the BiconomySmartAccountV2 instance {@link BiconomySmartAccountV2Config}. * @returns A promise that resolves to a new instance of BiconomySmartAccountV2. * @throws An error if something is wrong with the smart account instance creation. * @@ -877,7 +881,8 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { } // dummy signature depends on the validation module supplied. - async getDummySignatures(params?: ModuleInfo): Promise { + async getDummySignatures(_params?: ModuleInfo): Promise { + const params = { ..._params, ...(this.sessionData ? this.sessionData : {}) } this.isActiveValidationModuleDefined() return (await this.activeValidationModule.getDummySignature(params)) as Hex } @@ -906,8 +911,9 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { async signUserOp( userOp: Partial, - params?: SendUserOpParams + _params?: SendUserOpParams ): Promise { + const params = { ..._params, ...(this.sessionData ? this.sessionData : {}) } this.isActiveValidationModuleDefined() const requiredFields: UserOperationKey[] = [ "sender", diff --git a/src/account/utils/Constants.ts b/src/account/utils/Constants.ts index 320a89ac0..d2bae4697 100644 --- a/src/account/utils/Constants.ts +++ b/src/account/utils/Constants.ts @@ -69,6 +69,7 @@ export const DefaultGasLimit = { } export const ERROR_MESSAGES = { + ACCOUNT_NOT_DEPLOYED: "Account has not yet been deployed", ACCOUNT_ALREADY_DEPLOYED: "Account already deployed", NO_NATIVE_TOKEN_BALANCE_DURING_DEPLOY: "Native token balance is not available during deploy", @@ -81,7 +82,9 @@ export const ERROR_MESSAGES = { NATIVE_TOKEN_WITHDRAWAL_WITHOUT_AMOUNT: "'Amount' is required for withdrawal of native token without using a paymaster", MISSING_RPC_URL: - "rpcUrl is required for PrivateKeyAccount signer type, please provide it in the config" + "rpcUrl is required for PrivateKeyAccount signer type, please provide it in the config", + INVALID_SESSION_TYPES: + "Session types and transactions must be of the same length" } export const NATIVE_TOKEN_ALIAS: Hex = diff --git a/src/account/utils/Logger.ts b/src/account/utils/Logger.ts index c096cc270..7925028c4 100644 --- a/src/account/utils/Logger.ts +++ b/src/account/utils/Logger.ts @@ -12,7 +12,7 @@ class Logger { "BICONOMY_SDK_DEBUG", "REACT_APP_BICONOMY_SDK_DEBUG", "NEXT_PUBLIC_BICONOMY_SDK_DEBUG" - ].some((key) => process.env[key]?.toString() === "true") + ].some((key) => process?.env?.[key]?.toString() === "true") /** * \x1b[0m is an escape sequence to reset the color of the text @@ -21,7 +21,9 @@ class Logger { * warn - Magenta[time] Yellow[WARN]: Cyan[message]: [value] * error - Magenta[time] Red[ERROR]: Cyan[message]: [value] */ - static log(message: string, value = ""): void { + + // biome-ignore lint/suspicious/noExplicitAny: + static log(message: string, value: any = ""): void { const timestamp = new Date().toISOString() const logMessage = `\x1b[35m[${timestamp}]\x1b[0m \x1b[36m${message}\x1b[0m:` @@ -29,8 +31,8 @@ class Logger { console.log(logMessage, value === undefined ? "" : value) } } - - static warn(message: string, value = ""): void { + // biome-ignore lint/suspicious/noExplicitAny: + static warn(message: string, value: any = ""): void { const timestamp = new Date().toISOString() const warnMessage = `\x1b[35m[${timestamp}]\x1b[0m \x1b[33mWARN\x1b[0m: \x1b[36m${message}\x1b[0m` @@ -38,8 +40,8 @@ class Logger { console.warn(warnMessage, value === undefined ? "" : value) } } - - static error(message: string, value = ""): void { + // biome-ignore lint/suspicious/noExplicitAny: + static error(message: string, value: any = ""): void { const timestamp = new Date().toISOString() const errorMessage = `\x1b[35m[${timestamp}]\x1b[0m \x1b[31mERROR\x1b[0m: \x1b[36m${message}\x1b[0m` diff --git a/src/account/utils/Types.ts b/src/account/utils/Types.ts index e28ed8b3a..dda36373d 100644 --- a/src/account/utils/Types.ts +++ b/src/account/utils/Types.ts @@ -13,6 +13,10 @@ import type { } from "viem" import type { IBundler } from "../../bundler" import type { BaseValidationModule, ModuleInfo } from "../../modules" +import type { + ISessionStorage, + SessionLeafNode +} from "../../modules/interfaces/ISessionStorage" import type { FeeQuotesOrDataDto, IPaymaster, @@ -165,6 +169,8 @@ export type BiconomySmartAccountV2ConfigBaseProps = { viemChain?: Chain /** The initial code to be used for the smart account */ initCode?: Hex + /** Used for session key manager module */ + sessionData?: ModuleInfo } export type BiconomySmartAccountV2Config = BiconomySmartAccountV2ConfigBaseProps & @@ -202,6 +208,11 @@ export type BuildUserOpOptions = { useEmptyDeployCallData?: boolean } +export type SessionDataForAccount = { + sessionStorageClient: ISessionStorage + session: SessionLeafNode +} + export type NonceOptions = { /** nonceKey: The key to use for nonce */ nonceKey?: number diff --git a/src/bundler/utils/getAAError.ts b/src/bundler/utils/getAAError.ts index 7a04be10f..2f0425b3c 100644 --- a/src/bundler/utils/getAAError.ts +++ b/src/bundler/utils/getAAError.ts @@ -10,7 +10,8 @@ export type KnownError = { docsUrl?: string } -export const ERRORS_URL = "https://bcnmy.github.io/aa-errors/errors.json" +export const ERRORS_URL = + "https://raw.githubusercontent.com/bcnmy/aa-errors/main/docs/errors.json" export const DOCS_URL = "https://docs.biconomy.io/troubleshooting/commonerrors" const UNKOWN_ERROR_CODE = "520" @@ -19,7 +20,7 @@ const knownErrors: KnownError[] = [] const matchError = (message: string): null | KnownError => knownErrors.find( (knownError: KnownError) => - message.toLowerCase().indexOf(knownError.regex) > -1 + message.toLowerCase().indexOf(knownError.regex.toLowerCase()) > -1 ) ?? null const buildErrorStrings = ( diff --git a/src/modules/SessionKeyManagerModule.ts b/src/modules/SessionKeyManagerModule.ts index c325c4dd5..8d57370b0 100644 --- a/src/modules/SessionKeyManagerModule.ts +++ b/src/modules/SessionKeyManagerModule.ts @@ -19,7 +19,9 @@ import type { SessionSearchParam, SessionStatus } from "./interfaces/ISessionStorage.js" +import { SessionFileStorage } from "./session-storage/SessionFileStorage.js" import { SessionLocalStorage } from "./session-storage/SessionLocalStorage.js" +import { SessionMemoryStorage } from "./session-storage/SessionMemoryStorage.js" import { DEFAULT_SESSION_KEY_MANAGER_MODULE, SESSION_MANAGER_MODULE_ADDRESSES_BY_VERSION @@ -86,6 +88,16 @@ export class SessionKeyManagerModule extends BaseValidationModule { instance.sessionStorageClient = moduleConfig.sessionStorageClient } else { switch (moduleConfig.storageType) { + case StorageType.MEMORY_STORAGE: + instance.sessionStorageClient = new SessionMemoryStorage( + moduleConfig.smartAccountAddress + ) + break + case StorageType.FILE_STORAGE: + instance.sessionStorageClient = new SessionFileStorage( + moduleConfig.smartAccountAddress + ) + break case StorageType.LOCAL_STORAGE: instance.sessionStorageClient = new SessionLocalStorage( moduleConfig.smartAccountAddress diff --git a/src/modules/index.ts b/src/modules/index.ts index bf655585f..b31dc4981 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -9,7 +9,11 @@ export * from "./MultichainValidationModule.js" export * from "./SessionKeyManagerModule.js" export * from "./BatchedSessionRouterModule.js" export * from "./session-validation-modules/ERC20SessionValidationModule.js" - +export * from "./sessions/abi.js" +export * from "./sessions/erc20.js" +export * from "./sessions/batch.js" +export * from "./sessions/sessionSmartAccountClient.js" +export * from "./session-storage/index.js" import { BatchedSessionRouterModule, ECDSAOwnershipValidationModule, @@ -27,5 +31,4 @@ export const createECDSAOwnershipValidationModule = export const createSessionKeyManagerModule = SessionKeyManagerModule.create export const createERC20SessionValidationModule = ERC20SessionValidationModule.create - // export * from './PasskeyValidationModule' diff --git a/src/modules/interfaces/ISessionStorage.ts b/src/modules/interfaces/ISessionStorage.ts index 6372b77e3..a2076e5a5 100644 --- a/src/modules/interfaces/ISessionStorage.ts +++ b/src/modules/interfaces/ISessionStorage.ts @@ -1,4 +1,4 @@ -import type { Hex } from "viem" +import type { Chain, Hex } from "viem" import type { SmartAccountSigner } from "../../account" import type { SignerData } from "../utils/Types.js" @@ -22,6 +22,11 @@ export type SessionSearchParam = { } export interface ISessionStorage { + /** + * The address of the smartAccount + */ + smartAccountAddress: Hex + /** * Adds a session leaf node to the session storage * @param leaf SessionLeafNode to be added to the session storage @@ -54,19 +59,25 @@ export interface ISessionStorage { * If no signer object is passed, it'll create a random signer and add it to the session storage * @param signer Optional signer to be added to the session storage */ - addSigner(_signer?: SignerData): Promise + addSigner(_signer?: SignerData, chain?: Chain): Promise /** * Fetch a signer from the session storage * @param signerPublicKey Public key of the signer to be fetched */ - getSignerByKey(_signerPublicKey: string): Promise + getSignerByKey( + _signerPublicKey: string, + chain: Chain + ): Promise /** * Fetch a signer from the session storage based on the session search param * @param param SessionSearchParam to be used to fetch the signer */ - getSignerBySession(_param: SessionSearchParam): Promise + getSignerBySession( + _param: SessionSearchParam, + chain: Chain + ): Promise /** * Fetch all the session leaf nodes from the session storage based on the session search param. diff --git a/src/modules/session-storage/SessionFileStorage.ts b/src/modules/session-storage/SessionFileStorage.ts index 9dafc67ef..63c33cd6b 100644 --- a/src/modules/session-storage/SessionFileStorage.ts +++ b/src/modules/session-storage/SessionFileStorage.ts @@ -1,11 +1,10 @@ -import { http, type Hex, createWalletClient } from "viem" -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" -import { polygonMumbai } from "viem/chains" +import { http, type Chain, type Hex, createWalletClient } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import { getRandomSigner } from "../.." import { Logger, type SmartAccountSigner, - WalletClientSigner, - getChain + WalletClientSigner } from "../../account" import type { ISessionStorage, @@ -16,10 +15,10 @@ import type { import type { SignerData } from "../utils/Types" export class SessionFileStorage implements ISessionStorage { - private smartAccountAddress: string + public smartAccountAddress: Hex - constructor(smartAccountAddress: string) { - this.smartAccountAddress = smartAccountAddress.toLowerCase() + constructor(smartAccountAddress: Hex) { + this.smartAccountAddress = smartAccountAddress.toLowerCase() as Hex } // This method reads data from the file and returns it in the JSON format @@ -189,38 +188,31 @@ export class SessionFileStorage implements ISessionStorage { await this.writeDataToFile(data, "sessions") // Use 'sessions' as the type } - async addSigner(signerData: SignerData): Promise { + async addSigner( + signerData: SignerData | undefined, + chain: Chain + ): Promise { const signers = await this.getSignerStore() - let signer: SignerData - if (!signerData) { - const pkey = generatePrivateKey() - signer = { - pvKey: pkey, - pbKey: privateKeyToAccount(pkey).publicKey - } - } else { - signer = signerData - } + const signer: SignerData = signerData ?? getRandomSigner() const accountSigner = privateKeyToAccount(signer.pvKey) - const viemChain = getChain(signerData?.chainId?.id || 1) const client = createWalletClient({ account: accountSigner, - chain: signerData.chainId, - transport: http(viemChain.rpcUrls.default.http[0]) + chain, + transport: http() }) const walletClientSigner: SmartAccountSigner = new WalletClientSigner( client, "json-rpc" // signerType ) - signers[this.toLowercaseAddress(accountSigner.address)] = { - pvKey: signer.pvKey, - pbKey: signer.pbKey - } + signers[this.toLowercaseAddress(accountSigner.address)] = signer await this.writeDataToFile(signers, "signers") // Use 'signers' as the type return walletClientSigner } - async getSignerByKey(sessionPublicKey: string): Promise { + async getSignerByKey( + sessionPublicKey: string, + chain: Chain + ): Promise { const signers = await this.getSignerStore() Logger.log("Got signers", signers) @@ -235,20 +227,20 @@ export class SessionFileStorage implements ISessionStorage { const signer = privateKeyToAccount(signerData.pvKey) const walletClient = createWalletClient({ account: signer, - transport: http(polygonMumbai.rpcUrls.default.http[0]) + chain, + transport: http() }) return new WalletClientSigner(walletClient, "json-rpc") } async getSignerBySession( - param: SessionSearchParam + param: SessionSearchParam, + chain: Chain ): Promise { const session = await this.getSessionData(param) Logger.log("got session") - const walletClientSinger = await this.getSignerByKey( - session.sessionPublicKey - ) - return walletClientSinger + const signer = await this.getSignerByKey(session.sessionPublicKey, chain) + return signer } async getAllSessionData( diff --git a/src/modules/session-storage/SessionLocalStorage.ts b/src/modules/session-storage/SessionLocalStorage.ts index 8501c2112..a1cbe76fb 100644 --- a/src/modules/session-storage/SessionLocalStorage.ts +++ b/src/modules/session-storage/SessionLocalStorage.ts @@ -1,7 +1,7 @@ -import { http, type Hex, createWalletClient, toHex } from "viem" -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" -import { mainnet } from "viem/chains" +import { http, type Chain, type Hex, createWalletClient } from "viem" +import { privateKeyToAccount } from "viem/accounts" import { type SmartAccountSigner, WalletClientSigner } from "../../account" +import { getRandomSigner } from "../../index.js" import type { ISessionStorage, SessionLeafNode, @@ -10,11 +10,15 @@ import type { } from "../interfaces/ISessionStorage.js" import type { SignerData } from "../utils/Types.js" +export const supportsLocalStorage = + // @ts-ignore: LocalStorage is not available in node + typeof window !== "undefined" && typeof window.localStorage !== "undefined" + export class SessionLocalStorage implements ISessionStorage { - private smartAccountAddress: string + public smartAccountAddress: Hex - constructor(smartAccountAddress: string) { - this.smartAccountAddress = smartAccountAddress.toLowerCase() + constructor(smartAccountAddress: Hex) { + this.smartAccountAddress = smartAccountAddress.toLowerCase() as Hex } private validateSearchParam(param: SessionSearchParam): void { @@ -36,7 +40,7 @@ export class SessionLocalStorage implements ISessionStorage { return data ? JSON.parse(data) : { merkleRoot: "", leafNodes: [] } } - private getSignerStore() { + private getSignerStore(): Record { // @ts-ignore: LocalStorage is not available in node const data = localStorage.getItem(this.getStorageKey("signers")) return data ? JSON.parse(data) : {} @@ -132,44 +136,41 @@ export class SessionLocalStorage implements ISessionStorage { localStorage.setItem(this.getStorageKey("sessions"), JSON.stringify(data)) } - async addSigner(signerData: SignerData): Promise { + async addSigner( + signerData: SignerData, + chain: Chain + ): Promise { const signers = this.getSignerStore() - let signer: SignerData - if (!signerData) { - const pkey = generatePrivateKey() - signer = { - pvKey: pkey, - pbKey: privateKeyToAccount(pkey).publicKey - } - } else { - signer = signerData - } - const accountSigner = privateKeyToAccount(toHex(signer.pvKey)) + const signer: SignerData = signerData ?? getRandomSigner() + const accountSigner = privateKeyToAccount(signer.pvKey) const client = createWalletClient({ account: accountSigner, - chain: signerData.chainId, + chain, transport: http() }) const walletClientSigner = new WalletClientSigner( client, "json-rpc" // signerType ) - signers[this.toLowercaseAddress(accountSigner.address)] = signerData + signers[this.toLowercaseAddress(accountSigner.address)] = signer // @ts-ignore: LocalStorage is not available in node localStorage.setItem(this.getStorageKey("signers"), JSON.stringify(signers)) return walletClientSigner } - async getSignerByKey(sessionPublicKey: string): Promise { + async getSignerByKey( + sessionPublicKey: string, + chain: Chain + ): Promise { const signers = this.getSignerStore() const signerData = signers[this.toLowercaseAddress(sessionPublicKey)] if (!signerData) { throw new Error("Signer not found.") } - const account = privateKeyToAccount(signerData.privateKey) + const account = privateKeyToAccount(signerData.pvKey) const client = createWalletClient({ account, - chain: mainnet, + chain, transport: http() }) const signer = new WalletClientSigner(client, "viem") @@ -177,10 +178,11 @@ export class SessionLocalStorage implements ISessionStorage { } async getSignerBySession( - param: SessionSearchParam + param: SessionSearchParam, + chain: Chain ): Promise { const session = await this.getSessionData(param) - return this.getSignerByKey(session.sessionPublicKey) + return this.getSignerByKey(session.sessionPublicKey, chain) } async getAllSessionData( diff --git a/src/modules/session-storage/SessionMemoryStorage.ts b/src/modules/session-storage/SessionMemoryStorage.ts index b046ee4fb..670f14297 100644 --- a/src/modules/session-storage/SessionMemoryStorage.ts +++ b/src/modules/session-storage/SessionMemoryStorage.ts @@ -1,7 +1,7 @@ -import { http, type Hex, createWalletClient } from "viem" -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" -import { mainnet } from "viem/chains" +import { http, type Chain, type Hex, createWalletClient } from "viem" +import { privateKeyToAccount } from "viem/accounts" import { type SmartAccountSigner, WalletClientSigner } from "../../account" +import { getRandomSigner } from "../../index.js" import type { ISessionStorage, SessionLeafNode, @@ -26,10 +26,10 @@ const memoryStorage: MemoryStore = { } export class SessionMemoryStorage implements ISessionStorage { - private smartAccountAddress: string + public smartAccountAddress: Hex - constructor(smartAccountAddress: string) { - this.smartAccountAddress = smartAccountAddress.toLowerCase() + constructor(smartAccountAddress: Hex) { + this.smartAccountAddress = smartAccountAddress.toLowerCase() as Hex } private validateSearchParam(param: SessionSearchParam): void { @@ -51,7 +51,7 @@ export class SessionMemoryStorage implements ISessionStorage { return data ? JSON.parse(data) : { merkleRoot: "", leafNodes: [] } } - private getSignerStore() { + public getSignerStore(): Record { const data = memoryStorage.getItem(this.getStorageKey("signers")) return data ? JSON.parse(data) : {} } @@ -143,29 +143,23 @@ export class SessionMemoryStorage implements ISessionStorage { memoryStorage.setItem(this.getStorageKey("sessions"), JSON.stringify(data)) } - async addSigner(signerData: SignerData): Promise { + async addSigner( + signerData: SignerData, + chain: Chain + ): Promise { const signers = this.getSignerStore() - let signer: SignerData - if (!signerData) { - const pkey = generatePrivateKey() - signer = { - pvKey: pkey, - pbKey: privateKeyToAccount(pkey).publicKey - } - } else { - signer = signerData - } + const signer: SignerData = signerData ?? getRandomSigner() const accountSigner = privateKeyToAccount(signer.pvKey) const client = createWalletClient({ account: accountSigner, - chain: signerData.chainId, + chain, transport: http() }) const walletClientSigner = new WalletClientSigner( client, "json-rpc" // signerType ) - signers[this.toLowercaseAddress(accountSigner.address)] = signerData + signers[this.toLowercaseAddress(accountSigner.address)] = signer memoryStorage.setItem( this.getStorageKey("signers"), JSON.stringify(signers) @@ -173,16 +167,19 @@ export class SessionMemoryStorage implements ISessionStorage { return walletClientSigner } - async getSignerByKey(sessionPublicKey: string): Promise { + async getSignerByKey( + sessionPublicKey: string, + chain: Chain + ): Promise { const signers = this.getSignerStore() const signerData = signers[this.toLowercaseAddress(sessionPublicKey)] if (!signerData) { throw new Error("Signer not found.") } - const account = privateKeyToAccount(signerData.privateKey) + const account = privateKeyToAccount(signerData.pvKey) const client = createWalletClient({ account, - chain: mainnet, + chain, transport: http() }) const signer = new WalletClientSigner(client, "viem") @@ -190,10 +187,11 @@ export class SessionMemoryStorage implements ISessionStorage { } async getSignerBySession( - param: SessionSearchParam + param: SessionSearchParam, + chain: Chain ): Promise { const session = await this.getSessionData(param) - return this.getSignerByKey(session.sessionPublicKey) + return this.getSignerByKey(session.sessionPublicKey, chain) } async getAllSessionData( diff --git a/src/modules/session-storage/index.ts b/src/modules/session-storage/index.ts new file mode 100644 index 000000000..05f1b359d --- /dev/null +++ b/src/modules/session-storage/index.ts @@ -0,0 +1,4 @@ +export { SessionFileStorage } from "./SessionFileStorage.js" +export { SessionLocalStorage } from "./SessionLocalStorage.js" +export { SessionMemoryStorage } from "./SessionMemoryStorage.js" +export * from "./utils.js" diff --git a/src/modules/session-storage/utils.ts b/src/modules/session-storage/utils.ts new file mode 100644 index 000000000..196821f72 --- /dev/null +++ b/src/modules/session-storage/utils.ts @@ -0,0 +1,39 @@ +import type { Chain, Hex } from "viem" +import { SessionFileStorage, SessionLocalStorage } from "../.." +import type { BiconomySmartAccountV2, SmartAccountSigner } from "../../account" +import type { ISessionStorage } from "../interfaces/ISessionStorage" +import { supportsLocalStorage } from "./SessionLocalStorage" + +export type SessionStoragePayload = { + sessionKeyAddress: Hex + signer: SmartAccountSigner + sessionStorageClient: ISessionStorage +} + +/** + * createSessionKeyEOA + * + * This function is used to store a new session key in the session storage. + * If the session storage client is not provided as the third argument, it will create a new session storage client based on the environment. + * When localStorage is supported, it will return SessionLocalStorage, otherwise it will assume you are in a backend and use SessionFileStorage. + * + * @param smartAccount: BiconomySmartAccountV2 + * @param chain: Chain + * @param _sessionStorageClient: ISessionStorage + * @returns + */ +export const createSessionKeyEOA = async ( + smartAccount: BiconomySmartAccountV2, + chain: Chain, + _sessionStorageClient?: ISessionStorage +): Promise => { + const userAccountAddress = await smartAccount.getAddress() + const sessionStorageClient = + _sessionStorageClient ?? + new (supportsLocalStorage ? SessionLocalStorage : SessionFileStorage)( + userAccountAddress + ) + const newSigner = await sessionStorageClient.addSigner(undefined, chain) + const sessionKeyAddress = await newSigner.getAddress() + return { sessionKeyAddress, signer: newSigner, sessionStorageClient } +} diff --git a/src/modules/sessions/abi.ts b/src/modules/sessions/abi.ts new file mode 100644 index 000000000..d65beb785 --- /dev/null +++ b/src/modules/sessions/abi.ts @@ -0,0 +1,322 @@ +import { + type AbiFunction, + type ByteArray, + type Hex, + concat, + isAddress, + pad, + slice, + toFunctionSelector, + toHex +} from "viem" +import type { + CreateSessionDataParams, + Permission, + UserOpResponse +} from "../../" +import { + type BiconomySmartAccountV2, + type BuildUserOpOptions, + ERROR_MESSAGES, + type Transaction +} from "../../account" +import { createSessionKeyManagerModule } from "../index" +import type { ISessionStorage } from "../interfaces/ISessionStorage" +import { + DEFAULT_ABI_SVM_MODULE, + DEFAULT_SESSION_KEY_MANAGER_MODULE +} from "../utils/Constants" +import type { DeprecatedPermission, Rule } from "../utils/Helper" + +export type SessionConfig = { + usersAccountAddress: Hex + smartAccount: BiconomySmartAccountV2 +} + +export type Session = { + /** The storage client specific to the smartAccountAddress which stores the session keys */ + sessionStorageClient: ISessionStorage + /** The relevant sessionID for the chosen session */ + sessionID: string +} + +export type SessionEpoch = { + /** The time at which the session is no longer valid */ + validUntil?: number + /** The time at which the session becomes valid */ + validAfter?: number +} + +export type Policy = { + /** The address of the contract to be included in the policy */ + contractAddress: Hex + /** The address of the sessionKey upon which the policy is to be imparted */ + sessionKeyAddress: Hex + /** The specific function selector from the contract to be included in the policy */ + functionSelector: string | AbiFunction + /** The rules to be included in the policy */ + rules: Rule[] + /** The time interval within which the session is valid. If left unset the session will remain invalid indefinitely */ + interval?: SessionEpoch + /** The maximum value that can be transferred in a single transaction */ + valueLimit: bigint +} + +export type SessionGrantedPayload = UserOpResponse & { session: Session } + +/** + * + * createSession + * + * Creates a session for a user's smart account. + * This grants a dapp permission to execute a specific function on a specific contract on behalf of a user. + * Permissions can be specified by the dapp in the form of rules{@link Rule}, and then submitted to the user for approval via signing. + * The session keys granted with the imparted policy are stored in a StorageClient {@link ISessionStorage}. They can later be retrieved and used to validate userops. + * + * @param smartAccount - The user's {@link BiconomySmartAccountV2} smartAccount instance. + * @param sessionKeyAddress - The address of the sessionKey upon which the policy is to be imparted. + * @param policy - An array of session configurations {@link Policy}. + * @param sessionStorageClient - The storage client to store the session keys. {@link ISessionStorage} + * @param buildUseropDto - Optional. {@link BuildUserOpOptions} + * @returns Promise<{@link SessionGrantedPayload}> - An object containing the status of the transaction and the sessionID. + * + * @example + * + * ```typescript + * import { createClient } from "viem" + * import { createSmartAccountClient } from "@biconomy/account" + * import { createWalletClient, http } from "viem"; + * import { polygonAmoy } from "viem/chains"; + * + * const signer = createWalletClient({ + * account, + * chain: polygonAmoy, + * transport: http(), + * }); + * + * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl, paymasterUrl }); // Retrieve bundler/paymaster url from dashboard + * const smartAccountAddress = await smartAccount.getAccountAddress(); + * const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e" + * const sessionStorage = new SessionFileStorage(smartAccountAddress) + * const sessionKeyAddress = (await sessionStorage.addSigner(undefined, polygonAmoy)).getAddress(); + * + * const { wait, sessionID } = await createSession( + * smartAccount, + * [ + * { + * sessionKeyAddress, + * contractAddress: nftAddress, + * functionSelector: "safeMint(address)", + * rules: [ + * { + * offset: 0, + * condition: 0, + * referenceValue: smartAccountAddress + * } + * ], + * interval: { + * validUntil: 0, + * validAfter: 0 + * }, + * valueLimit: 0n + * } + * ], + * sessionKeyAddress, + * sessionStorage, + * { + * paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + * } + * ) + * + * const { + * receipt: { transactionHash }, + * success + * } = await wait(); + * + * console.log({ sessionID, success }); // Use the sessionID later to retrieve the sessionKey from the storage client + * ``` + */ +export const createSession = async ( + smartAccount: BiconomySmartAccountV2, + policy: Policy[], + sessionKeyAddress: Hex, + sessionStorageClient: ISessionStorage, + buildUseropDto?: BuildUserOpOptions +): Promise => { + const userAccountAddress = await smartAccount.getAddress() + const sessionsModule = await createSessionKeyManagerModule({ + smartAccountAddress: userAccountAddress, + sessionStorageClient + }) + + const { data: policyData } = await sessionsModule.createSessionData( + policy.map(createABISessionDatum) + ) + + const permitTx = { + to: DEFAULT_SESSION_KEY_MANAGER_MODULE, + data: policyData + } + + const txs: Transaction[] = [] + + const isDeployed = await smartAccount.isAccountDeployed() + if (!isDeployed) throw new Error(ERROR_MESSAGES.ACCOUNT_NOT_DEPLOYED) + + const enabled = await smartAccount.isModuleEnabled( + DEFAULT_SESSION_KEY_MANAGER_MODULE + ) + + if (!enabled) { + txs.push( + await smartAccount.getEnableModuleData(DEFAULT_SESSION_KEY_MANAGER_MODULE) + ) + } + txs.push(permitTx) + + const userOpResponse = await smartAccount.sendTransaction(txs, buildUseropDto) + + const sessionID = + ( + await sessionStorageClient.getSessionData({ + sessionPublicKey: sessionKeyAddress, + sessionValidationModule: DEFAULT_ABI_SVM_MODULE + }) + ).sessionID ?? "" + + return { + session: { + sessionStorageClient, + sessionID + }, + ...userOpResponse + } +} + +export type CreateSessionDatumParams = { + interval?: SessionEpoch + sessionKeyAddress: Hex + contractAddress: Hex + functionSelector: string | AbiFunction + rules: Rule[] + valueLimit: bigint +} + +/** + * + * createABISessionDatum + * + * Used to create a session datum for the ABI Session Validation Module. + * It can also be used to create a session datum for batchSession mode. + * + * @param createSessionDataParams - {@link CreateSessionDatumParams} + * @returns {@link CreateSessionDataParams} + */ +export const createABISessionDatum = ({ + /** The time interval within which the session is valid. If left unset the session will remain invalid indefinitely {@link SessionEpoch} */ + interval, + /** The sessionKeyAddress upon which the policy is to be imparted. Used as a reference to the stored session keys */ + sessionKeyAddress, + /** The address of the contract to be included in the policy */ + contractAddress, + /** The specific function selector from the contract to be included in the policy */ + functionSelector, + /** The rules to be included in the policy */ + rules, + /** The maximum value that can be transferred in a single transaction */ + valueLimit +}: CreateSessionDatumParams): CreateSessionDataParams => { + const { validUntil = 0, validAfter = 0 } = interval ?? {} + return { + validUntil, + validAfter, + sessionValidationModule: DEFAULT_ABI_SVM_MODULE, + sessionPublicKey: sessionKeyAddress, + sessionKeyData: getSessionDatum(sessionKeyAddress, { + destContract: contractAddress, + functionSelector: slice(toFunctionSelector(functionSelector), 0, 4), + valueLimit, + rules + }) + } +} + +/** + * @deprecated + */ +export async function getABISVMSessionKeyData( + sessionKey: `0x${string}` | Uint8Array, + permission: DeprecatedPermission +): Promise<`0x${string}` | Uint8Array> { + let sessionKeyData = concat([ + sessionKey, + permission.destContract, + permission.functionSelector, + pad(toHex(permission.valueLimit), { size: 16 }), + pad(toHex(permission.rules.length), { size: 2 }) // this can't be more 2**11 (see below), so uint16 (2 bytes) is enough + ]) as `0x${string}` + + for (let i = 0; i < permission.rules.length; i++) { + sessionKeyData = concat([ + sessionKeyData, + pad(toHex(permission.rules[i].offset), { size: 2 }), // offset is uint16, so there can't be more than 2**16/32 args = 2**11 + pad(toHex(permission.rules[i].condition), { size: 1 }), // uint8 + permission.rules[i].referenceValue + ]) + } + return sessionKeyData +} + +export function getSessionDatum( + sessionKeyAddress: Hex, + permission: Permission +): Hex { + let sessionKeyData = concat([ + sessionKeyAddress, + permission.destContract, + permission.functionSelector, + pad(toHex(permission.valueLimit), { size: 16 }), + pad(toHex(permission.rules.length), { size: 2 }) // this can't be more 2**11 (see below), so uint16 (2 bytes) is enough + ]) as Hex + + for (let i = 0; i < permission.rules.length; i++) { + sessionKeyData = concat([ + sessionKeyData, + pad(toHex(permission.rules[i].offset), { size: 2 }), // offset is uint16, so there can't be more than 2**16/32 args = 2**11 + pad(toHex(permission.rules[i].condition), { size: 1 }), // uint8 + parseReferenceValue(permission.rules[i].referenceValue) + ]) as Hex + } + return sessionKeyData +} + +type HardcodedReference = { + raw: Hex +} +type BaseReferenceValue = string | number | bigint | boolean | ByteArray +type AnyReferenceValue = BaseReferenceValue | HardcodedReference + +/** + * + * parseReferenceValue + * + * Parses the reference value to a hex string. + * The reference value can be hardcoded using the {@link HardcodedReference} type. + * Otherwise, it can be a string, number, bigint, boolean, or ByteArray. + * + * @param referenceValue {@link AnyReferenceValue} + * @returns Hex + */ +export function parseReferenceValue(referenceValue: AnyReferenceValue): Hex { + try { + if ((referenceValue as HardcodedReference)?.raw) { + return (referenceValue as HardcodedReference)?.raw + } + if (isAddress(referenceValue as string)) { + return pad(referenceValue as Hex, { size: 32 }) + } + return toHex(referenceValue as BaseReferenceValue) + } catch (e) { + return toHex(referenceValue as BaseReferenceValue) + } +} diff --git a/src/modules/sessions/batch.ts b/src/modules/sessions/batch.ts new file mode 100644 index 000000000..feef5f063 --- /dev/null +++ b/src/modules/sessions/batch.ts @@ -0,0 +1,232 @@ +import type { Chain, Hex } from "viem" +import { + type CreateSessionDataParams, + DEFAULT_ABI_SVM_MODULE, + DEFAULT_BATCHED_SESSION_ROUTER_MODULE, + DEFAULT_SESSION_KEY_MANAGER_MODULE, + MODULE_ADDRESSES, + type Session, + type SessionGrantedPayload, + type SessionParams, + createBatchedSessionRouterModule, + createSessionKeyManagerModule +} from ".." +import { + type BiconomySmartAccountV2, + type BuildUserOpOptions, + ERROR_MESSAGES, + type Transaction +} from "../../account" +import type { ISessionStorage } from "../interfaces/ISessionStorage" + +export type CreateBatchSessionConfig = { + /** The storage client to be used for storing the session data */ + sessionStorageClient: ISessionStorage + /** An array of session configurations */ + leaves: CreateSessionDataParams[] +} + +/** + * + * createBatchSession + * + * Creates a session manager that handles multiple sessions at once for a given user's smart account. + * Useful for handling multiple granted sessions at once. + * + * @param smartAccount - The user's {@link BiconomySmartAccountV2} smartAccount instance. + * @param sessionKeyAddress - The address of the sessionKey upon which the policy is to be imparted. + * @param batchSessionConfig - An array of session configurations {@link CreateBatchSessionConfig}. + * @param buildUseropDto - Optional. {@link BuildUserOpOptions} + * @returns Promise<{@link SessionGrantedPayload}> - An object containing the status of the transaction and the sessionID. + * + * @example + * + * ```typescript + * import { createClient } from "viem" + * import { createSmartAccountClient } from "@biconomy/account" + * import { createWalletClient, http } from "viem"; + * import { polygonAmoy } from "viem/chains"; + * + * const signer = createWalletClient({ + * account, + * chain: polygonAmoy, + * transport: http(), + * }); + * + * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl, paymasterUrl }); // Retrieve bundler/paymaster url from dashboard + * const smartAccountAddress = await smartAccount.getAccountAddress(); + * const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e" + * const sessionStorage = new SessionFileStorage(smartAccountAddress); + * const sessionKeyAddress = (await sessionStorage.addSigner(undefined, polygonAmoy)).getAddress(); + * + * const leaves: CreateSessionDataParams[] = [ + * createERC20SessionDatum({ + * interval: { + * validUntil: 0, + * validAfter: 0 + * }, + * sessionKeyAddress, + * sessionKeyData: encodeAbiParameters( + * [ + * { type: "address" }, + * { type: "address" }, + * { type: "address" }, + * { type: "uint256" } + * ], + * [sessionKeyAddress, token, recipient, amount] + * ) + * }), + * createABISessionDatum({ + * interval: { + * validUntil: 0, + * validAfter: 0 + * }, + * sessionKeyAddress, + * contractAddress: nftAddress, + * functionSelector: "safeMint(address)", + * rules: [ + * { + * offset: 0, + * condition: 0, + * referenceValue: smartAccountAddress + * } + * ], + * valueLimit: 0n + * }) + * ] + * + * const { wait, sessionID } = await createBatchSession( + * smartAccount, + * sessionKeyAddress, + * sessionStorageClient: sessionStorage, + * leaves, + * { + * paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + * } + * ) + * + * const { + * receipt: { transactionHash }, + * success + * } = await wait(); + * + * console.log({ sessionID, success }); // Use the sessionID later to retrieve the sessionKey from the storage client + * + * ``` + */ + +export const createBatchSession = async ( + smartAccount: BiconomySmartAccountV2, + sessionKeyAddress: Hex, + /** The storage client to be used for storing the session data */ + sessionStorageClient: ISessionStorage, + /** An array of session configurations */ + leaves: CreateSessionDataParams[], + buildUseropDto?: BuildUserOpOptions +): Promise => { + const userAccountAddress = await smartAccount.getAddress() + + const sessionsModule = await createSessionKeyManagerModule({ + smartAccountAddress: userAccountAddress, + sessionStorageClient + }) + + // Create batched session module + const batchedSessionModule = await createBatchedSessionRouterModule({ + smartAccountAddress: userAccountAddress, + sessionKeyManagerModule: sessionsModule + }) + + const { data: policyData } = + await batchedSessionModule.createSessionData(leaves) + + const permitTx = { + to: DEFAULT_SESSION_KEY_MANAGER_MODULE, + data: policyData + } + + const isDeployed = await smartAccount.isAccountDeployed() + if (!isDeployed) throw new Error(ERROR_MESSAGES.ACCOUNT_NOT_DEPLOYED) + + const txs: Transaction[] = [] + const [isSessionModuleEnabled, isBatchedSessionModuleEnabled] = + await Promise.all([ + smartAccount.isModuleEnabled(DEFAULT_SESSION_KEY_MANAGER_MODULE), + smartAccount.isModuleEnabled(DEFAULT_BATCHED_SESSION_ROUTER_MODULE) + ]) + + if (!isSessionModuleEnabled) { + txs.push( + await smartAccount.getEnableModuleData(DEFAULT_SESSION_KEY_MANAGER_MODULE) + ) + } + if (!isBatchedSessionModuleEnabled) { + txs.push( + await smartAccount.getEnableModuleData( + DEFAULT_BATCHED_SESSION_ROUTER_MODULE + ) + ) + } + txs.push(permitTx) + + const userOpResponse = await smartAccount.sendTransaction(txs, buildUseropDto) + + const sessionID = + ( + await sessionStorageClient.getSessionData({ + sessionPublicKey: sessionKeyAddress, + sessionValidationModule: DEFAULT_ABI_SVM_MODULE + }) + ).sessionID ?? "" + + return { + session: { + sessionStorageClient, + sessionID + }, + ...userOpResponse + } +} + +const types = ["ERC20", "ABI"] as const +export type BatchSessionParamsPayload = { + params: { batchSessionParams: SessionParams[] } +} +export type SessionValidationType = (typeof types)[number] +/** + * getBatchSessionTxParams + * + * Retrieves the transaction parameters for a batched session. + * + * @param sessionTypes - An array of session types. + * @param transactions - An array of {@link Transaction}s. + * @param session - {@link Session}. + * @param chain - The chain. + * @returns Promise<{@link BatchSessionParamsPayload}> - session parameters. + * + */ +export const getBatchSessionTxParams = async ( + sessionValidationTypes: SessionValidationType[], + transactions: Transaction[], + { sessionID, sessionStorageClient }: Session, + chain: Chain +): Promise => { + if (sessionValidationTypes.length !== transactions.length) { + throw new Error(ERROR_MESSAGES.INVALID_SESSION_TYPES) + } + const sessionSigner = await sessionStorageClient.getSignerBySession( + { + sessionID + }, + chain + ) + + return { + params: { + batchSessionParams: sessionValidationTypes.map((sessionType) => ({ + sessionSigner, + sessionValidationModule: MODULE_ADDRESSES[sessionType] + })) + } + } +} diff --git a/src/modules/sessions/erc20.ts b/src/modules/sessions/erc20.ts new file mode 100644 index 000000000..eac4ff675 --- /dev/null +++ b/src/modules/sessions/erc20.ts @@ -0,0 +1,32 @@ +import type { EncodeAbiParametersReturnType, Hex } from "viem" +import type { SessionEpoch } from ".." +import { DEFAULT_ERC20_MODULE } from "../utils/Constants" +import type { CreateSessionDataParams } from "../utils/Types" + +export type CreateERC20SessionConfig = { + interval: SessionEpoch + sessionKeyAddress: Hex + sessionKeyData: EncodeAbiParametersReturnType +} +/** + * + * @param erc20SessionConfig {@link CreateERC20SessionConfig} + * @returns {@link CreateSessionDataParams} + */ +export const createERC20SessionDatum = ({ + /** The time interval within which the session is valid. If left unset the session will remain invalid indefinitely {@link SessionEpoch} */ + interval, + /** The sessionKeyAddress upon which the policy is to be imparted. Used as a reference to the stored session keys */ + sessionKeyAddress, + /** The sessionKeyData to be included in the policy {@link EncodeAbiParametersReturnType}*/ + sessionKeyData +}: CreateERC20SessionConfig): CreateSessionDataParams => { + const { validUntil = 0, validAfter = 0 } = interval ?? {} + return { + validUntil, + validAfter, + sessionValidationModule: DEFAULT_ERC20_MODULE, + sessionPublicKey: sessionKeyAddress, + sessionKeyData + } +} diff --git a/src/modules/sessions/sessionSmartAccountClient.ts b/src/modules/sessions/sessionSmartAccountClient.ts new file mode 100644 index 000000000..aa3155515 --- /dev/null +++ b/src/modules/sessions/sessionSmartAccountClient.ts @@ -0,0 +1,119 @@ +import { http, type Hex, createWalletClient } from "viem" +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { + createBatchedSessionRouterModule, + createSessionKeyManagerModule +} from ".." +import { + type BiconomySmartAccountV2, + type BiconomySmartAccountV2Config, + createSmartAccountClient, + getChain +} from "../../account" +import type { ModuleInfo } from "../utils/Types" +import type { Session } from "./abi" + +export type ImpersonatedSmartAccountConfig = Omit< + BiconomySmartAccountV2Config, + "signer" +> & { + accountAddress: Hex + chainId: number + bundlerUrl: string +} +/** + * + * createSessionSmartAccountClient + * + * Creates a new instance of BiconomySmartAccountV2 class. This is used to impersonate a users smart account by a dapp, for use + * with a valid session that has previously been granted by the user. A dummy signer is passed into the smart account instance, which cannot be used. + * The sessionSigner is used instead for signing transactions, which is fetched from the session storage using the sessionID. {@link ISessionStorage} + * + * @param biconomySmartAccountConfig - Configuration for initializing the BiconomySmartAccountV2 instance {@link ImpersonatedSmartAccountConfig}. + * @returns A promise that resolves to a new instance of {@link BiconomySmartAccountV2}. + * @throws An error if something is wrong with the smart account instance creation. + * + * @example + * import { createClient } from "viem" + * import { createSmartAccountClient, BiconomySmartAccountV2 } from "@biconomy/account" + * import { createWalletClient, http } from "viem"; + * import { polygonAmoy } from "viem/chains"; + * + * const signer = createWalletClient({ + * account, + * chain: polygonAmoy, + * transport: http(), + * }); + * + * + * // The following fields are required to create a session smart account client + * const smartAccountAddress = '0x...'; + * const sessionStorage = new SessionFileStorage(smartAccountAddress); + * const sessionKeyAddress = '0x...'; + * const sessionID = '0x...'; + * + * const smartAccountWithSession = await createSessionSmartAccountClient( + * { + * accountAddress: smartAccountAddress, // Set the account address on behalf of the user + * bundlerUrl, + * paymasterUrl, + * chainId + * }, + * { + * sessionStorageClient: storeForSingleSession, + * sessionID + * } + * ) + * + * // The smartAccountWithSession instance can now be used to interact with the blockchain on behalf of the user in the same manner as a regular smart account instance. + * // smartAccountWithSession.sendTransaction(...) etc. + * + */ +export const createSessionSmartAccountClient = async ( + biconomySmartAccountConfig: ImpersonatedSmartAccountConfig, + { sessionStorageClient, sessionID }: Session, + multiMode = false +): Promise => { + const account = privateKeyToAccount(generatePrivateKey()) + + const chain = + biconomySmartAccountConfig.viemChain ?? + getChain(biconomySmartAccountConfig.chainId) + + const incompatibleSigner = createWalletClient({ + account, + chain, + transport: http() + }) + + const sessionSigner = await sessionStorageClient.getSignerBySession( + { + sessionID + }, + chain + ) + + const sessionData: ModuleInfo | undefined = multiMode + ? undefined + : { + sessionID, + sessionSigner + } + + const sessionModule = await createSessionKeyManagerModule({ + smartAccountAddress: biconomySmartAccountConfig.accountAddress, + sessionStorageClient + }) + + const batchedSessionModule = await createBatchedSessionRouterModule({ + smartAccountAddress: biconomySmartAccountConfig.accountAddress, + sessionKeyManagerModule: sessionModule + }) + + return await createSmartAccountClient({ + ...biconomySmartAccountConfig, + signer: incompatibleSigner, // This is a dummy signer, it will remain unused + activeValidationModule: multiMode ? batchedSessionModule : sessionModule, + sessionData // contains the sessionSigner that will be used for txs + }) +} diff --git a/src/modules/utils/Constants.ts b/src/modules/utils/Constants.ts index 4c8c34192..93d11684b 100644 --- a/src/modules/utils/Constants.ts +++ b/src/modules/utils/Constants.ts @@ -1,16 +1,17 @@ +import type { Hex } from "viem" +import type { SessionValidationType } from "../../index.js" import type { ModuleVersion } from "./Types.js" export const DEFAULT_MODULE_VERSION: ModuleVersion = "V1_0_0" -// Note: we could append these defaults with ADDRESS suffix -export const DEFAULT_ECDSA_OWNERSHIP_MODULE = +export const DEFAULT_ECDSA_OWNERSHIP_MODULE: Hex = "0x0000001c5b32F37F5beA87BDD5374eB2aC54eA8e" export const ECDSA_OWNERSHIP_MODULE_ADDRESSES_BY_VERSION = { V1_0_0: "0x0000001c5b32F37F5beA87BDD5374eB2aC54eA8e" } -export const DEFAULT_SESSION_KEY_MANAGER_MODULE = +export const DEFAULT_SESSION_KEY_MANAGER_MODULE: Hex = "0x000002FbFfedd9B33F4E7156F2DE8D48945E7489" export const SESSION_MANAGER_MODULE_ADDRESSES_BY_VERSION = { @@ -18,18 +19,27 @@ export const SESSION_MANAGER_MODULE_ADDRESSES_BY_VERSION = { V1_0_1: "0x000002FbFfedd9B33F4E7156F2DE8D48945E7489" } -export const DEFAULT_BATCHED_SESSION_ROUTER_MODULE = +export const DEFAULT_BATCHED_SESSION_ROUTER_MODULE: Hex = "0x00000D09967410f8C76752A104c9848b57ebba55" export const BATCHED_SESSION_ROUTER_MODULE_ADDRESSES_BY_VERSION = { V1_0_0: "0x00000D09967410f8C76752A104c9848b57ebba55" } -export const DEFAULT_ERC20_MODULE = "0x000000D50C68705bd6897B2d17c7de32FB519fDA" +export const DEFAULT_ERC20_MODULE: Hex = + "0x000000D50C68705bd6897B2d17c7de32FB519fDA" -export const DEFAULT_MULTICHAIN_MODULE = +export const DEFAULT_MULTICHAIN_MODULE: Hex = "0x000000824dc138db84FD9109fc154bdad332Aa8E" export const MULTICHAIN_VALIDATION_MODULE_ADDRESSES_BY_VERSION = { V1_0_0: "0x000000824dc138db84FD9109fc154bdad332Aa8E" } + +export const DEFAULT_ABI_SVM_MODULE: Hex = + "0x000006bC2eCdAe38113929293d241Cf252D91861" + +export const MODULE_ADDRESSES: Record = { + ERC20: DEFAULT_ERC20_MODULE, + ABI: DEFAULT_ABI_SVM_MODULE +} diff --git a/src/modules/utils/Helper.ts b/src/modules/utils/Helper.ts index aa125e759..4f79a1e2b 100644 --- a/src/modules/utils/Helper.ts +++ b/src/modules/utils/Helper.ts @@ -1,24 +1,59 @@ import { + type ByteArray, + type Chain, type Hex, - concat, encodeAbiParameters, keccak256, - pad, - parseAbiParameters, - toHex + parseAbiParameters } from "viem" -import type { UserOperationStruct } from "../../account" +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import type { ChainInfo, SignerData } from "../.." +import { type UserOperationStruct, getChain } from "../../account" export interface Rule { + /** The index of the param from the selected contract function upon which the condition will be applied */ offset: number + /** + * Conditions: + * + * 0 - Equal + * 1 - Less than or equal + * 2 - Less than + * 3 - Greater than or equal + * 4 - Greater than + * 5 - Not equal + */ condition: number - referenceValue: `0x${string}` + /** The value to compare against */ + referenceValue: string | number | bigint | boolean | ByteArray +} + +/** + * @deprecated + */ +export interface DeprecatedRule { + offset: number + condition: number + referenceValue: Hex +} +/** + * @deprecated + */ +export interface DeprecatedPermission { + destContract: `0x${string}` + functionSelector: `0x${string}` + valueLimit: bigint + rules: DeprecatedRule[] } export interface Permission { + /** The address of the contract to which the permission applies */ destContract: `0x${string}` + /** The function selector of the contract to which the permission applies */ functionSelector: `0x${string}` + /** The maximum value that can be transferred in a single transaction */ valueLimit: bigint + /** The rules that define the conditions under which the permission is granted */ rules: Rule[] } @@ -81,25 +116,16 @@ export const getUserOpHash = ( return keccak256(enc) } -export async function getABISVMSessionKeyData( - sessionKey: `0x${string}` | Uint8Array, - permission: Permission -): Promise<`0x${string}` | Uint8Array> { - let sessionKeyData = concat([ - sessionKey, - permission.destContract, - permission.functionSelector, - pad(toHex(permission.valueLimit), { size: 16 }), - pad(toHex(permission.rules.length), { size: 2 }) // this can't be more 2**11 (see below), so uint16 (2 bytes) is enough - ]) as `0x${string}` - - for (let i = 0; i < permission.rules.length; i++) { - sessionKeyData = concat([ - sessionKeyData, - pad(toHex(permission.rules[i].offset), { size: 2 }), // offset is uint16, so there can't be more than 2**16/32 args = 2**11 - pad(toHex(permission.rules[i].condition), { size: 1 }), // uint8 - permission.rules[i].referenceValue - ]) +export const getRandomSigner = (): SignerData => { + const pkey = generatePrivateKey() + const account = privateKeyToAccount(pkey) + return { + pvKey: pkey, + pbKey: account.address } - return sessionKeyData +} + +export const parseChain = (chainInfo: ChainInfo): Chain => { + if (typeof chainInfo === "number") return getChain(chainInfo) + return chainInfo } diff --git a/src/modules/utils/Types.ts b/src/modules/utils/Types.ts index 26c0badcf..57879056e 100644 --- a/src/modules/utils/Types.ts +++ b/src/modules/utils/Types.ts @@ -41,7 +41,7 @@ export interface SessionKeyManagerModuleConfig /** Version of the module */ version?: ModuleVersion /** SmartAccount address */ - smartAccountAddress: string + smartAccountAddress: Hex storageType?: StorageType sessionStorageClient?: ISessionStorage } @@ -57,13 +57,15 @@ export interface BatchedSessionRouterModuleConfig /** Session Key Manager module address */ sessionManagerModuleAddress?: Hex /** Address of the associated smart account */ - smartAccountAddress: string + smartAccountAddress: Hex /** Storage type, e.g. local storage */ storageType?: StorageType } export enum StorageType { - LOCAL_STORAGE = 0 + LOCAL_STORAGE = 0, + MEMORY_STORAGE = 1, + FILE_STORAGE = 2 } export type SessionDataTuple = [ @@ -96,6 +98,7 @@ export type ModuleInfo = { sessionValidationModule?: Hex /** Additional info if needed to be appended in signature */ additionalSessionData?: string + /** Batch session params */ batchSessionParams?: SessionParams[] } @@ -105,14 +108,14 @@ export interface SendUserOpParams extends ModuleInfo { } export type SignerData = { - /** Public key */ - pbKey: string + /** This is not the public as provided by viem, key but address for the given pvKey */ + pbKey: Hex /** Private key */ - pvKey: `0x${string}` - /** Network Id */ - chainId?: Chain + pvKey: Hex } +export type ChainInfo = number | Chain + export type CreateSessionDataResponse = { data: string sessionIDInfo: Array @@ -123,8 +126,11 @@ export interface CreateSessionDataParams { validUntil: number /** window start for the session key */ validAfter: number + /** Address of the session validation module */ sessionValidationModule: Hex + /** Public key of the session */ sessionPublicKey: Hex + /** The hex of the rules {@link Rule} that make up the policy */ sessionKeyData: Hex /** we generate uuid based sessionId. but if you prefer to track it on your side and attach custom session identifier this can be passed */ preferredSessionId?: string diff --git a/tests/account/read.test.ts b/tests/account/read.test.ts index ca24cc709..dd53d1cfc 100644 --- a/tests/account/read.test.ts +++ b/tests/account/read.test.ts @@ -8,8 +8,10 @@ import { encodeAbiParameters, encodeFunctionData, hashMessage, + pad, parseAbi, - parseAbiParameters + parseAbiParameters, + toHex } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { bsc } from "viem/chains" diff --git a/tests/account/write.test.ts b/tests/account/write.test.ts index 72e28d643..8467a5e27 100644 --- a/tests/account/write.test.ts +++ b/tests/account/write.test.ts @@ -535,7 +535,7 @@ describe("Account:Write", () => { ) const receipt = await response.wait() expect(receipt.success).toBe("true") - }, 35000) + }, 50000) test("should revert transfer ownership with signer that is not the owner", async () => { _smartAccount = await createSmartAccountClient({ @@ -563,7 +563,7 @@ describe("Account:Write", () => { } ) ).rejects.toThrowError() - }, 35000) + }, 50000) test("send an user op with the new owner", async () => { _smartAccount = await createSmartAccountClient({ @@ -589,7 +589,7 @@ describe("Account:Write", () => { }) const response = await wait() expect(response.success).toBe("true") - }, 35000) + }, 50000) test("should revert if sending an user op with the old owner", async () => { _smartAccount = await createSmartAccountClient({ @@ -613,7 +613,7 @@ describe("Account:Write", () => { ).rejects.toThrowError( await getAAError("Error coming from Bundler: AA24 signature error") ) - }, 35000) + }, 50000) test("should transfer ownership of smart account back to EOA 1", async () => { _smartAccount = await createSmartAccountClient({ @@ -642,6 +642,6 @@ describe("Account:Write", () => { ) const receipt = await response.wait() expect(receipt.success).toBe("true") - }, 45000) + }, 50000) }) }) diff --git a/tests/bundler/read.test.ts b/tests/bundler/read.test.ts index 9a0604976..73d2f415c 100644 --- a/tests/bundler/read.test.ts +++ b/tests/bundler/read.test.ts @@ -58,7 +58,7 @@ describe("Bundler:Read", () => { }) test.concurrent( - "Should throw and give advice", + "should throw and give advice", async () => { const randomPrivateKey = generatePrivateKey() const unfundedAccount = privateKeyToAccount(randomPrivateKey) diff --git a/tests/modules/read.test.ts b/tests/modules/read.test.ts index ad1c21649..e54381ce8 100644 --- a/tests/modules/read.test.ts +++ b/tests/modules/read.test.ts @@ -1,7 +1,14 @@ import { defaultAbiCoder } from "@ethersproject/abi" import { JsonRpcProvider } from "@ethersproject/providers" import { Wallet } from "@ethersproject/wallet" -import { http, type Hex, createWalletClient, encodeAbiParameters } from "viem" +import { + http, + type Hex, + createWalletClient, + encodeAbiParameters, + slice, + toFunctionSelector +} from "viem" import { privateKeyToAccount } from "viem/accounts" import { beforeAll, describe, expect, test } from "vitest" import { @@ -9,8 +16,10 @@ import { createSmartAccountClient } from "../../src/account" import { + DEFAULT_ERC20_MODULE, createECDSAOwnershipValidationModule, - createMultiChainValidationModule + createMultiChainValidationModule, + getSessionDatum } from "../../src/modules" import { getConfig } from "../utils" @@ -59,10 +68,60 @@ describe("Modules:Read", () => { ) }) + test.concurrent("should check session key helpers", async () => { + const EXPECTED_SESSION_KEY_DATA = + "0x000000000000000000000000fa66e705cf2582cf56528386bb9dfca119767262000000000000000000000000747a4168db14f57871fa8cda8b5455d8c2a8e90a0000000000000000000000003079b249dfde4692d7844aa261f8cf7d927a0da50000000000000000000000000000000000000000000000000000000000989680" + const EXPECTED_ABI_SESSION_DATA = + "0xFA66E705cf2582cF56528386Bb9dFCA119767262747A4168DB14F57871fa8cda8B5455D8C2a8e90aa9059cbb0000000000000000000000000098968000020000000000000000000000000000003079B249DFDE4692D7844aA261f8cf7D927A0DA5000101989680" + const sessionKeyEOA = "0xFA66E705cf2582cF56528386Bb9dFCA119767262" + const token = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a" + const recipient = "0x3079B249DFDE4692D7844aA261f8cf7D927A0DA5" + const amount = 10000000n + + const sessionKeyData = encodeAbiParameters( + [ + { type: "address" }, + { type: "address" }, + { type: "address" }, + { type: "uint256" } + ], + [ + sessionKeyEOA, + token, // erc20 token address + recipient, // receiver address + amount + ] + ) + + const abiSessionData = await getSessionDatum(sessionKeyEOA, { + destContract: token, + functionSelector: slice( + toFunctionSelector("transfer(address,uint256)"), + 0, + 4 + ), + valueLimit: amount, + rules: [ + { + offset: 0, + condition: 0, + referenceValue: recipient + }, + { + offset: 1, + condition: 1, + referenceValue: amount + } + ] + }) + + expect(EXPECTED_ABI_SESSION_DATA).toEqual(abiSessionData) + expect(EXPECTED_SESSION_KEY_DATA).toEqual(sessionKeyData) + }) + test.concurrent("should encode params successfully", async () => { const hardcodedPaddedSignature = "0x000000000000000000000000000002fbffedd9b33f4e7156f2de8d48945e74890000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d50c68705bd6897b2d17c7de32fb519fda00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000080000000000000000000000000fa66e705cf2582cf56528386bb9dfca119767262000000000000000000000000da5289fcaaf71d52a80a254da614a192b693e9770000000000000000000000003079b249dfde4692d7844aa261f8cf7d927a0da500000000000000000000000000000000000000000000000000000000009896800000000000000000000000000000000000000000000000000000000000000003ca2b0ef4564d7ca7044b8c9d75685659975c0cab591408cb20e4a2ab278ab282633eb23075a8cb9bc5b01a5a4fa367b73da76821105f67a62ed7eedd335f6c0e361e73015ce4bb83439ab0742bdfed338ec39e2e8dd0819528f02be218fc1db90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ba4a7338d7a90dfa465cf975cc6691812c3772e00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000fa66e705cf2582cf56528386bb9dfca1197672620000000000000000000000000000000000000000000000000000000000000003b91f47666ba9b0b6b2cfbb60bf39b241d269786aa01f388021057d080863dd40633eb23075a8cb9bc5b01a5a4fa367b73da76821105f67a62ed7eedd335f6c0e361e73015ce4bb83439ab0742bdfed338ec39e2e8dd0819528f02be218fc1db90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004173c3ac716c487ca34bb858247b5ccf1dc354fbaabdd089af3b2ac8e78ba85a4959a2d76250325bd67c11771c31fccda87c33ceec17cc0de912690521bb95ffcb1b00000000000000000000000000000000000000000000000000000000000000" - const sessionKeyManagerAddress = "0x000002FbFfedd9B33F4E7156F2DE8D48945E7489" const mockEcdsaSessionKeySig = @@ -71,7 +130,7 @@ describe("Modules:Read", () => { [ 0n, 0n, - "0x000000d50c68705bd6897b2d17c7de32fb519fda", + DEFAULT_ERC20_MODULE, "0x000000000000000000000000fa66e705cf2582cf56528386bb9dfca119767262000000000000000000000000da5289fcaaf71d52a80a254da614a192b693e9770000000000000000000000003079b249dfde4692d7844aa261f8cf7d927a0da50000000000000000000000000000000000000000000000000000000000989680", [ "0xca2b0ef4564d7ca7044b8c9d75685659975c0cab591408cb20e4a2ab278ab282", diff --git a/tests/modules/write.test.ts b/tests/modules/write.test.ts index 9f1e32357..32fef3b22 100644 --- a/tests/modules/write.test.ts +++ b/tests/modules/write.test.ts @@ -17,29 +17,64 @@ import { beforeAll, describe, expect, test } from "vitest" import { type BiconomySmartAccountV2, type Transaction, + type TransferOwnershipCompatibleModule, type WalletClientSigner, createSmartAccountClient } from "../../src/account" import { Logger, getChain } from "../../src/account" -import { ECDSAModuleAbi } from "../../src/account/abi/ECDSAModule" import { + type CreateSessionDataParams, DEFAULT_BATCHED_SESSION_ROUTER_MODULE, DEFAULT_ECDSA_OWNERSHIP_MODULE, DEFAULT_MULTICHAIN_MODULE, DEFAULT_SESSION_KEY_MANAGER_MODULE, ECDSA_OWNERSHIP_MODULE_ADDRESSES_BY_VERSION, createBatchedSessionRouterModule, - createECDSAOwnershipValidationModule, createMultiChainValidationModule, createSessionKeyManagerModule, getABISVMSessionKeyData } from "../../src/modules" + +import { ECDSAModuleAbi } from "../../src/account/abi/ECDSAModule" import { SessionMemoryStorage } from "../../src/modules/session-storage/SessionMemoryStorage" +import { createSessionKeyEOA } from "../../src/modules/session-storage/utils" +import { + type Session, + createABISessionDatum, + createSession +} from "../../src/modules/sessions/abi" +import { + createBatchSession, + getBatchSessionTxParams +} from "../../src/modules/sessions/batch" +import { createERC20SessionDatum } from "../../src/modules/sessions/erc20" +import { createSessionSmartAccountClient } from "../../src/modules/sessions/sessionSmartAccountClient" import { PaymasterMode } from "../../src/paymaster" import { checkBalance, getBundlerUrl, getConfig, topUp } from "../utils" describe("Modules:Write", () => { const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e" + const token = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a" + const amount = parseUnits(".0001", 6) + + const withSponsorship = { + paymasterServiceData: { mode: PaymasterMode.SPONSORED } + } + + const stores: { + single: Session + batch: Session + } = { + single: { + sessionStorageClient: new SessionMemoryStorage("0x"), + sessionID: "0x" + }, + batch: { + sessionStorageClient: new SessionMemoryStorage("0x"), + sessionID: "0x" + } + } + const { chain, chainId, @@ -56,8 +91,18 @@ describe("Modules:Write", () => { transport: http() }) - let [smartAccount, smartAccountTwo]: BiconomySmartAccountV2[] = [] - let [smartAccountAddress, smartAccountAddressTwo]: Hex[] = [] + let [ + smartAccount, + smartAccountTwo, + smartAccountThree, + smartAccountFour + ]: BiconomySmartAccountV2[] = [] + let [ + smartAccountAddress, + smartAccountAddressTwo, + smartAccountAddressThree, + smartAccountAddressFour + ]: Hex[] = [] const [walletClient, walletClientTwo] = [ createWalletClient({ @@ -72,6 +117,8 @@ describe("Modules:Write", () => { }) ] + const recipient = walletClientTwo.account.address + beforeAll(async () => { ;[smartAccount, smartAccountTwo] = await Promise.all( [walletClient, walletClientTwo].map((client) => @@ -88,34 +135,288 @@ describe("Modules:Write", () => { account.getAccountAddress() ) ) - }) - test("should enable session module", async () => { - const smartAccount = await createSmartAccountClient({ + // Same user as smartAccount, but different smart account + smartAccountThree = await createSmartAccountClient({ signer: walletClient, bundlerUrl, - paymasterUrl + paymasterUrl, + index: 7 }) - const isSessionKeyEnabled = await smartAccount.isModuleEnabled( - DEFAULT_SESSION_KEY_MANAGER_MODULE + smartAccountFour = await createSmartAccountClient({ + signer: walletClient, + bundlerUrl, + paymasterUrl, + index: 6 + }) + + smartAccountAddressThree = await smartAccountThree.getAccountAddress() + smartAccountAddressFour = await smartAccountFour.getAccountAddress() + + stores.single.sessionStorageClient = new SessionMemoryStorage( + smartAccountAddressThree + ) + stores.batch.sessionStorageClient = new SessionMemoryStorage( + smartAccountAddressFour ) - if (!isSessionKeyEnabled) { - const tx = await smartAccount.getEnableModuleData( - DEFAULT_SESSION_KEY_MANAGER_MODULE + await Promise.all([ + topUp(smartAccountAddress, undefined, token), + topUp(smartAccountAddress, undefined), + topUp(smartAccountAddressTwo, undefined, token), + topUp(smartAccountAddressTwo, undefined), + topUp(smartAccountAddressThree, undefined, token), + topUp(smartAccountAddressThree, undefined), + topUp(smartAccountAddressFour, undefined, token), + topUp(smartAccountAddressFour, undefined) + ]) + }) + + // User must be connected with a wallet to grant permissions + test("should create a single session on behalf of a user", async () => { + const { sessionKeyAddress, sessionStorageClient } = + await createSessionKeyEOA( + smartAccountThree, + chain, + stores.single.sessionStorageClient ) - const { wait } = await smartAccount.sendTransaction(tx, { - paymasterServiceData: { mode: PaymasterMode.SPONSORED } + + const { wait, session } = await createSession( + smartAccountThree, + [ + { + sessionKeyAddress, + contractAddress: nftAddress, + functionSelector: "safeMint(address)", + rules: [ + { + offset: 0, + condition: 0, + referenceValue: smartAccountAddressThree + } + ], + interval: { + validUntil: 0, + validAfter: 0 + }, + valueLimit: 0n + } + ], + sessionKeyAddress, + sessionStorageClient, + withSponsorship + ) + + const { + receipt: { transactionHash }, + success + } = await wait() + + // Save the sessionID for the next test + stores.single.sessionID = session.sessionID + + expect(success).toBe("true") + Logger.log("Tx Hash: ", transactionHash) + }, 50000) + + // User no longer has to be connected, + // Only the reference to the relevant sessionID and the store from the previous step is needed to execute txs on the user's behalf + test("should use the session to mint an NFT for the user", async () => { + // Setup + const session = stores.single + expect(stores.single.sessionID).toBeTruthy() // Should have been set in the previous test + + // Assume the real signer for userSmartAccountThree is no longer available (ie. user has logged out) + const smartAccountThreeWithSession = await createSessionSmartAccountClient( + { + accountAddress: smartAccountAddressThree, // Set the account address on behalf of the user + bundlerUrl, + paymasterUrl, + chainId + }, + session + ) + + const sessionSmartAccountThreeAddress = + await smartAccountThreeWithSession.getAccountAddress() + + expect(sessionSmartAccountThreeAddress).toEqual(smartAccountAddressThree) + + const nftMintTx = { + to: nftAddress, + data: encodeFunctionData({ + abi: parseAbi(["function safeMint(address _to)"]), + functionName: "safeMint", + args: [smartAccountAddressThree] + }) + } + + const nftBalanceBefore = await checkBalance( + smartAccountAddressThree, + nftAddress + ) + + const { wait } = await smartAccountThreeWithSession.sendTransaction( + nftMintTx, + withSponsorship + ) + + const { success } = await wait() + + expect(success).toBe("true") + + const nftBalanceAfter = await checkBalance( + smartAccountAddressThree, + nftAddress + ) + + expect(nftBalanceAfter - nftBalanceBefore).toBe(1n) + }) + + // User must be connected with a wallet to grant permissions + test("should create a batch session on behalf of a user", async () => { + const { sessionKeyAddress, sessionStorageClient } = + await createSessionKeyEOA( + smartAccountFour, + chain, + stores.batch.sessionStorageClient + ) + + const leaves: CreateSessionDataParams[] = [ + createERC20SessionDatum({ + interval: { + validUntil: 0, + validAfter: 0 + }, + sessionKeyAddress, + sessionKeyData: encodeAbiParameters( + [ + { type: "address" }, + { type: "address" }, + { type: "address" }, + { type: "uint256" } + ], + [sessionKeyAddress, token, recipient, amount] + ) + }), + createABISessionDatum({ + interval: { + validUntil: 0, + validAfter: 0 + }, + sessionKeyAddress, + contractAddress: nftAddress, + functionSelector: "safeMint(address)", + rules: [ + { + offset: 0, + condition: 0, + referenceValue: smartAccountAddressFour + } + ], + valueLimit: 0n + }) + ] + + const { wait, session } = await createBatchSession( + smartAccountFour, + sessionKeyAddress, + sessionStorageClient, + leaves, + withSponsorship + ) + + const { + receipt: { transactionHash }, + success + } = await wait() + + expect(success).toBe("true") + stores.batch.sessionID = session.sessionID // Save the sessionID for the next test + + expect(session.sessionID).toBeTruthy() + // Save the sessionID for the next test + + Logger.log("Tx Hash: ", transactionHash) + Logger.log("session: ", { session }) + }, 50000) + + // User no longer has to be connected, + // Only the reference to the relevant sessionID and the store from the previous step is needed to execute txs on the user's behalf + test("should use the batch session to mint an NFT, and pay some token for the user", async () => { + // Setup + // Setup + const session = stores.batch + expect(session.sessionID).toBeTruthy() // Should have been set in the previous test + + // Assume the real signer for userSmartAccountFour is no longer available (ie. user has logged out); + const smartAccountFourWithSession = await createSessionSmartAccountClient( + { + accountAddress: smartAccountAddressFour, // Set the account address on behalf of the user + bundlerUrl, + paymasterUrl, + chainId + }, + session, + true // if batching + ) + + const sessionSmartAccountFourAddress = + await smartAccountFourWithSession.getAccountAddress() + + expect(sessionSmartAccountFourAddress).toEqual(smartAccountAddressFour) + + const transferTx: Transaction = { + to: token, + data: encodeFunctionData({ + abi: parseAbi(["function transfer(address _to, uint256 _value)"]), + functionName: "transfer", + args: [recipient, amount] + }) + } + const nftMintTx: Transaction = { + to: nftAddress, + data: encodeFunctionData({ + abi: parseAbi(["function safeMint(address _to)"]), + functionName: "safeMint", + args: [smartAccountAddressFour] }) - const { success } = await wait() - expect(success).toBe("true") } + + const nftBalanceBefore = await checkBalance( + smartAccountAddressFour, + nftAddress + ) + const tokenBalanceBefore = await checkBalance(recipient, token) + + const txs = [transferTx, nftMintTx] + + const batchSessionParams = await getBatchSessionTxParams( + ["ERC20", "ABI"], + txs, + session, + chain + ) + + const { wait } = await smartAccountFourWithSession.sendTransaction(txs, { + ...batchSessionParams, + ...withSponsorship + }) + const { success } = await wait() + expect(success).toBe("true") + + const tokenBalanceAfter = await checkBalance(recipient, token) + const nftBalanceAfter = await checkBalance( + smartAccountAddressFour, + nftAddress + ) + expect(tokenBalanceAfter - tokenBalanceBefore).toBe(amount) + expect(nftBalanceAfter - nftBalanceBefore).toBe(1n) }, 50000) - test.skip("should use MultichainValidationModule to mint an NFT on two chains with sponsorship", async () => { + test("should use MultichainValidationModule to mint an NFT on two chains with sponsorship", async () => { const nftAddress: Hex = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e" - const recipientForBothChains = walletClient.account.address const chainIdBase = 84532 const bundlerUrlBase = getBundlerUrl(chainIdBase) @@ -158,16 +459,12 @@ describe("Modules:Write", () => { baseAccount.isAccountDeployed() ]) if (!isPolygonDeployed) { - const { wait } = await polygonAccount.deploy({ - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }) + const { wait } = await polygonAccount.deploy(withSponsorship) const { success } = await wait() expect(success).toBe("true") } if (!isBaseDeployed) { - const { wait } = await baseAccount.deploy({ - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }) + const { wait } = await baseAccount.deploy(withSponsorship) const { success } = await wait() expect(success).toBe("true") } @@ -191,7 +488,7 @@ describe("Modules:Write", () => { "function safeMint(address owner) view returns (uint balance)" ]), functionName: "safeMint", - args: [recipientForBothChains] + args: [recipient] }) const transaction = { @@ -200,12 +497,8 @@ describe("Modules:Write", () => { } const [partialUserOp1, partialUserOp2] = await Promise.all([ - baseAccount.buildUserOp([transaction], { - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }), - polygonAccount.buildUserOp([transaction], { - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }) + baseAccount.buildUserOp([transaction], withSponsorship), + polygonAccount.buildUserOp([transaction], withSponsorship) ]) expect(partialUserOp1.paymasterAndData).not.toBe("0x") @@ -251,7 +544,7 @@ describe("Modules:Write", () => { signer: walletClient, bundlerUrl, paymasterUrl, - index: 5 // Increasing index to not conflict with other test cases and use a new smart account + index: 11 // Increasing index to not conflict with other test cases and use a new smart account }) const accountAddress = await smartAccount.getAccountAddress() const sessionMemoryStorage: SessionMemoryStorage = new SessionMemoryStorage( @@ -269,13 +562,18 @@ describe("Modules:Write", () => { } try { - sessionSigner = await sessionMemoryStorage.getSignerByKey(sessionKeyEOA) + sessionSigner = await sessionMemoryStorage.getSignerByKey( + sessionKeyEOA, + chain + ) } catch (error) { - sessionSigner = await sessionMemoryStorage.addSigner({ - pbKey: sessionKeyEOA, - pvKey: `0x${privateKey}`, - chainId: chain - }) + sessionSigner = await sessionMemoryStorage.addSigner( + { + pbKey: sessionKeyEOA, + pvKey: `0x${privateKey}` + }, + chain + ) } expect(sessionSigner).toBeTruthy() @@ -368,9 +666,6 @@ describe("Modules:Write", () => { expect(userOpResponse2.userOpHash).not.toBeNull() const maticBalanceAfter = await checkBalance(smartAccountAddress) expect(maticBalanceAfter).toEqual(maticBalanceBefore) - Logger.log( - `Tx at: https://jiffyscan.xyz/userOpHash/${userOpResponse2.userOpHash}?network=mumbai` - ) }, 60000) test("should enable batched module", async () => { @@ -396,345 +691,157 @@ describe("Modules:Write", () => { } }, 50000) - test.skip("should use BatchedSessionValidationModule to send a user op", async () => { - let sessionSigner: WalletClientSigner - const sessionKeyEOA = walletClient.account.address - const recipient = walletClientTwo.account.address - const token = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a" - - let smartAccount = await createSmartAccountClient({ + test("should use ABI SVM to allow transfer ownership of smart account", async () => { + const smartAccount = await createSmartAccountClient({ chainId, signer: walletClient, bundlerUrl, paymasterUrl, - index: 6 // Increasing index to not conflict with other test cases and use a new smart account + index: 10 // Increasing index to not conflict with other test cases and use a new smart account }) - // const accountAddress = await smartAccount.getAccountAddress() - const smartAccountAddress = await smartAccount.getAddress() - await topUp(smartAccountAddress, undefined, token) - await topUp(smartAccountAddress, undefined) + const smartAccountAddressForPreviousOwner = + await smartAccount.getAccountAddress() + + const signerOfAccount = walletClient.account.address + const ownerOfAccount = await publicClient.readContract({ + address: DEFAULT_ECDSA_OWNERSHIP_MODULE, + abi: ECDSAModuleAbi, + functionName: "getOwner", + args: [await smartAccount.getAccountAddress()] + }) + if (ownerOfAccount !== signerOfAccount) { + // Re-create the smart account instance with the new owner + const smartAccountWithOtherOwner = await createSmartAccountClient({ + chainId, + signer: walletClientTwo, + bundlerUrl, + paymasterUrl, + accountAddress: smartAccountAddressForPreviousOwner + }) + + // Transfer ownership back to walletClient 1 + await smartAccountWithOtherOwner.transferOwnership( + walletClient.account.address, + DEFAULT_ECDSA_OWNERSHIP_MODULE as TransferOwnershipCompatibleModule, + { paymasterServiceData: { mode: PaymasterMode.SPONSORED } } + ) + } + + let sessionSigner: WalletClientSigner + const sessionKeyEOA = walletClient.account.address + const newOwner = walletClientTwo.account.address + + const accountAddress = await smartAccount.getAccountAddress() const sessionMemoryStorage: SessionMemoryStorage = new SessionMemoryStorage( - smartAccountAddress + accountAddress ) + // First we need to check if smart account is deployed + // if not deployed, send an empty transaction to deploy it + const isDeployed = await smartAccount.isAccountDeployed() + if (!isDeployed) { + const { wait } = await smartAccount.deploy({ + paymasterServiceData: { mode: PaymasterMode.SPONSORED } + }) + const { success } = await wait() + expect(success).toBe("true") + } try { - sessionSigner = await sessionMemoryStorage.getSignerByKey(sessionKeyEOA) + sessionSigner = await sessionMemoryStorage.getSignerByKey( + sessionKeyEOA, + chain + ) } catch (error) { - sessionSigner = await sessionMemoryStorage.addSigner({ - pbKey: sessionKeyEOA, - pvKey: `0x${privateKey}`, - chainId: chain - }) + sessionSigner = await sessionMemoryStorage.addSigner( + { + pbKey: sessionKeyEOA, + pvKey: `0x${privateKeyTwo}` + }, + chain + ) } expect(sessionSigner).toBeTruthy() + // Create session module const sessionModule = await createSessionKeyManagerModule({ moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, - smartAccountAddress, + smartAccountAddress: await smartAccount.getAddress(), sessionStorageClient: sessionMemoryStorage }) - // Create batched session module - const batchedSessionModule = await createBatchedSessionRouterModule({ - moduleAddress: DEFAULT_BATCHED_SESSION_ROUTER_MODULE, - smartAccountAddress, - sessionKeyManagerModule: sessionModule - }) - - // Set enabled call on session, only allows calling USDC contract transfer with <= 10 USDC - const sessionKeyData = encodeAbiParameters( - [ - { type: "address" }, - { type: "address" }, - { type: "address" }, - { type: "uint256" } - ], - [ - sessionKeyEOA, - token, // erc20 token address - recipient, // receiver address - parseUnits("10", 6) - ] - ) - // only requires that the caller is the session key - // can call anything using the mock session module - const sessionKeyData2 = encodeAbiParameters( - [{ type: "address" }], - [sessionKeyEOA] + const functionSelectorTransferOwnership = slice( + toFunctionSelector("transferOwnership(address) public"), + 0, + 4 ) - const erc20ModuleAddr = "0x000000D50C68705bd6897B2d17c7de32FB519fDA" - const mockSessionModuleAddr = "0x7Ba4a7338D7A90dfA465cF975Cc6691812C3772E" - const sessionTxData = await batchedSessionModule.createSessionData([ + const sessionKeyDataTransferOwnership = await getABISVMSessionKeyData( + sessionKeyEOA as Hex, { - validUntil: 0, - validAfter: 0, - sessionValidationModule: erc20ModuleAddr, - sessionPublicKey: sessionKeyEOA, - sessionKeyData: sessionKeyData - }, - { - validUntil: 0, - validAfter: 0, - sessionValidationModule: mockSessionModuleAddr, - sessionPublicKey: sessionKeyEOA, - sessionKeyData: sessionKeyData2 + destContract: ECDSA_OWNERSHIP_MODULE_ADDRESSES_BY_VERSION.V1_0_0 as Hex, // ECDSA module address + functionSelector: functionSelectorTransferOwnership, + valueLimit: parseEther("0"), + rules: [ + { + offset: 0, // offset 0 means we are checking first parameter of transferOwnership (recipient address) + condition: 0, // 0 = Condition.EQUAL + referenceValue: pad(walletClient.account.address, { + size: 32 + }) // new owner address + } + ] } - ]) - - const setSessionAllowedTrx = { + ) + const abiSvmAddress = "0x000006bC2eCdAe38113929293d241Cf252D91861" + const sessionTxDataTransferOwnership = + await sessionModule.createSessionData([ + { + validUntil: 0, + validAfter: 0, + sessionValidationModule: abiSvmAddress, + sessionPublicKey: sessionKeyEOA as Hex, + sessionKeyData: sessionKeyDataTransferOwnership as Hex + } + ]) + const setSessionAllowedTransferOwnerhsipTrx = { to: DEFAULT_SESSION_KEY_MANAGER_MODULE, - data: sessionTxData.data - } - - const isDeployed = await smartAccount.isAccountDeployed() - if (!isDeployed) { - const { wait } = await smartAccount.deploy() - const { success } = await wait() - expect(success).toBe("true") + data: sessionTxDataTransferOwnership.data } - - const txArray: Transaction[] = [] - // Check if session module is enabled + // biome-ignore lint/suspicious/noExplicitAny: + const txArray: any = [] + // Check if module is enabled const isEnabled = await smartAccount.isModuleEnabled( DEFAULT_SESSION_KEY_MANAGER_MODULE ) + if (!isEnabled) { const enableModuleTrx = await smartAccount.getEnableModuleData( DEFAULT_SESSION_KEY_MANAGER_MODULE ) txArray.push(enableModuleTrx) + txArray.push(setSessionAllowedTransferOwnerhsipTrx) + } else { + Logger.log("MODULE ALREADY ENABLED") + txArray.push(setSessionAllowedTransferOwnerhsipTrx) } - // Check if batched session module is enabled - const isBRMenabled = await smartAccount.isModuleEnabled( - DEFAULT_BATCHED_SESSION_ROUTER_MODULE - ) - if (!isBRMenabled) { - // -----> enableModule batched session router module - const tx2 = await smartAccount.getEnableModuleData( - DEFAULT_BATCHED_SESSION_ROUTER_MODULE - ) - txArray.push(tx2) - } - txArray.push(setSessionAllowedTrx) const userOpResponse1 = await smartAccount.sendTransaction(txArray, { paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }) // this user op will enable the modules and setup session allowed calls + }) const transactionDetails = await userOpResponse1.wait() expect(transactionDetails.success).toBe("true") Logger.log("Tx Hash: ", transactionDetails.receipt.transactionHash) - const usdcBalance = await checkBalance(smartAccountAddress, token) - const nativeTokenBalance = await checkBalance(smartAccountAddress) - - expect(usdcBalance).toBeGreaterThan(0) - smartAccount = smartAccount.setActiveValidationModule(batchedSessionModule) - // WARNING* If the smart account does not have enough USDC, user op execution will FAIL - const encodedCall = encodeFunctionData({ - abi: parseAbi(["function transfer(address _to, uint256 _value)"]), - functionName: "transfer", - args: [recipient, parseUnits("0.001", 6)] - }) - const encodedCall2 = encodeFunctionData({ - abi: parseAbi(["function transfer(address _to, uint256 _value)"]), - functionName: "transfer", - args: [ - "0xd3C85Fdd3695Aee3f0A12B3376aCD8DC54020549", - parseUnits("0.001", 6) - ] - }) - const transferTx = { - to: "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a", - data: encodedCall - } - const transferTx2 = { - to: "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a", - data: encodedCall2 - } - const activeModule = smartAccount.activeValidationModule - expect(activeModule).toEqual(batchedSessionModule) - const maticBalanceBefore = await checkBalance(smartAccountAddress) - // failing with dummyTx because of invalid sessionKeyData - const userOpResponse2 = await smartAccount.sendTransaction( - [transferTx, transferTx2], + // Transfer ownership back to walletClient + await smartAccount.transferOwnership( + newOwner, + DEFAULT_ECDSA_OWNERSHIP_MODULE as TransferOwnershipCompatibleModule, { + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, params: { - batchSessionParams: [ - { - sessionSigner: walletClient, - sessionValidationModule: erc20ModuleAddr - }, - { - sessionSigner: walletClient, - sessionValidationModule: mockSessionModuleAddr - } - ] - }, - paymasterServiceData: { - mode: PaymasterMode.SPONSORED + sessionSigner: sessionSigner, + sessionValidationModule: abiSvmAddress } } ) - const receipt = await userOpResponse2.wait() - console.log(receipt.userOpHash, "Batched user op hash") - expect(receipt.success).toBe("true") - expect(userOpResponse2.userOpHash).toBeTruthy() - expect(userOpResponse2.userOpHash).not.toBeNull() - const maticBalanceAfter = await checkBalance(smartAccountAddress) - expect(maticBalanceAfter).toEqual(maticBalanceBefore) - Logger.log( - `Tx at: https://jiffyscan.xyz/userOpHash/${userOpResponse2.userOpHash}?network=mumbai` - ) }, 60000) - - describe("Transfer ownership", () => { - test("should use ABI SVM to allow transfer ownership of smart account", async () => { - const smartAccount = await createSmartAccountClient({ - chainId, - signer: walletClient, - bundlerUrl, - paymasterUrl, - index: 10 // Increasing index to not conflict with other test cases and use a new smart account - }) - - const smartAccountAddressForPreviousOwner = - await smartAccount.getAccountAddress() - - const signerOfAccount = walletClient.account.address - const ownerOfAccount = await publicClient.readContract({ - address: DEFAULT_ECDSA_OWNERSHIP_MODULE, - abi: ECDSAModuleAbi, - functionName: "getOwner", - args: [await smartAccount.getAccountAddress()] - }) - - if (ownerOfAccount !== signerOfAccount) { - // Re-create the smart account instance with the new owner - const smartAccountWithOtherOwner = await createSmartAccountClient({ - chainId, - signer: walletClientTwo, - bundlerUrl, - paymasterUrl, - accountAddress: smartAccountAddressForPreviousOwner - }) - - // Transfer ownership back to walletClient 1 - await smartAccountWithOtherOwner.transferOwnership( - walletClient.account.address, - DEFAULT_ECDSA_OWNERSHIP_MODULE, - { paymasterServiceData: { mode: PaymasterMode.SPONSORED } } - ) - } - - let sessionSigner: WalletClientSigner - const sessionKeyEOA = walletClient.account.address - const newOwner = walletClientTwo.account.address - - const accountAddress = await smartAccount.getAccountAddress() - const sessionMemoryStorage: SessionMemoryStorage = - new SessionMemoryStorage(accountAddress) - // First we need to check if smart account is deployed - // if not deployed, send an empty transaction to deploy it - const isDeployed = await smartAccount.isAccountDeployed() - if (!isDeployed) { - const { wait } = await smartAccount.deploy({ - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }) - const { success } = await wait() - expect(success).toBe("true") - } - - try { - sessionSigner = await sessionMemoryStorage.getSignerByKey(sessionKeyEOA) - } catch (error) { - sessionSigner = await sessionMemoryStorage.addSigner({ - pbKey: sessionKeyEOA, - pvKey: `0x${privateKeyTwo}`, - chainId: chain - }) - } - - expect(sessionSigner).toBeTruthy() - // Create session module - const sessionModule = await createSessionKeyManagerModule({ - moduleAddress: DEFAULT_SESSION_KEY_MANAGER_MODULE, - smartAccountAddress: await smartAccount.getAddress(), - sessionStorageClient: sessionMemoryStorage - }) - const functionSelectorTransferOwnership = slice( - toFunctionSelector("transferOwnership(address) public"), - 0, - 4 - ) - const sessionKeyDataTransferOwnership = await getABISVMSessionKeyData( - sessionKeyEOA as Hex, - { - destContract: - ECDSA_OWNERSHIP_MODULE_ADDRESSES_BY_VERSION.V1_0_0 as Hex, // ECDSA module address - functionSelector: functionSelectorTransferOwnership, - valueLimit: parseEther("0"), - rules: [ - { - offset: 0, // offset 0 means we are checking first parameter of transferOwnership (recipient address) - condition: 0, // 0 = Condition.EQUAL - referenceValue: pad(walletClient.account.address, { - size: 32 - }) // new owner address - } - ] - } - ) - const abiSvmAddress = "0x000006bC2eCdAe38113929293d241Cf252D91861" - const sessionTxDataTransferOwnership = - await sessionModule.createSessionData([ - { - validUntil: 0, - validAfter: 0, - sessionValidationModule: abiSvmAddress, - sessionPublicKey: sessionKeyEOA as Hex, - sessionKeyData: sessionKeyDataTransferOwnership as Hex - } - ]) - const setSessionAllowedTransferOwnerhsipTrx = { - to: DEFAULT_SESSION_KEY_MANAGER_MODULE, - data: sessionTxDataTransferOwnership.data - } - // biome-ignore lint/suspicious/noExplicitAny: - const txArray: any = [] - // Check if module is enabled - const isEnabled = await smartAccount.isModuleEnabled( - DEFAULT_SESSION_KEY_MANAGER_MODULE - ) - - if (!isEnabled) { - const enableModuleTrx = await smartAccount.getEnableModuleData( - DEFAULT_SESSION_KEY_MANAGER_MODULE - ) - txArray.push(enableModuleTrx) - txArray.push(setSessionAllowedTransferOwnerhsipTrx) - } else { - Logger.log("MODULE ALREADY ENABLED") - txArray.push(setSessionAllowedTransferOwnerhsipTrx) - } - const userOpResponse1 = await smartAccount.sendTransaction(txArray, { - paymasterServiceData: { mode: PaymasterMode.SPONSORED } - }) - const transactionDetails = await userOpResponse1.wait() - expect(transactionDetails.success).toBe("true") - Logger.log("Tx Hash: ", transactionDetails.receipt.transactionHash) - - // Transfer ownership back to walletClient - const resp = await smartAccount.transferOwnership( - newOwner, - DEFAULT_ECDSA_OWNERSHIP_MODULE, - { - paymasterServiceData: { mode: PaymasterMode.SPONSORED }, - params: { - sessionSigner: sessionSigner, - sessionValidationModule: abiSvmAddress - } - } - ) - }, 60000) - }) }) diff --git a/tests/paymaster/read.test.ts b/tests/paymaster/read.test.ts index 3b851100b..1509ee322 100644 --- a/tests/paymaster/read.test.ts +++ b/tests/paymaster/read.test.ts @@ -90,7 +90,8 @@ describe("Paymaster:Read", () => { }) expect(feeQuotesResponse.feeQuotes?.length).toBeGreaterThan(1) - } + }, + 30000 ) test.concurrent( diff --git a/tests/utils.ts b/tests/utils.ts index 2a89b0121..908982e3b 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -7,7 +7,8 @@ import { parseAbi } from "viem" import { privateKeyToAccount } from "viem/accounts" -import { Logger, getChain } from "../src/account" +import { Logger } from "../src/account/utils/Logger" +import { getChain } from "../src/account/utils/getChain" import { extractChainIdFromBundlerUrl, extractChainIdFromPaymasterUrl @@ -22,7 +23,7 @@ export const getEnvVars = () => { "E2E_BICO_PAYMASTER_KEY_BASE", "CHAIN_ID" ] - const errorFields = fields.filter((field) => !process.env[field]) + const errorFields = fields.filter((field) => !process?.env?.[field]) if (errorFields.length) { throw new Error( `Missing environment variable${