diff --git a/.env.example b/.env.example index 4c5a02a1a..f3592f0c1 100644 --- a/.env.example +++ b/.env.example @@ -5,5 +5,4 @@ E2E_BICO_PAYMASTER_KEY_AMOY= E2E_BICO_PAYMASTER_KEY_BASE= CHAIN_ID=80002 CODECOV_TOKEN= -TESTING=false -SILENCE_LABS_NPM_TOKEN=npm_XXX \ No newline at end of file +TESTING=false \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f264edf53..08c18cd34 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,5 +12,3 @@ jobs: - name: Build uses: ./.github/actions/build - env: - SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4f2c6aad9..949c9eb0f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,8 +17,6 @@ jobs: - name: Install dependencies uses: ./.github/actions/install-dependencies - env: - SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Run the account tests run: bun run test:ci -t=Account:Write diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 63ed3848b..078b083ba 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,8 +17,6 @@ jobs: - name: Install dependencies uses: ./.github/actions/install-dependencies - env: - SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Set remote url run: git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/bcnmy/biconomy-client-sdk.git diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 028022ec8..3e77f7837 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -12,8 +12,6 @@ jobs: - name: Install dependencies uses: ./.github/actions/install-dependencies - env: - SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Use commitlint to check PR title run: echo "${{ github.event.pull_request.title }}" | bun commitlint diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml index 63e396f34..60d32b4ed 100644 --- a/.github/workflows/size-report.yml +++ b/.github/workflows/size-report.yml @@ -28,8 +28,6 @@ jobs: - name: Build uses: ./.github/actions/build - env: - SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Report bundle size uses: andresz1/size-limit-action@master diff --git a/.github/workflows/test-read.yml b/.github/workflows/test-read.yml index 14f88a31d..f6338828d 100644 --- a/.github/workflows/test-read.yml +++ b/.github/workflows/test-read.yml @@ -16,8 +16,6 @@ jobs: - name: Install dependencies uses: ./.github/actions/install-dependencies - env: - SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Run the tests run: bun run test:ci -t=Read diff --git a/.github/workflows/test-write.yml b/.github/workflows/test-write.yml index 446fb63df..d5ebe9dd1 100644 --- a/.github/workflows/test-write.yml +++ b/.github/workflows/test-write.yml @@ -22,8 +22,6 @@ jobs: - name: Install dependencies uses: ./.github/actions/install-dependencies - env: - SILENCE_LABS_NPM_TOKEN: ${{ secrets.SILENCE_LABS_NPM_TOKEN }} - name: Run the account tests run: bun run test:ci -t=Account:Write diff --git a/.size-limit.json b/.size-limit.json index 52596234f..4b0c64b54 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,20 +2,20 @@ { "name": "core (esm)", "path": "./dist/_esm/index.js", - "limit": "80 kB", + "limit": "140 kB", "import": "*", "ignore": ["node:fs", "fs"] }, { "name": "core (cjs)", "path": "./dist/_cjs/index.js", - "limit": "80 kB", + "limit": "140 kB", "ignore": ["node:fs", "fs"] }, { "name": "account (tree-shaking)", "path": "./dist/_esm/index.js", - "limit": "80 kB", + "limit": "140 kB", "import": "{ createSmartAccountClient }", "ignore": ["node:fs", "fs"] }, @@ -36,7 +36,7 @@ { "name": "modules (tree-shaking)", "path": "./dist/_esm/modules/index.js", - "limit": "80 kB", + "limit": "140 kB", "import": "{ createSessionKeyManagerModule }", "ignore": ["node:fs", "fs"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d4b4aaf..2229bbcc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,11 @@ - Distributed Session Keys ## 4.5.3 +## 4.6.0 ### Minor Changes -- Sessions Dx +- Distributed Sessions ## 4.5.2 diff --git a/biome.json b/biome.json index 4e9fc4e32..38251fac8 100644 --- a/biome.json +++ b/biome.json @@ -14,8 +14,7 @@ "_types", "bun.lockb", "docs", - "dist", - "walletprovider-sdk" + "dist" ] }, "organizeImports": { diff --git a/bun.lockb b/bun.lockb index d07f06325..0e8882e57 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml deleted file mode 100644 index 8e3a10b0a..000000000 --- a/bunfig.toml +++ /dev/null @@ -1,2 +0,0 @@ -[install.scopes] -silencelaboratories = { token = "$SILENCE_LABS_NPM_TOKEN", url = "https://registry.npmjs.org" } \ No newline at end of file diff --git a/package.json b/package.json index 1181cd173..303543907 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "sideEffects": false, "name": "@biconomy/account", "author": "Biconomy", - "version": "4.5.5", + "version": "4.6.0", "description": "SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.", "keywords": [ "erc-7579", @@ -97,6 +97,7 @@ "dotenv": "^16.4.5", "ethers": "^6.12.0", "gh-pages": "^6.1.1", + "node-gyp-build": "^4.8.1", "rimraf": "^5.0.5", "simple-git-hooks": "^2.9.0", "size-limit": "^11", @@ -106,7 +107,7 @@ "vitest": "^1.3.1" }, "peerDependencies": { - "typescript": "^5", + "typescript": "^5.5.3", "viem": "^2" }, "commitlint": { @@ -119,7 +120,7 @@ "commit-msg": "npx --no -- commitlint --edit ${1}" }, "dependencies": { - "merkletreejs": "^0.4.0", - "@silencelaboratories/walletprovider-sdk": "^0.1.0" + "@silencelaboratories/walletprovider-sdk": "^0.1.0", + "merkletreejs": "^0.4.0" } } diff --git a/src/account/BiconomySmartAccountV2.ts b/src/account/BiconomySmartAccountV2.ts index 17ccf0d4d..059878b77 100644 --- a/src/account/BiconomySmartAccountV2.ts +++ b/src/account/BiconomySmartAccountV2.ts @@ -21,7 +21,6 @@ import { toBytes, toHex, } from "viem"; -import type { Prettify } from "viem/chains"; import type { IBundler } from "../bundler/IBundler.js"; import { Bundler, @@ -36,8 +35,11 @@ import { type SessionType, createECDSAOwnershipValidationModule, getBatchSessionTxParams, - getSingleSessionTxParams, -} from "../modules"; + getDanSessionTxParams, + getSingleSessionTxParams +} from "../modules" +import type { ISessionStorage } from "../modules/interfaces/ISessionStorage.js" +import { getDefaultStorageClient } from "../modules/session-storage/utils.js" import { BiconomyPaymaster, type FeeQuotesOrDataDto, @@ -82,6 +84,7 @@ import type { BiconomyTokenPaymasterRequest, BuildUserOpOptions, CounterFactualAddressParam, + GetSessionParams, NonceOptions, PaymasterUserOperationDto, QueryParamsForAddressResolver, @@ -105,9 +108,11 @@ type UserOperationKey = keyof UserOperationStruct; export class BiconomySmartAccountV2 extends BaseSmartContractAccount { private sessionData?: ModuleInfo; - private sessionType: SessionType | null = null; + private sessionType: SessionType | null = null - private SENTINEL_MODULE = "0x0000000000000000000000000000000000000001"; + private sessionStorageClient: ISessionStorage | undefined; + + private SENTINEL_MODULE = "0x0000000000000000000000000000000000000001" private index: number; @@ -160,8 +165,8 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { DEFAULT_BICONOMY_FACTORY_ADDRESS, }); - this.sessionData = biconomySmartAccountConfig.sessionData; - this.sessionType = biconomySmartAccountConfig.sessionType ?? null; + this.sessionData = biconomySmartAccountConfig.sessionData + this.sessionType = biconomySmartAccountConfig.sessionType ?? null this.defaultValidationModule = biconomySmartAccountConfig.defaultValidationModule; @@ -213,14 +218,15 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { getChain(biconomySmartAccountConfig.chainId), transport: http( biconomySmartAccountConfig.rpcUrl || - getChain(biconomySmartAccountConfig.chainId).rpcUrls.default.http[0], - ), - }); + getChain(biconomySmartAccountConfig.chainId).rpcUrls.default.http[0] + ) + }) this.scanForUpgradedAccountsFromV1 = - biconomySmartAccountConfig.scanForUpgradedAccountsFromV1 ?? false; - this.maxIndexForScan = biconomySmartAccountConfig.maxIndexForScan ?? 10; - this.getAccountAddress(); + biconomySmartAccountConfig.scanForUpgradedAccountsFromV1 ?? false + this.maxIndexForScan = biconomySmartAccountConfig.maxIndexForScan ?? 10 + this.getAccountAddress() + this.sessionStorageClient = biconomySmartAccountConfig.sessionStorageClient; } /** @@ -297,7 +303,6 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { if (!chainId) { throw new Error("chainId required"); } - const bundler: IBundler = biconomySmartAccountConfig.bundler ?? new Bundler({ @@ -329,6 +334,7 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { if (!resolvedSmartAccountSigner) { throw new Error("signer required"); } + const config: BiconomySmartAccountV2ConfigConstructorProps = { ...biconomySmartAccountConfig, defaultValidationModule, @@ -906,13 +912,13 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { async getDummySignatures(params?: ModuleInfo): Promise { const defaultedParams = { ...(this.sessionData ? this.sessionData : {}), - ...params, - }; + ...params + } - this.isActiveValidationModuleDefined(); + this.isActiveValidationModuleDefined() return (await this.activeValidationModule.getDummySignature( - defaultedParams, - )) as Hex; + defaultedParams + )) as Hex } // TODO: review this @@ -939,13 +945,14 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { async signUserOp( userOp: Partial, - params?: SendUserOpParams, + params?: SendUserOpParams ): Promise { const defaultedParams = { ...(this.sessionData ? this.sessionData : {}), ...params, - }; - this.isActiveValidationModuleDefined(); + rawUserOperation: userOp + } + this.isActiveValidationModuleDefined() const requiredFields: UserOperationKey[] = [ "sender", "nonce", @@ -956,25 +963,25 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { "preVerificationGas", "maxFeePerGas", "maxPriorityFeePerGas", - "paymasterAndData", - ]; - this.validateUserOp(userOp, requiredFields); + "paymasterAndData" + ] + this.validateUserOp(userOp, requiredFields) - const userOpHash = await this.getUserOpHash(userOp); + const userOpHash = await this.getUserOpHash(userOp) const moduleSig = (await this.activeValidationModule.signUserOpHash( userOpHash, - defaultedParams, - )) as Hex; + defaultedParams + )) as Hex const signatureWithModuleAddress = this.getSignatureWithModuleAddress( moduleSig, this.activeValidationModule.getAddress() as Hex, ); - userOp.signature = signatureWithModuleAddress; + userOp.signature = signatureWithModuleAddress - return userOp as UserOperationStruct; + return userOp as UserOperationStruct } getSignatureWithModuleAddress( @@ -982,13 +989,13 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { moduleAddress?: Hex, ): Hex { const moduleAddressToUse = - moduleAddress ?? (this.activeValidationModule.getAddress() as Hex); + moduleAddress ?? (this.activeValidationModule.getAddress() as Hex) const result = encodeAbiParameters(parseAbiParameters("bytes, address"), [ moduleSignature, - moduleAddressToUse, - ]); + moduleAddressToUse + ]) - return result; + return result } public async getPaymasterUserOp( @@ -1255,7 +1262,7 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { delete userOp.signature; const userOperation = await this.signUserOp(userOp, params); - const bundlerResponse = await this.sendSignedUserOp(userOperation); + const bundlerResponse = await this.sendSignedUserOp(userOperation) return bundlerResponse; } @@ -1416,7 +1423,6 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * @description This function will transfer ownership of the smart account to a new owner. If you use session key manager module, after transferring the ownership * you will need to re-create a session for the smart account with the new owner (signer) and specify "accountAddress" in "createSmartAccountClient" function. * @example - * ```typescript * * let walletClient = createWalletClient({ account, @@ -1445,7 +1451,6 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { chainId: 84532, accountAddress: await smartAccount.getAccountAddress() }) - * ``` */ async transferOwnership( newOwner: Address, @@ -1475,10 +1480,11 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * * @param manyOrOneTransactions Array of {@link Transaction} to be batched and sent. Can also be a single {@link Transaction}. * @param buildUseropDto {@link BuildUserOpOptions}. - * @param sessionData + * @param sessionData - Optional parameter. If you are using session keys, you can pass the sessionIds, the session and the storage client to retrieve the session data while sending a tx {@link GetSessionParams} * @returns Promise<{@link UserOpResponse}> that you can use to track the user operation. * * @example + * ```ts * import { createClient } from "viem" * import { createSmartAccountClient } from "@biconomy/account" * import { createWalletClient, http } from "viem"; @@ -1504,11 +1510,45 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * * const { waitForTxHash } = await smartAccount.sendTransaction(transaction); * const { transactionHash, userOperationReceipt } = await wait(); + * ``` + */ + async sendTransaction( + manyOrOneTransactions: Transaction | Transaction[], + buildUseropDto?: BuildUserOpOptions, + sessionData?: GetSessionParams + ): Promise { + let defaultedBuildUseropDto = { ...buildUseropDto } ?? {} + if (this.sessionType && sessionData) { + const store = this.sessionStorageClient ?? sessionData?.store; + const getSessionParameters = await this.getSessionParams({ ...sessionData, store, txs: manyOrOneTransactions }) + defaultedBuildUseropDto = { + ...defaultedBuildUseropDto, + ...getSessionParameters + } + } + + const userOp = await this.buildUserOp( + Array.isArray(manyOrOneTransactions) + ? manyOrOneTransactions + : [manyOrOneTransactions], + defaultedBuildUseropDto + ) + + return this.sendUserOp(userOp, { ...defaultedBuildUseropDto?.params }) + } + /** + * Retrieves the session parameters for sending the session transaction + * + * @description This method is called under the hood with the third argument passed into the smartAccount.sendTransaction(...args) method. It is used to retrieve the relevant session parameters while sending the session transaction. * - * @remarks - * This example shows how to increase the estimated gas values for a transaction using `gasOffset` parameter. + * @param leafIndex - The leaf index(es) of the session in the storage client to be used. If you want to use the last leaf index, you can pass "LAST_LEAVES" as the value. + * @param store - The {@link ISessionStorage} client to be used. If you want to use the default storage client (localStorage in the browser), you can pass "DEFAULT_STORE" as the value. Alternatively you can pass in {@link SessionSearchParam} for more control over how the leaves are stored and retrieved. + * @param chain - Optional, will be inferred if left unset + * @param txs - Optional, used only for validation while using Batched session type + * @returns Promise<{@link GetSessionParams}> * - * @example + * @example + * ```ts * import { createClient } from "viem" * import { createSmartAccountClient } from "@biconomy/account" * import { createWalletClient, http } from "viem"; @@ -1532,94 +1572,65 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * data: encodedCall * } * - * const { waitForTxHash } = await smartAccount.sendTransaction(transaction, { - * gasOffset: { - * verificationGasLimitOffsetPct: 25, // 25% increase for the already estimated gas limit - * preVerificationGasOffsetPct: 10 // 10% increase for the already estimated gas limit - * } - * }); + * const { waitForTxHash } = await smartAccount.sendTransaction(transaction); * const { transactionHash, userOperationReceipt } = await wait(); - * + * ``` */ - async sendTransaction( - manyOrOneTransactions: Transaction | Transaction[], - buildUseropDto?: BuildUserOpOptions, - sessionData?: Prettify>, - ): Promise { - let defaultedBuildUseropDto = { ...buildUseropDto } ?? {}; - - if (this.sessionType && sessionData) { - const getSessionParameters = await this.getSessionParams( - ...(sessionData ?? []), - ); - defaultedBuildUseropDto = { - ...defaultedBuildUseropDto, - ...getSessionParameters, - }; - } - - const userOp = await this.buildUserOp( - Array.isArray(manyOrOneTransactions) - ? manyOrOneTransactions - : [manyOrOneTransactions], - defaultedBuildUseropDto, - ); - - if (defaultedBuildUseropDto?.params?.danModuleInfo) { - defaultedBuildUseropDto.params.danModuleInfo.userOperation = { - ...userOp, - }; - } - - return this.sendUserOp(userOp, { ...defaultedBuildUseropDto?.params }); - } - - public async getSessionParams( - correspondingIndexes?: number[] | number | undefined | null, - conditionalSession?: SessionSearchParam, - chain?: Chain, - txs?: Transaction | Transaction[], - ): Promise<{ params: ModuleInfo }> { + public async getSessionParams({ + leafIndex, + store, + chain, + txs + }: GetSessionParams): Promise<{ params: ModuleInfo }> { + + const accountAddress = await this.getAccountAddress() const defaultedTransactions: Transaction[] | null = txs ? Array.isArray(txs) ? [...txs] : [txs] - : []; + : [] - const defaultedConditionalSession: SessionSearchParam = - conditionalSession ?? (await this.getAccountAddress()); + const defaultedConditionalSession: SessionSearchParam = store === "DEFAULT_STORE" ? getDefaultStorageClient(accountAddress) : + store ?? (await this.getAccountAddress()) - const defaultedCorrespondingIndexes: number[] | null = correspondingIndexes - ? Array.isArray(correspondingIndexes) - ? [...correspondingIndexes] - : [correspondingIndexes] - : null; + const defaultedCorrespondingIndexes: (number[] | null) = ["LAST_LEAF", "LAST_LEAVES"].includes(String(leafIndex)) ? null : leafIndex + ? (Array.isArray(leafIndex) + ? leafIndex + : [leafIndex]) as number[] + : null const correspondingIndex: number | null = defaultedCorrespondingIndexes ? defaultedCorrespondingIndexes[0] - : null; + : null const defaultedChain: Chain = - chain ?? getChain(await this.provider.getChainId()); + chain ?? getChain(await this.provider.getChainId()) - if (!defaultedChain) throw new Error("Chain is not provided"); + if (!defaultedChain) throw new Error("Chain is not provided") + if (this.sessionType === "DISTRIBUTED_KEY") { + return getDanSessionTxParams( + defaultedConditionalSession, + defaultedChain, + correspondingIndex + ) + } if (this.sessionType === "BATCHED") { return getBatchSessionTxParams( defaultedTransactions, defaultedCorrespondingIndexes, defaultedConditionalSession, - defaultedChain, - ); + defaultedChain + ) } if (this.sessionType === "STANDARD") { return getSingleSessionTxParams( defaultedConditionalSession, defaultedChain, - correspondingIndex, - ); + correspondingIndex + ) } - throw new Error("Session type is not provided"); + throw new Error("Session type is not provided") } /** @@ -2117,10 +2128,10 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { async signMessage(message: string | Uint8Array): Promise { // biome-ignore lint/suspicious/noExplicitAny: - let signature: any; - this.isActiveValidationModuleDefined(); - const dataHash = typeof message === "string" ? toBytes(message) : message; - signature = await this.activeValidationModule.signMessage(dataHash); + let signature: any + this.isActiveValidationModuleDefined() + const dataHash = typeof message === "string" ? toBytes(message) : message + signature = await this.activeValidationModule.signMessage(dataHash) const potentiallyIncorrectV = Number.parseInt(signature.slice(-2), 16); if (![27, 28].includes(potentiallyIncorrectV)) { @@ -2254,4 +2265,4 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { const modules: Array = result[0] as Array; return modules; } -} +} \ No newline at end of file diff --git a/src/account/utils/Types.ts b/src/account/utils/Types.ts index 5328427aa..28ccf1ae3 100644 --- a/src/account/utils/Types.ts +++ b/src/account/utils/Types.ts @@ -9,192 +9,188 @@ import type { SignableMessage, TypedData, TypedDataDefinition, - WalletClient, -} from "viem"; -import type { IBundler } from "../../bundler"; -import type { - BaseValidationModule, - ModuleInfo, - SessionType, -} from "../../modules"; + WalletClient +} from "viem" +import type { IBundler } from "../../bundler" +import type { BaseValidationModule, ModuleInfo, SessionSearchParam, SessionType } from "../../modules" import type { ISessionStorage, - SessionLeafNode, -} from "../../modules/interfaces/ISessionStorage"; + SessionLeafNode +} from "../../modules/interfaces/ISessionStorage" import type { FeeQuotesOrDataDto, IPaymaster, PaymasterFeeQuote, PaymasterMode, SmartAccountData, - SponsorUserOperationDto, -} from "../../paymaster"; + SponsorUserOperationDto +} from "../../paymaster" -export type EntryPointAddresses = Record; -export type BiconomyFactories = Record; -export type BiconomyImplementations = Record; -export type EntryPointAddressesByVersion = Record; -export type BiconomyFactoriesByVersion = Record; -export type BiconomyImplementationsByVersion = Record; +export type EntryPointAddresses = Record +export type BiconomyFactories = Record +export type BiconomyImplementations = Record +export type EntryPointAddressesByVersion = Record +export type BiconomyFactoriesByVersion = Record +export type BiconomyImplementationsByVersion = Record export type SmartAccountConfig = { /** entryPointAddress: address of the entry point */ - entryPointAddress: string; + entryPointAddress: string /** factoryAddress: address of the smart account factory */ - bundler?: IBundler; -}; + bundler?: IBundler +} export interface BalancePayload { /** address: The address of the account */ - address: string; + address: string /** chainId: The chainId of the network */ - chainId: number; + chainId: number /** amount: The amount of the balance */ - amount: bigint; + amount: bigint /** decimals: The number of decimals */ - decimals: number; + decimals: number /** formattedAmount: The amount of the balance formatted */ - formattedAmount: string; + formattedAmount: string } export interface WithdrawalRequest { /** The address of the asset */ - address: Hex; + address: Hex /** The amount to withdraw. Expects unformatted amount. Will use max amount if unset */ - amount?: bigint; + amount?: bigint /** The destination address of the funds. The second argument from the `withdraw(...)` function will be used as the default if left unset. */ - recipient?: Hex; + recipient?: Hex } export interface GasOverheads { /** fixed: fixed gas overhead */ - fixed: number; + fixed: number /** perUserOp: per user operation gas overhead */ - perUserOp: number; + perUserOp: number /** perUserOpWord: per user operation word gas overhead */ - perUserOpWord: number; + perUserOpWord: number /** zeroByte: per byte gas overhead */ - zeroByte: number; + zeroByte: number /** nonZeroByte: per non zero byte gas overhead */ - nonZeroByte: number; + nonZeroByte: number /** bundleSize: per signature bundleSize */ - bundleSize: number; + bundleSize: number /** sigSize: sigSize gas overhead */ - sigSize: number; + sigSize: number } export type BaseSmartAccountConfig = { /** index: helps to not conflict with other smart account instances */ - index?: number; + index?: number /** provider: WalletClientSigner from viem */ - provider?: WalletClient; + provider?: WalletClient /** entryPointAddress: address of the smart account entry point */ - entryPointAddress?: string; + entryPointAddress?: string /** accountAddress: address of the smart account, potentially counterfactual */ - accountAddress?: string; + accountAddress?: string /** overheads: {@link GasOverheads} */ - overheads?: Partial; + overheads?: Partial /** paymaster: {@link IPaymaster} interface */ - paymaster?: IPaymaster; + paymaster?: IPaymaster /** chainId: chainId of the network */ - chainId?: number; -}; + chainId?: number +} export type BiconomyTokenPaymasterRequest = { /** feeQuote: {@link PaymasterFeeQuote} */ - feeQuote: PaymasterFeeQuote; + feeQuote: PaymasterFeeQuote /** spender: The address of the spender who is paying for the transaction, this can usually be set to feeQuotesResponse.tokenPaymasterAddress */ - spender: Hex; + spender: Hex /** maxApproval: If set to true, the paymaster will approve the maximum amount of tokens required for the transaction. Not recommended */ - maxApproval?: boolean; + maxApproval?: boolean /* skip option to patch callData if approval is already given to the paymaster */ - skipPatchCallData?: boolean; -}; + skipPatchCallData?: boolean +} export type RequireAtLeastOne = Pick< T, Exclude > & { - [K in Keys]-?: Required> & Partial>>; - }[Keys]; + [K in Keys]-?: Required> & Partial>> + }[Keys] export type ConditionalBundlerProps = RequireAtLeastOne< { - bundler: IBundler; - bundlerUrl: string; + bundler: IBundler + bundlerUrl: string }, "bundler" | "bundlerUrl" ->; +> export type ResolvedBundlerProps = { - bundler: IBundler; -}; + bundler: IBundler +} export type ConditionalValidationProps = RequireAtLeastOne< { - defaultValidationModule: BaseValidationModule; - signer: SupportedSigner; + defaultValidationModule: BaseValidationModule + signer: SupportedSigner }, "defaultValidationModule" | "signer" ->; +> export type ResolvedValidationProps = { /** defaultValidationModule: {@link BaseValidationModule} */ - defaultValidationModule: BaseValidationModule; + defaultValidationModule: BaseValidationModule /** activeValidationModule: {@link BaseValidationModule}. The active validation module. Will default to the defaultValidationModule */ - activeValidationModule: BaseValidationModule; + activeValidationModule: BaseValidationModule /** signer: ethers Wallet, viemWallet or alchemys SmartAccountSigner */ - signer: SmartAccountSigner; + signer: SmartAccountSigner /** chainId: chainId of the network */ - chainId: number; -}; + chainId: number +} export type BiconomySmartAccountV2ConfigBaseProps = { /** Factory address of biconomy factory contract or some other contract you have deployed on chain */ - factoryAddress?: Hex; + factoryAddress?: Hex /** Sender address: If you want to override the Signer address with some other address and get counterfactual address can use this to pass the EOA and get SA address */ - senderAddress?: Hex; + senderAddress?: Hex /** implementation of smart contract address or some other contract you have deployed and want to override */ - implementationAddress?: Hex; + implementationAddress?: Hex /** defaultFallbackHandler: override the default fallback contract address */ - defaultFallbackHandler?: Hex; + defaultFallbackHandler?: Hex /** rpcUrl: Rpc url, optional, we set default rpc url if not passed. */ - rpcUrl?: string; // as good as Provider + rpcUrl?: string // as good as Provider /** paymasterUrl: The Paymaster URL retrieved from the Biconomy dashboard */ - paymasterUrl?: string; + paymasterUrl?: string /** biconomyPaymasterApiKey: The API key retrieved from the Biconomy dashboard */ - biconomyPaymasterApiKey?: string; + biconomyPaymasterApiKey?: string /** activeValidationModule: The active validation module. Will default to the defaultValidationModule */ - activeValidationModule?: BaseValidationModule; + activeValidationModule?: BaseValidationModule /** scanForUpgradedAccountsFromV1: set to true if you you want the userwho was using biconomy SA v1 to upgrade to biconomy SA v2 */ - scanForUpgradedAccountsFromV1?: boolean; + scanForUpgradedAccountsFromV1?: boolean /** the index of SA the EOA have generated and till which indexes the upgraded SA should scan */ - maxIndexForScan?: number; + maxIndexForScan?: number /** Can be used to optionally override the chain with a custom chain if it doesn't already exist in viems list of supported chains. Alias of customChain */ - viemChain?: Chain; + viemChain?: Chain /** Can be used to optionally override the chain with a custom chain if it doesn't already exist in viems list of supported chain. Alias of viemChain */ - customChain?: Chain; + customChain?: Chain /** The initial code to be used for the smart account */ - initCode?: Hex; + initCode?: Hex /** Used for session key manager module */ - sessionData?: ModuleInfo; + sessionData?: ModuleInfo /** Used to skip the chain checks between singer, bundler and paymaster */ - skipChainCheck?: boolean; + skipChainCheck?: boolean /** The type of the relevant session. Used with createSessionSmartAccountClient */ sessionType?: SessionType; - /** The storage client to be used for storing the session data */ - sessionStorageClient?: ISessionStorage -}; + /** The sessionStorageClient used for persisting and retrieving session data */ + sessionStorageClient?: ISessionStorage; +} export type BiconomySmartAccountV2Config = BiconomySmartAccountV2ConfigBaseProps & BaseSmartAccountConfig & ConditionalBundlerProps & - ConditionalValidationProps; + ConditionalValidationProps export type BiconomySmartAccountV2ConfigConstructorProps = BiconomySmartAccountV2ConfigBaseProps & BaseSmartAccountConfig & ResolvedBundlerProps & - ResolvedValidationProps; + ResolvedValidationProps /** * Represents options for building a user operation. @@ -209,30 +205,41 @@ export type BiconomySmartAccountV2ConfigConstructorProps = * @property {boolean} [useEmptyDeployCallData] - Set to true if the transaction is being used only to deploy the smart contract, so "0x" is set as the user operation call data. */ export type BuildUserOpOptions = { - gasOffset?: GasOffsetPct; - params?: ModuleInfo; - nonceOptions?: NonceOptions; - forceEncodeForBatch?: boolean; - paymasterServiceData?: PaymasterUserOperationDto; - simulationType?: SimulationType; - stateOverrideSet?: StateOverrideSet; - dummyPndOverride?: BytesLike; - useEmptyDeployCallData?: boolean; -}; + gasOffset?: GasOffsetPct + params?: ModuleInfo + nonceOptions?: NonceOptions + forceEncodeForBatch?: boolean + paymasterServiceData?: PaymasterUserOperationDto + simulationType?: SimulationType + stateOverrideSet?: StateOverrideSet + dummyPndOverride?: BytesLike + useEmptyDeployCallData?: boolean +} + +export type GetSessionParams = { + /** The index of the session leaf(ves) from the session storage client */ + leafIndex: number[] | number | undefined | null | "LAST_LEAF" | "LAST_LEAVES", + /** The session search parameter, can be a full {@link Session}, {@link ISessionStorage} or a smartAccount address */ + store?: SessionSearchParam | "DEFAULT_STORE", + /** The chain to use */ + chain?: Chain, + /** the txs being submitted */ + txs?: Transaction | Transaction[] +} export type SessionDataForAccount = { - sessionStorageClient: ISessionStorage; - session: SessionLeafNode; -}; + sessionStorageClient: ISessionStorage + session: SessionLeafNode +} export type NonceOptions = { /** nonceKey: The key to use for nonce */ - nonceKey?: number; + nonceKey?: number /** nonceOverride: The nonce to use for the transaction */ - nonceOverride?: number; -}; + nonceOverride?: number +} -export type SimulationType = "validation" | "validation_and_execution"; +export type SimulationType = "validation" | "validation_and_execution" /** * Represents an offset percentage value used for gas-related calculations. @@ -253,177 +260,177 @@ export type SimulationType = "validation" | "validation_and_execution"; * @property {number} [maxPriorityFeePerGasOffsetPct] - Percentage offset for the maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas). */ export type GasOffsetPct = { - callGasLimitOffsetPct?: number; - verificationGasLimitOffsetPct?: number; - preVerificationGasOffsetPct?: number; - maxFeePerGasOffsetPct?: number; - maxPriorityFeePerGasOffsetPct?: number; -}; + callGasLimitOffsetPct?: number + verificationGasLimitOffsetPct?: number + preVerificationGasOffsetPct?: number + maxFeePerGasOffsetPct?: number + maxPriorityFeePerGasOffsetPct?: number +} export type InitilizationData = { - accountIndex?: number; - signerAddress?: string; -}; + accountIndex?: number + signerAddress?: string +} export type PaymasterUserOperationDto = SponsorUserOperationDto & FeeQuotesOrDataDto & { /** mode: sponsored or erc20 */ - mode: PaymasterMode; + mode: PaymasterMode /** Always recommended, especially when using token paymaster */ - calculateGasLimits?: boolean; + calculateGasLimits?: boolean /** Expiry duration in seconds */ - expiryDuration?: number; + expiryDuration?: number /** Webhooks to be fired after user op is sent */ // biome-ignore lint/suspicious/noExplicitAny: - webhookData?: Record; + webhookData?: Record /** Smart account meta data */ - smartAccountInfo?: SmartAccountData; + smartAccountInfo?: SmartAccountData /** the fee-paying token address */ - feeTokenAddress?: string; + feeTokenAddress?: string /** The fee quote */ - feeQuote?: PaymasterFeeQuote; + feeQuote?: PaymasterFeeQuote /** The address of the spender. This is usually set to FeeQuotesOrDataResponse.tokenPaymasterAddress */ - spender?: Hex; + spender?: Hex /** Not recommended */ - maxApproval?: boolean; + maxApproval?: boolean /* skip option to patch callData if approval is already given to the paymaster */ - skipPatchCallData?: boolean; - }; + skipPatchCallData?: boolean + } export type InitializeV2Data = { - accountIndex?: number; -}; + accountIndex?: number +} export type EstimateUserOpGasParams = { - userOp: Partial; + userOp: Partial /** Currrently has no effect */ // skipBundlerGasEstimation?: boolean; /** paymasterServiceData: Options specific to transactions that involve a paymaster */ - paymasterServiceData?: SponsorUserOperationDto; -}; + paymasterServiceData?: SponsorUserOperationDto +} export interface TransactionDetailsForUserOp { /** target: The address of the contract to call */ - target: string; + target: string /** data: The data to send to the contract */ - data: string; + data: string /** value: The value to send to the contract */ - value?: BigNumberish; + value?: BigNumberish /** gasLimit: The gas limit to use for the transaction */ - gasLimit?: BigNumberish; + gasLimit?: BigNumberish /** maxFeePerGas: The maximum fee per gas to use for the transaction */ - maxFeePerGas?: BigNumberish; + maxFeePerGas?: BigNumberish /** maxPriorityFeePerGas: The maximum priority fee per gas to use for the transaction */ - maxPriorityFeePerGas?: BigNumberish; + maxPriorityFeePerGas?: BigNumberish /** nonce: The nonce to use for the transaction */ - nonce?: BigNumberish; + nonce?: BigNumberish } export type CounterFactualAddressParam = { - index?: number; - validationModule?: BaseValidationModule; + index?: number + validationModule?: BaseValidationModule /** scanForUpgradedAccountsFromV1: set to true if you you want the userwho was using biconomy SA v1 to upgrade to biconomy SA v2 */ - scanForUpgradedAccountsFromV1?: boolean; + scanForUpgradedAccountsFromV1?: boolean /** the index of SA the EOA have generated and till which indexes the upgraded SA should scan */ - maxIndexForScan?: number; -}; + maxIndexForScan?: number +} export type QueryParamsForAddressResolver = { - eoaAddress: Hex; - index: number; - moduleAddress: Hex; - moduleSetupData: Hex; - maxIndexForScan?: number; -}; + eoaAddress: Hex + index: number + moduleAddress: Hex + moduleSetupData: Hex + maxIndexForScan?: number +} export type SmartAccountInfo = { /** accountAddress: The address of the smart account */ - accountAddress: Hex; + accountAddress: Hex /** factoryAddress: The address of the smart account factory */ - factoryAddress: Hex; + factoryAddress: Hex /** currentImplementation: The address of the current implementation */ - currentImplementation: string; + currentImplementation: string /** currentVersion: The version of the smart account */ - currentVersion: string; + currentVersion: string /** factoryVersion: The version of the factory */ - factoryVersion: string; + factoryVersion: string /** deploymentIndex: The index of the deployment */ - deploymentIndex: BigNumberish; -}; + deploymentIndex: BigNumberish +} export type ValueOrData = RequireAtLeastOne< { - value: BigNumberish | string; - data: string; + value: BigNumberish | string + data: string }, "value" | "data" ->; +> export type Transaction = { - to: string; -} & ValueOrData; + to: string +} & ValueOrData export type SupportedToken = Omit< PaymasterFeeQuote, "maxGasFeeUSD" | "usdPayment" | "maxGasFee" | "validUntil" -> & { balance: BalancePayload }; +> & { balance: BalancePayload } export type Signer = LightSigner & { // biome-ignore lint/suspicious/noExplicitAny: any is used here to allow for the ethers provider - provider: any; -}; -export type SupportedSignerName = "alchemy" | "ethers" | "viem"; + provider: any +} +export type SupportedSignerName = "alchemy" | "ethers" | "viem" export type SupportedSigner = | SmartAccountSigner | WalletClient | Signer | LightSigner - | PrivateKeyAccount; -export type Service = "Bundler" | "Paymaster"; + | PrivateKeyAccount +export type Service = "Bundler" | "Paymaster" export interface LightSigner { - getAddress(): Promise; - signMessage(message: string | Uint8Array): Promise; + getAddress(): Promise + signMessage(message: string | Uint8Array): Promise } export type StateOverrideSet = { [key: string]: { - balance?: string; - nonce?: string; - code?: string; - state?: object; - stateDiff?: object; - }; -}; + balance?: string + nonce?: string + code?: string + state?: object + stateDiff?: object + } +} -export type BigNumberish = Hex | number | bigint; -export type BytesLike = Uint8Array | Hex; +export type BigNumberish = Hex | number | bigint +export type BytesLike = Uint8Array | Hex //#region UserOperationStruct // based on @account-abstraction/common // this is used for building requests export interface UserOperationStruct { /* the origin of the request */ - sender: string; + sender: string /* nonce of the transaction, returned from the entry point for this Address */ - nonce: BigNumberish; + nonce: BigNumberish /* the initCode for creating the sender if it does not exist yet, otherwise "0x" */ - initCode: BytesLike | "0x"; + initCode: BytesLike | "0x" /* the callData passed to the target */ - callData: BytesLike; + callData: BytesLike /* Value used by inner account execution */ - callGasLimit?: BigNumberish; + callGasLimit?: BigNumberish /* Actual gas used by the validation of this UserOperation */ - verificationGasLimit?: BigNumberish; + verificationGasLimit?: BigNumberish /* Gas overhead of this UserOperation */ - preVerificationGas?: BigNumberish; + preVerificationGas?: BigNumberish /* Maximum fee per gas (similar to EIP-1559 max_fee_per_gas) */ - maxFeePerGas?: BigNumberish; + maxFeePerGas?: BigNumberish /* Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas) */ - maxPriorityFeePerGas?: BigNumberish; + maxPriorityFeePerGas?: BigNumberish /* Address of paymaster sponsoring the transaction, followed by extra data to send to the paymaster ("0x" for self-sponsored transaction) */ - paymasterAndData: BytesLike | "0x"; + paymasterAndData: BytesLike | "0x" /* Data passed into the account along with the nonce during the verification step */ - signature: BytesLike; + signature: BytesLike } //#endregion UserOperationStruct @@ -443,19 +450,19 @@ export interface UserOperationStruct { // biome-ignore lint/suspicious/noExplicitAny: export interface SmartAccountSigner { - signerType: string; - inner: Inner; + signerType: string + inner: Inner - getAddress: () => Promise
; + getAddress: () => Promise
- signMessage: (message: SignableMessage) => Promise; + signMessage: (message: SignableMessage) => Promise signTypedData: < const TTypedData extends TypedData | { [key: string]: unknown }, - TPrimaryType extends string = string, + TPrimaryType extends string = string >( - params: TypedDataDefinition, - ) => Promise; + params: TypedDataDefinition + ) => Promise } //#endregion SmartAccountSigner @@ -463,47 +470,47 @@ export interface SmartAccountSigner { export type UserOperationCallData = | { /* the target of the call */ - target: Address; + target: Address /* the data passed to the target */ - data: Hex; + data: Hex /* the amount of native token to send to the target (default: 0) */ - value?: bigint; + value?: bigint } - | Hex; + | Hex //#endregion UserOperationCallData //#region BatchUserOperationCallData -export type BatchUserOperationCallData = Exclude[]; +export type BatchUserOperationCallData = Exclude[] //#endregion BatchUserOperationCallData -export type SignTypedDataParams = Omit; +export type SignTypedDataParams = Omit export type BasSmartContractAccountProps = BiconomySmartAccountV2ConfigConstructorProps & { /** chain: The chain from viem */ - chain: Chain; + chain: Chain /** rpcClient: The rpc url string */ - rpcClient: string; + rpcClient: string /** factoryAddress: The address of the factory */ - factoryAddress: Hex; + factoryAddress: Hex /** entryPointAddress: The address of the entry point */ - entryPointAddress: Hex; + entryPointAddress: Hex /** accountAddress: The address of the account */ - accountAddress?: Address; - }; + accountAddress?: Address + } export interface ISmartContractAccount< - TSigner extends SmartAccountSigner = SmartAccountSigner, + TSigner extends SmartAccountSigner = SmartAccountSigner > { /** * The RPC provider the account uses to make RPC calls */ - readonly rpcProvider: PublicClient; + readonly rpcProvider: PublicClient /** * @returns the init code for the account */ - getInitCode(): Promise; + getInitCode(): Promise /** * This is useful for estimating gas costs. It should return a signature that doesn't cause the account to revert @@ -511,7 +518,7 @@ export interface ISmartContractAccount< * * @returns a dummy signature that doesn't cause the account to revert during estimation */ - getDummySignature(): Hex; + getDummySignature(): Hex /** * Encodes a call to the account's execute function. @@ -520,7 +527,7 @@ export interface ISmartContractAccount< * @param value - optionally the amount of native token to send * @param data - the call data or "0x" if empty */ - encodeExecute(target: string, value: bigint, data: string): Promise; + encodeExecute(target: string, value: bigint, data: string): Promise /** * Encodes a batch of transactions to the account's batch execute function. @@ -528,12 +535,12 @@ export interface ISmartContractAccount< * @param txs - An Array of objects containing the target, value, and data for each transaction * @returns the encoded callData for a UserOperation */ - encodeBatchExecute(txs: BatchUserOperationCallData): Promise; + encodeBatchExecute(txs: BatchUserOperationCallData): Promise /** * @returns the nonce of the account */ - getNonce(): Promise; + getNonce(): Promise /** * If your account handles 1271 signatures of personal_sign differently @@ -542,7 +549,7 @@ export interface ISmartContractAccount< * @param uoHash -- The hash of the UserOperation to sign * @returns the signature of the UserOperation */ - signUserOperationHash(uoHash: Hash): Promise; + signUserOperationHash(uoHash: Hash): Promise /** * Returns a signed and prefixed message. @@ -550,7 +557,7 @@ export interface ISmartContractAccount< * @param msg - the message to sign * @returns the signature of the message */ - signMessage(msg: string | Uint8Array | Hex): Promise; + signMessage(msg: string | Uint8Array | Hex): Promise /** * Signs a typed data object as per ERC-712 @@ -566,7 +573,7 @@ export interface ISmartContractAccount< * @param msg - the message to sign * @returns ths signature wrapped in 6492 format */ - signMessageWith6492(msg: string | Uint8Array | Hex): Promise; + signMessageWith6492(msg: string | Uint8Array | Hex): Promise /** * If the account is not deployed, it will sign the typed data blob and then wrap it in 6492 format @@ -579,7 +586,7 @@ export interface ISmartContractAccount< /** * @returns the address of the account */ - getAddress(): Promise
; + getAddress(): Promise
/** * @returns the current account signer instance that the smart account client @@ -588,17 +595,17 @@ export interface ISmartContractAccount< * The signer is expected to be the owner or one of the owners of the account * for the signatures to be valid for the acting account. */ - getSigner(): TSigner; + getSigner(): TSigner /** * @returns the address of the factory contract for the smart account */ - getFactoryAddress(): Address; + getFactoryAddress(): Address /** * @returns the address of the entry point contract for the smart account */ - getEntryPointAddress(): Address; + getEntryPointAddress(): Address /** * Allows you to add additional functionality and utility methods to this account @@ -625,14 +632,14 @@ export interface ISmartContractAccount< * with the extension methods * @returns -- the account with the extension methods added */ - extend: (extendFn: (self: this) => R) => this & R; + extend: (extendFn: (self: this) => R) => this & R encodeUpgradeToAndCall: ( upgradeToImplAddress: Address, - upgradeToInitData: Hex, - ) => Promise; + upgradeToInitData: Hex + ) => Promise } export type TransferOwnershipCompatibleModule = | "0x0000001c5b32F37F5beA87BDD5374eB2aC54eA8e" - | "0x000000824dc138db84FD9109fc154bdad332Aa8E"; + | "0x000000824dc138db84FD9109fc154bdad332Aa8E" diff --git a/src/account/utils/convertSigner.ts b/src/account/utils/convertSigner.ts index f9a895692..e4370eb1f 100644 --- a/src/account/utils/convertSigner.ts +++ b/src/account/utils/convertSigner.ts @@ -77,7 +77,8 @@ export const convertSigner = async ( chainId = walletClient.chain.id } // convert viems walletClient to alchemy's SmartAccountSigner under the hood - resolvedSmartAccountSigner = new WalletClientSigner(walletClient, "viem") + resolvedSmartAccountSigner = new WalletClientSigner(walletClient, "viem"); + rpcUrl = walletClient?.transport?.url ?? undefined } else if (isPrivateKeyAccount(signer)) { if (rpcUrl !== null && rpcUrl !== undefined) { diff --git a/src/modules/DANSessionKeyManagerModule.ts b/src/modules/DANSessionKeyManagerModule.ts new file mode 100644 index 000000000..b9de92339 --- /dev/null +++ b/src/modules/DANSessionKeyManagerModule.ts @@ -0,0 +1,375 @@ +import { MerkleTree } from "merkletreejs" +import { + type Hex, + concat, + encodeAbiParameters, + encodeFunctionData, + keccak256, + pad, + parseAbi, + parseAbiParameters, + // toBytes, + toHex +} from "viem" +import { DEFAULT_ENTRYPOINT_ADDRESS, type SmartAccountSigner, type UserOperationStruct } from "../account" +import { BaseValidationModule } from "./BaseValidationModule.js" +import { danSDK } from "./index.js" +import type { + ISessionStorage, + SessionLeafNode, + SessionSearchParam, + SessionStatus +} from "./interfaces/ISessionStorage.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 +} from "./utils/Constants.js" +import { + type CreateSessionDataParams, + type CreateSessionDataResponse, + type DanSignatureObject, + type ModuleInfo, + type ModuleVersion, + type SessionKeyManagerModuleConfig, + type SessionParams, + StorageType +} from "./utils/Types.js" +import { generateRandomHex } from "./utils/Uid.js" + +export type WalletProviderDefs = { + walletProviderId: string + walletProviderUrl: string +} + +export type Config = { + walletProvider: WalletProviderDefs +} + +export type SendUserOpArgs = SessionParams & { rawUserOperation: Partial } + +export class DANSessionKeyManagerModule extends BaseValidationModule { + version: ModuleVersion = "V1_0_0" + + moduleAddress!: Hex + + merkleTree!: MerkleTree + + sessionStorageClient!: ISessionStorage + + readonly mockEcdsaSessionKeySig: Hex = + "0x73c3ac716c487ca34bb858247b5ccf1dc354fbaabdd089af3b2ac8e78ba85a4959a2d76250325bd67c11771c31fccda87c33ceec17cc0de912690521bb95ffcb1b" + + /** + * This constructor is private. Use the static create method to instantiate SessionKeyManagerModule + * @param moduleConfig The configuration for the module + * @returns An instance of SessionKeyManagerModule + */ + private constructor(moduleConfig: SessionKeyManagerModuleConfig) { + super(moduleConfig) + } + + /** + * Asynchronously creates and initializes an instance of SessionKeyManagerModule + * @param moduleConfig The configuration for the module + * @returns A Promise that resolves to an instance of SessionKeyManagerModule + */ + public static async create( + moduleConfig: SessionKeyManagerModuleConfig + ): Promise { + // TODO: (Joe) stop doing things in a 'create' call after the instance has been created + const instance = new DANSessionKeyManagerModule(moduleConfig) + + if (moduleConfig.moduleAddress) { + instance.moduleAddress = moduleConfig.moduleAddress + } else if (moduleConfig.version) { + const moduleAddr = SESSION_MANAGER_MODULE_ADDRESSES_BY_VERSION[ + moduleConfig.version + ] as Hex + if (!moduleAddr) { + throw new Error(`Invalid version ${moduleConfig.version}`) + } + instance.moduleAddress = moduleAddr + instance.version = moduleConfig.version as ModuleVersion + } else { + instance.moduleAddress = DEFAULT_SESSION_KEY_MANAGER_MODULE + // Note: in this case Version remains the default one + } + + if (moduleConfig.sessionStorageClient) { + instance.sessionStorageClient = moduleConfig.sessionStorageClient + } else { + switch (moduleConfig.storageType) { + case StorageType.MEMORY_STORAGE: + instance.sessionStorageClient = new SessionMemoryStorage( + moduleConfig.smartAccountAddress + ) + break + case StorageType.LOCAL_STORAGE: + instance.sessionStorageClient = new SessionLocalStorage( + moduleConfig.smartAccountAddress + ) + break + default: + instance.sessionStorageClient = new SessionLocalStorage( + moduleConfig.smartAccountAddress + ) + } + } + + const existingSessionData = + await instance.sessionStorageClient.getAllSessionData() + const existingSessionDataLeafs = existingSessionData.map((sessionData) => { + const leafDataHex = concat([ + pad(toHex(sessionData.validUntil), { size: 6 }), + pad(toHex(sessionData.validAfter), { size: 6 }), + pad(sessionData.sessionValidationModule, { size: 20 }), + sessionData.sessionKeyData + ]) + return keccak256(leafDataHex) + }) + + instance.merkleTree = new MerkleTree(existingSessionDataLeafs, keccak256, { + sortPairs: true, + hashLeaves: false + }) + + return instance + } + + /** + * Method to create session data for any module. The session data is used to create a leaf in the merkle tree + * @param leavesData The data of one or more leaves to be used to create session data + * @returns The session data + */ + createSessionData = async ( + leavesData: CreateSessionDataParams[] + ): Promise => { + const sessionKeyManagerModuleABI = parseAbi([ + "function setMerkleRoot(bytes32 _merkleRoot)" + ]) + + const leavesToAdd: Buffer[] = [] + const sessionIDInfo: string[] = [] + + for (const leafData of leavesData) { + const leafDataHex = concat([ + pad(toHex(leafData.validUntil), { size: 6 }), + pad(toHex(leafData.validAfter), { size: 6 }), + pad(leafData.sessionValidationModule, { size: 20 }), + leafData.sessionKeyData + ]) + + const generatedSessionId = + leafData.preferredSessionId ?? generateRandomHex() + + // TODO: verify this, might not be buffer + leavesToAdd.push(keccak256(leafDataHex) as unknown as Buffer) + sessionIDInfo.push(generatedSessionId) + + const sessionLeafNode = { + ...leafData, + sessionID: generatedSessionId, + status: "PENDING" as SessionStatus + } + + await this.sessionStorageClient.addSessionData(sessionLeafNode) + } + + this.merkleTree.addLeaves(leavesToAdd) + + const leaves = this.merkleTree.getLeaves() + + const newMerkleTree = new MerkleTree(leaves, keccak256, { + sortPairs: true, + hashLeaves: false + }) + + this.merkleTree = newMerkleTree + + const setMerkleRootData = encodeFunctionData({ + abi: sessionKeyManagerModuleABI, + functionName: "setMerkleRoot", + args: [this.merkleTree.getHexRoot() as Hex] + }) + + await this.sessionStorageClient.setMerkleRoot(this.merkleTree.getHexRoot()) + return { + data: setMerkleRootData, + sessionIDInfo: sessionIDInfo + } + } + + /** + * This method is used to sign the user operation using the session signer + * @param userOp The user operation to be signed + * @param sessionSigner The signer to be used to sign the user operation + * @returns The signature of the user operation + */ + async signUserOpHash(_: string, { sessionID, rawUserOperation, additionalSessionData }: SendUserOpArgs): Promise { + const sessionSignerData = await this.getLeafInfo({ sessionID }) + + if (!rawUserOperation) throw new Error("Missing userOperation") + if (!sessionID) throw new Error("Missing sessionID") + if (!sessionSignerData.danModuleInfo) throw new Error("Missing danModuleInfo") + + if ( + !rawUserOperation.verificationGasLimit || + !rawUserOperation.callGasLimit || + !rawUserOperation.callData || + !rawUserOperation.paymasterAndData || + !rawUserOperation.initCode + ) { + throw new Error("Missing params from User operation") + } + + const userOpTemp = { + ...rawUserOperation, + verificationGasLimit: rawUserOperation.verificationGasLimit.toString(), + callGasLimit: rawUserOperation.callGasLimit.toString(), + callData: rawUserOperation.callData.slice(2), + paymasterAndData: rawUserOperation.paymasterAndData.slice(2), + initCode: String(rawUserOperation.initCode).slice(2) + } + + const objectToSign: DanSignatureObject = { + // @ts-ignore + userOperation: userOpTemp, + entryPointVersion: "v0.6.0", + entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, + chainId: sessionSignerData.danModuleInfo.chainId + } + const messageToSign = JSON.stringify(objectToSign) + + const signature = await danSDK.signMessage(messageToSign, sessionSignerData.danModuleInfo) + + const leafDataHex = concat([ + pad(toHex(sessionSignerData.validUntil), { size: 6 }), + pad(toHex(sessionSignerData.validAfter), { size: 6 }), + pad(sessionSignerData.sessionValidationModule, { size: 20 }), + sessionSignerData.sessionKeyData + ]) + + // Generate the padded signature with (validUntil,validAfter,sessionVerificationModuleAddress,validationData,merkleProof,signature) + let paddedSignature: Hex = encodeAbiParameters( + parseAbiParameters("uint48, uint48, address, bytes, bytes32[], bytes"), + [ + sessionSignerData.validUntil, + sessionSignerData.validAfter, + sessionSignerData.sessionValidationModule, + sessionSignerData.sessionKeyData, + this.merkleTree.getHexProof(keccak256(leafDataHex)) as Hex[], + signature as Hex + ] + ) + + if (additionalSessionData) { + paddedSignature += additionalSessionData + } + + return paddedSignature as Hex + } + + private async getLeafInfo(params: ModuleInfo): Promise { + if (params?.sessionID) { + const matchedDatum = await this.sessionStorageClient.getSessionData({ + sessionID: params.sessionID + }) + if (matchedDatum) { + return matchedDatum + } + } + throw new Error("Session data not found") + } + + /** + * Update the session data pending state to active + * @param param The search param to find the session data + * @param status The status to be updated + * @returns + */ + async updateSessionStatus( + param: SessionSearchParam, + status: SessionStatus + ): Promise { + this.sessionStorageClient.updateSessionStatus(param, status) + } + + /** + * @remarks This method is used to clear all the pending sessions + * @returns + */ + async clearPendingSessions(): Promise { + this.sessionStorageClient.clearPendingSessions() + } + + /** + * @returns SessionKeyManagerModule address + */ + getAddress(): Hex { + return this.moduleAddress + } + + /** + * @remarks This is the version of the module contract + */ + async getSigner(): Promise { + throw new Error("Method not implemented.") + } + + /** + * @remarks This is the dummy signature for the module, used in buildUserOp for bundler estimation + * @returns Dummy signature + */ + async getDummySignature(params?: ModuleInfo): Promise { + if (!params) { + throw new Error("Params must be provided.") + } + + const sessionSignerData = await this.getLeafInfo(params) + const leafDataHex = concat([ + pad(toHex(sessionSignerData.validUntil), { size: 6 }), + pad(toHex(sessionSignerData.validAfter), { size: 6 }), + pad(sessionSignerData.sessionValidationModule, { size: 20 }), + sessionSignerData.sessionKeyData + ]) + + // Generate the padded signature with (validUntil,validAfter,sessionVerificationModuleAddress,validationData,merkleProof,signature) + let paddedSignature: Hex = encodeAbiParameters( + parseAbiParameters("uint48, uint48, address, bytes, bytes32[], bytes"), + [ + sessionSignerData.validUntil, + sessionSignerData.validAfter, + sessionSignerData.sessionValidationModule, + sessionSignerData.sessionKeyData, + this.merkleTree.getHexProof(keccak256(leafDataHex)) as Hex[], + this.mockEcdsaSessionKeySig + ] + ) + if (params?.additionalSessionData) { + paddedSignature += params.additionalSessionData + } + + const dummySig = encodeAbiParameters( + parseAbiParameters(["bytes, address"]), + [paddedSignature as Hex, this.getAddress()] + ) + + return dummySig + } + + /** + * @remarks Other modules may need additional attributes to build init data + */ + async getInitData(): Promise { + throw new Error("Method not implemented.") + } + + /** + * @remarks This Module dont have knowledge of signer. So, this method is not implemented + */ + async signMessage(_message: Uint8Array | string): Promise { + throw new Error("Method not implemented.") + } +} diff --git a/src/modules/index.ts b/src/modules/index.ts index c5d7f8276..50877d085 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -15,6 +15,7 @@ export * from "./sessions/batch.js" export * from "./sessions/dan.js" export * from "./sessions/sessionSmartAccountClient.js" export * from "./session-storage/index.js" +import { DANSessionKeyManagerModule } from "./DANSessionKeyManagerModule.js" import { BatchedSessionRouterModule, ECDSAOwnershipValidationModule, @@ -29,6 +30,8 @@ export const createMultiChainValidationModule = export const createECDSAOwnershipValidationModule = ECDSAOwnershipValidationModule.create export const createSessionKeyManagerModule = SessionKeyManagerModule.create +export const createDANSessionKeyManagerModule = + DANSessionKeyManagerModule.create export const createERC20SessionValidationModule = ERC20SessionValidationModule.create diff --git a/src/modules/sessions/abi.ts b/src/modules/sessions/abi.ts index faa1c078b..1b9741566 100644 --- a/src/modules/sessions/abi.ts +++ b/src/modules/sessions/abi.ts @@ -7,20 +7,16 @@ import { pad, slice, toFunctionSelector, - toHex -} from "viem" + toHex, +} from "viem"; import { + type CreateSessionDataParams, + type DanModuleInfo, + type SessionParams, createSessionKeyManagerModule, didProvideFullSession, - resumeSession -} from "../" - -import type { - CreateSessionDataParams, - DanModuleInfo, - SessionParams, -} from "../utils/Types" - + resumeSession, +} from "../"; import { type BiconomySmartAccountV2, type BuildUserOpOptions, @@ -35,34 +31,34 @@ import type { ISessionStorage } from "../interfaces/ISessionStorage" import { createSessionKeyEOA } from "../session-storage/utils" import { DEFAULT_ABI_SVM_MODULE, - DEFAULT_SESSION_KEY_MANAGER_MODULE -} from "../utils/Constants" -import type { Permission, SessionSearchParam } from "../utils/Helper" -import type { DeprecatedPermission, Rule } from "../utils/Helper" + DEFAULT_SESSION_KEY_MANAGER_MODULE, +} from "../utils/Constants"; +import type { Permission, SessionSearchParam } from "../utils/Helper"; +import type { DeprecatedPermission, Rule } from "../utils/Helper"; export type SessionConfig = { - usersAccountAddress: Hex - smartAccount: BiconomySmartAccountV2 -} + usersAccountAddress: Hex; + smartAccount: BiconomySmartAccountV2; +}; export type Session = { /** The storage client specific to the smartAccountAddress which stores the session keys */ - sessionStorageClient: ISessionStorage + sessionStorageClient: ISessionStorage; /** The relevant sessionID for the chosen session */ - sessionIDInfo: string[] -} + sessionIDInfo: string[]; +}; export type SessionEpoch = { /** The time at which the session is no longer valid */ - validUntil?: number + validUntil?: number; /** The time at which the session becomes valid */ - validAfter?: number -} + validAfter?: number; +}; export const PolicyHelpers = { Indefinitely: { validUntil: 0, validAfter: 0 }, - NoValueLimit: 0n -} + NoValueLimit: 0n, +}; const RULE_CONDITIONS = [ "EQUAL", "LASS_THAN_OR_EQUAL", @@ -70,34 +66,33 @@ const RULE_CONDITIONS = [ "GREATER_THAN_OR_EQUAL", "GREATER_THAN", "NOT_EQUAL" -] as const - -export type RuleCondition = typeof RULE_CONDITIONS[number] +]; +type RuleCondition = "EQUAL" | "LASS_THAN_OR_EQUAL" | "LESS_THAN" | "GREATER_THAN_OR_EQUAL" | "GREATER_THAN" | "NOT_EQUAL"; export const RuleHelpers = { - OffsetByIndex: (i: number): number => i * 32, - Condition: (condition: RuleCondition): number => RULE_CONDITIONS.map(r => r.toUpperCase()).indexOf(condition.toUpperCase()) -} + OffsetByIndex: (i: number) => i * 32, + Condition: (condition: RuleCondition) => RULE_CONDITIONS.indexOf(condition), +}; export type PolicyWithOptionalSessionKey = Omit & { - sessionKeyAddress?: Hex -} + sessionKeyAddress?: Hex; +}; export type Policy = { /** The address of the contract to be included in the policy */ - contractAddress: Hex + contractAddress: Hex; /** The address of the sessionKey upon which the policy is to be imparted */ - sessionKeyAddress: Hex + sessionKeyAddress: Hex; /** The specific function selector from the contract to be included in the policy */ - functionSelector: string | AbiFunction + functionSelector: string | AbiFunction; /** The rules to be included in the policy */ - rules: Rule[] + rules: Rule[]; /** The time interval within which the session is valid. If left unset the session will remain invalid indefinitely */ - interval?: SessionEpoch + interval?: SessionEpoch; /** The maximum value that can be transferred in a single transaction */ - valueLimit: bigint -} + valueLimit: bigint; +}; -export type SessionGrantedPayload = UserOpResponse & { session: Session } +export type SessionGrantedPayload = UserOpResponse & { session: Session }; /** * @@ -113,8 +108,6 @@ export type SessionGrantedPayload = UserOpResponse & { session: Session } * @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} - * @param storeSessionKeyInDAN - Optional. If true, the session key stored on the DAN network. Must be used with "DISTRIBUTED_KEY" {@link SessionType} when creating the sessionSmartAccountClient and using the session - * @param browserWallet - Optional. The browser wallet instance. Only relevant when storeSessionKeyInDan is true. {@link CreateSessionWithDistributedKeyParams['browserWallet']} * @returns Promise<{@link SessionGrantedPayload}> - An object containing the status of the transaction and the sessionID. * * @example @@ -178,87 +171,89 @@ export const createSession = async ( sessionStorageClient?: ISessionStorage | null, buildUseropDto?: BuildUserOpOptions, ): Promise => { - - const smartAccountAddress = await smartAccount.getAddress() + const smartAccountAddress = await smartAccount.getAddress(); const defaultedChainId = extractChainIdFromBundlerUrl( - smartAccount?.bundler?.getBundlerUrl() ?? "" - ) + smartAccount?.bundler?.getBundlerUrl() ?? "", + ); if (!defaultedChainId) { - throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND) + throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND); } - const chain = getChain(defaultedChainId) + const chain = getChain(defaultedChainId); const { sessionKeyAddress, - sessionStorageClient: storageClientFromCreateKey - } = await createSessionKeyEOA(smartAccount, chain) + sessionStorageClient: storageClientFromCreateKey, + } = await createSessionKeyEOA(smartAccount, chain); const defaultedSessionStorageClient = - sessionStorageClient ?? storageClientFromCreateKey + sessionStorageClient ?? storageClientFromCreateKey; const sessionsModule = await createSessionKeyManagerModule({ smartAccountAddress, - sessionStorageClient: defaultedSessionStorageClient - }) + sessionStorageClient: defaultedSessionStorageClient, + }); const defaultedPolicy: Policy[] = policy.map((p) => - !p.sessionKeyAddress ? { ...p, sessionKeyAddress } : (p as Policy) - ) - const humanReadablePolicyArray = defaultedPolicy.map(createABISessionDatum) + !p.sessionKeyAddress ? { ...p, sessionKeyAddress } : (p as Policy), + ); + const humanReadablePolicyArray = defaultedPolicy.map(createABISessionDatum); const { data: policyData, sessionIDInfo } = - await sessionsModule.createSessionData(humanReadablePolicyArray) + await sessionsModule.createSessionData(humanReadablePolicyArray); const permitTx = { to: DEFAULT_SESSION_KEY_MANAGER_MODULE, - data: policyData - } + data: policyData, + }; - const txs: Transaction[] = [] + const txs: Transaction[] = []; - const isDeployed = await smartAccount.isAccountDeployed() + const isDeployed = await smartAccount.isAccountDeployed(); const enableSessionTx = await smartAccount.getEnableModuleData( - DEFAULT_SESSION_KEY_MANAGER_MODULE - ) + DEFAULT_SESSION_KEY_MANAGER_MODULE, + ); if (isDeployed) { const enabled = await smartAccount.isModuleEnabled( - DEFAULT_SESSION_KEY_MANAGER_MODULE - ) + DEFAULT_SESSION_KEY_MANAGER_MODULE, + ); if (!enabled) { - txs.push(enableSessionTx) + txs.push(enableSessionTx); } } else { - Logger.log(ERROR_MESSAGES.ACCOUNT_NOT_DEPLOYED) - txs.push(enableSessionTx) + Logger.log(ERROR_MESSAGES.ACCOUNT_NOT_DEPLOYED); + txs.push(enableSessionTx); } - txs.push(permitTx) + txs.push(permitTx); - const userOpResponse = await smartAccount.sendTransaction(txs, buildUseropDto) + const userOpResponse = await smartAccount.sendTransaction( + txs, + buildUseropDto, + ); return { session: { sessionStorageClient: defaultedSessionStorageClient, - sessionIDInfo + sessionIDInfo, }, - ...userOpResponse - } -} + ...userOpResponse, + }; +}; export type HardcodedFunctionSelector = { - raw: Hex -} + raw: Hex; +}; export type CreateSessionDatumParams = { - interval?: SessionEpoch - sessionKeyAddress: Hex - contractAddress: Hex - functionSelector: string | AbiFunction | HardcodedFunctionSelector - rules: Rule[] - valueLimit: bigint - danModuleInfo?: DanModuleInfo -} + interval?: SessionEpoch; + sessionKeyAddress: Hex; + contractAddress: Hex; + functionSelector: string | AbiFunction | HardcodedFunctionSelector; + rules: Rule[]; + valueLimit: bigint; + danModuleInfo?: DanModuleInfo; +}; /** * @@ -284,25 +279,26 @@ export const createABISessionDatum = ({ /** The maximum value that can be transferred in a single transaction */ valueLimit, /** information pertinent to the DAN module */ - danModuleInfo + danModuleInfo, }: CreateSessionDatumParams): CreateSessionDataParams => { - const { validUntil = 0, validAfter = 0 } = interval ?? {} + const { validUntil = 0, validAfter = 0 } = interval ?? {}; - let parsedFunctionSelector: Hex = "0x" + let parsedFunctionSelector: Hex = "0x"; const rawFunctionSelectorWasProvided = !!( functionSelector as HardcodedFunctionSelector - )?.raw + )?.raw; if (rawFunctionSelectorWasProvided) { - parsedFunctionSelector = (functionSelector as HardcodedFunctionSelector).raw + parsedFunctionSelector = (functionSelector as HardcodedFunctionSelector) + .raw; } else { - const unparsedFunctionSelector = functionSelector as AbiFunction | string + const unparsedFunctionSelector = functionSelector as AbiFunction | string; parsedFunctionSelector = slice( toFunctionSelector(unparsedFunctionSelector), 0, - 4 - ) + 4, + ); } const result = { @@ -314,68 +310,68 @@ export const createABISessionDatum = ({ destContract: contractAddress, functionSelector: parsedFunctionSelector, valueLimit, - rules - }) - } + rules, + }), + }; - return danModuleInfo ? { ...result, danModuleInfo } : result -} + return danModuleInfo ? { ...result, danModuleInfo } : result; +}; /** * @deprecated */ export async function getABISVMSessionKeyData( sessionKey: `0x${string}` | Uint8Array, - permission: DeprecatedPermission + 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}` + 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 - ]) + permission.rules[i].referenceValue, + ]); } - return sessionKeyData + return sessionKeyData; } export function getSessionDatum( sessionKeyAddress: Hex, - permission: Permission + 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 + 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 + parseReferenceValue(permission.rules[i].referenceValue), + ]) as Hex; } - return sessionKeyData + return sessionKeyData; } export type HardcodedReference = { - raw: Hex -} -type BaseReferenceValue = string | number | bigint | boolean | ByteArray -type AnyReferenceValue = BaseReferenceValue | HardcodedReference + raw: Hex; +}; +type BaseReferenceValue = string | number | bigint | boolean | ByteArray; +type AnyReferenceValue = BaseReferenceValue | HardcodedReference; /** * @@ -391,20 +387,20 @@ type AnyReferenceValue = BaseReferenceValue | HardcodedReference export function parseReferenceValue(referenceValue: AnyReferenceValue): Hex { try { if ((referenceValue as HardcodedReference)?.raw) { - return (referenceValue as HardcodedReference)?.raw + return (referenceValue as HardcodedReference)?.raw; } if (typeof referenceValue === "bigint") { - return pad(toHex(referenceValue), { size: 32 }) as Hex + return pad(toHex(referenceValue), { size: 32 }) as Hex; } - return pad(referenceValue as Hex, { size: 32 }) + return pad(referenceValue as Hex, { size: 32 }); } catch (e) { - return pad(referenceValue as Hex, { size: 32 }) + return pad(referenceValue as Hex, { size: 32 }); } } export type SingleSessionParamsPayload = { - params: SessionParams -} + params: SessionParams; +}; /** * getSingleSessionTxParams * @@ -419,27 +415,26 @@ export type SingleSessionParamsPayload = { export const getSingleSessionTxParams = async ( conditionalSession: SessionSearchParam, chain: Chain, - correspondingIndex: number | null | undefined + correspondingIndex?: number | null | undefined, ): Promise => { - const { sessionStorageClient } = await resumeSession(conditionalSession) - + const { sessionStorageClient } = await resumeSession(conditionalSession); // if correspondingIndex is null then use the last session. - const allSessions = await sessionStorageClient.getAllSessionData() + const allSessions = await sessionStorageClient.getAllSessionData(); const sessionID = didProvideFullSession(conditionalSession) ? (conditionalSession as Session).sessionIDInfo[correspondingIndex ?? 0] - : allSessions[correspondingIndex ?? allSessions.length - 1].sessionID + : allSessions[correspondingIndex ?? allSessions.length - 1].sessionID; const sessionSigner = await sessionStorageClient.getSignerBySession( { - sessionID + sessionID, }, - chain - ) + chain, + ); return { params: { sessionSigner, - sessionID - } - } -} + sessionID, + }, + }; +}; diff --git a/src/modules/sessions/dan.ts b/src/modules/sessions/dan.ts index 8b0fd8074..d303a3fcb 100644 --- a/src/modules/sessions/dan.ts +++ b/src/modules/sessions/dan.ts @@ -1,22 +1,48 @@ +import { EOAAuth, EphAuth, type IBrowserWallet, NetworkSigner, WalletProviderServiceClient, computeAddress } from "@silencelaboratories/walletprovider-sdk" import type { Chain, Hex } from "viem" -import type { - BiconomySmartAccountV2, - BuildUserOpOptions, +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { type Session, createDANSessionKeyManagerModule } from "../" +import { + type BiconomySmartAccountV2, + type BuildUserOpOptions, + ERROR_MESSAGES, + Logger, + type Transaction, + isWalletClient } from "../../account" +import { extractChainIdFromBundlerUrl } from "../../bundler" import type { ISessionStorage } from "../interfaces/ISessionStorage" -import type { DanModuleInfo, IBrowserWallet } from "../utils/Types" -import type { Policy } from "./abi" +import { getDefaultStorageClient } from "../session-storage/utils" +import { + DAN_BACKEND_URL, + DEFAULT_SESSION_KEY_MANAGER_MODULE +} from "../utils/Constants" +import { + NodeWallet, + type SessionSearchParam, + didProvideFullSession, + hexToUint8Array, + resumeSession +} from "../utils/Helper" +import type { DanModuleInfo } from "../utils/Types" +import { + type Policy, + type SessionGrantedPayload, + createABISessionDatum +} from "./abi" export type PolicyLeaf = Omit -export const DEFAULT_SESSION_DURATION = 60 * 60 +export const DEFAULT_SESSION_DURATION = 60 * 60 * 24 * 365 // 1 year +export const QUORUM_PARTIES = 5 +export const QUORUM_THRESHOLD = 3 -export type CreateDistributedParams = { +export type CreateSessionWithDistributedKeyParams = { /** The user's smart account instance */ smartAccountClient: BiconomySmartAccountV2, /** An array of session configurations */ policy: PolicyLeaf[], /** The storage client to store the session keys */ - sessionStorageClient?: ISessionStorage, + sessionStorageClient?: ISessionStorage | null, /** The build userop dto */ buildUseropDto?: BuildUserOpOptions, /** The chain ID */ @@ -25,13 +51,143 @@ export type CreateDistributedParams = { browserWallet?: IBrowserWallet } +/** + * + * createSessionWithDistributedKey + * + * 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 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} + * @param chainId - Optional. Will be inferred if left unset. + * @param browserWallet - Optional. The user's {@link IBrowserWallet} instance. Default will be the signer associated with the smart account. + * @returns Promise<{@link SessionGrantedPayload}> - An object containing the status of the transaction and the sessionID. + * + * @example + * + * import { type PolicyLeaf, type Session, createSessionWithDistributedKey } from "@biconomy/account" + * + * const policy: PolicyLeaf[] = [{ + * contractAddress: nftAddress, + * functionSelector: "safeMint(address)", + * rules: [ + * { + * offset: 0, + * condition: 0, + * referenceValue: smartAccountAddress + * } + * ], + * interval: { + * validUntil: 0, + * validAfter: 0 + * }, + * valueLimit: 0n + * }] + * + * const { wait, session } = await createSessionWithDistributedKey({ + * smartAccountClient, + * policy + * }) + * + * const { success } = await wait() +*/ +export const createSessionWithDistributedKey = async ({ + smartAccountClient, + policy, + sessionStorageClient, + buildUseropDto, + chainId, + browserWallet +}: CreateSessionWithDistributedKeyParams): Promise => { + const defaultedChainId = + chainId ?? + extractChainIdFromBundlerUrl(smartAccountClient?.bundler?.getBundlerUrl() ?? ""); + + if (!defaultedChainId) { + throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND) + } + const smartAccountAddress = await smartAccountClient.getAddress() + const defaultedSessionStorageClient = + sessionStorageClient || getDefaultStorageClient(smartAccountAddress) + + const sessionsModule = await createDANSessionKeyManagerModule({ + smartAccountAddress, + sessionStorageClient: defaultedSessionStorageClient + }) + + let duration = DEFAULT_SESSION_DURATION + if (policy?.[0].interval?.validUntil) { + duration = Math.round(policy?.[0].interval?.validUntil - Date.now() / 1000) + } + + const { sessionKeyEOA: sessionKeyAddress, ...other } = await danSDK.generateSessionKey({ + smartAccountClient, + browserWallet, + duration, + chainId + }) + + const danModuleInfo: DanModuleInfo = { ...other } + const defaultedPolicy: Policy[] = policy.map((p) => ({ ...p, sessionKeyAddress })) + + const humanReadablePolicyArray = defaultedPolicy.map((p) => + createABISessionDatum({ ...p, danModuleInfo }) + ) + + const { data: policyData, sessionIDInfo } = + await sessionsModule.createSessionData(humanReadablePolicyArray) + + const permitTx = { + to: DEFAULT_SESSION_KEY_MANAGER_MODULE, + data: policyData + } + + const txs: Transaction[] = [] + + const isDeployed = await smartAccountClient.isAccountDeployed() + const enableSessionTx = await smartAccountClient.getEnableModuleData( + DEFAULT_SESSION_KEY_MANAGER_MODULE + ) + + if (isDeployed) { + const enabled = await smartAccountClient.isModuleEnabled( + DEFAULT_SESSION_KEY_MANAGER_MODULE + ) + if (!enabled) { + txs.push(enableSessionTx) + } + } else { + Logger.log(ERROR_MESSAGES.ACCOUNT_NOT_DEPLOYED) + txs.push(enableSessionTx) + } + + txs.push(permitTx) + + const userOpResponse = await smartAccountClient.sendTransaction(txs, buildUseropDto) + + smartAccountClient.setActiveValidationModule(sessionsModule) + + return { + session: { + sessionStorageClient: defaultedSessionStorageClient, + sessionIDInfo + }, + ...userOpResponse + } +} + export type DanSessionKeyPayload = { /** Dan Session ephemeral key*/ sessionKeyEOA: Hex; /** Dan Session MPC key ID*/ mpcKeyId: string; - /** Dan Session ephemeral private key without 0x prefi x*/ - hexEphSKWithout0x: string; + /** Dan Session ephemeral private key without 0x prefix */ + jwt: string; /** Number of nodes that participate in keygen operation. Also known as n. */ partiesNumber: number; /** Number of nodes that needs to participate in protocol in order to generate valid signature. Also known as t. */ @@ -52,7 +208,82 @@ export type DanSessionKeyRequestParams = { /** Optional duration of the session key in seconds. Default is 3600 seconds. */ duration?: number; /** Optional chainId. Will be inferred if left unset. */ - chain?: Chain; + chainId?: number; +} + +/** + * + * generateSessionKey + * + * @description This function is used to generate a new session key for a Distributed Account Network (DAN) session. This information is kept in the session storage and can be used to validate userops without the user's direct involvement. + * + * Generates a new session key for a Distributed Account Network (DAN) session. + * @param smartAccount - The user's {@link BiconomySmartAccountV2} smartAccount instance. + * @param browserWallet - Optional. The user's {@link IBrowserWallet} instance. + * @param hardcodedValues - Optional. {@link DanModuleInfo} - Additional information for the DAN module configuration to override the default values. + * @param duration - Optional. The duration of the session key in seconds. Default is 3600 seconds. + * @param chainId - Optional. The chain ID. Will be inferred if left unset. + * @returns Promise<{@link DanModuleInfo}> - An object containing the session key, the MPC key ID, the number of parties, the threshold, and the EOA address. + * +*/ +export const generateSessionKey = async ({ + smartAccountClient, + browserWallet, + hardcodedValues = {}, + duration = DEFAULT_SESSION_DURATION, + chainId +}: DanSessionKeyRequestParams): Promise => { + + const eoaAddress = hardcodedValues?.eoaAddress ?? (await smartAccountClient.getSigner().getAddress()) as Hex // Smart account owner + const innerSigner = smartAccountClient.getSigner().inner + + const defaultedChainId = chainId ?? extractChainIdFromBundlerUrl( + smartAccountClient?.bundler?.getBundlerUrl() ?? "" + ) + + if (!defaultedChainId) { + throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND) + } + + if (!browserWallet && !isWalletClient(innerSigner)) + throw new Error(ERROR_MESSAGES.INVALID_BROWSER_WALLET) + const wallet = browserWallet ?? new NodeWallet(innerSigner) + + const hexEphSK = generatePrivateKey() + const account = privateKeyToAccount(hexEphSK) + const jwt = hardcodedValues?.jwt ?? hexEphSK.slice(2); + + const ephPK: Uint8Array = hexToUint8Array(account.address.slice(2)) + + const wpClient = new WalletProviderServiceClient({ + walletProviderId: "WalletProvider", + walletProviderUrl: DAN_BACKEND_URL + }) + + const eoaAuth = new EOAAuth(eoaAddress, wallet, ephPK, duration); + + const partiesNumber = hardcodedValues?.partiesNumber ?? QUORUM_PARTIES + const threshold = hardcodedValues?.threshold ?? QUORUM_THRESHOLD + + const sdk = new NetworkSigner(wpClient, threshold, partiesNumber, eoaAuth) + + // @ts-ignore + const resp = await sdk.authenticateAndCreateKey(ephPK) + + const pubKey = resp.publicKey + const mpcKeyId = resp.keyId as Hex + + const sessionKeyEOA = computeAddress(pubKey) + + return { + sessionKeyEOA, + mpcKeyId, + jwt, + partiesNumber, + threshold, + eoaAddress, + chainId: defaultedChainId + } } export type DanSessionParamsPayload = { @@ -60,3 +291,120 @@ export type DanSessionParamsPayload = { sessionID: string } } +/** + * getDanSessionTxParams + * + * Retrieves the transaction parameters for a batched session. + * + * @param correspondingIndex - An index for the transaction corresponding to the relevant session. If not provided, the last session index is used. + * @param conditionalSession - {@link SessionSearchParam} The session data that contains the sessionID and sessionSigner. If not provided, The default session storage (localStorage in browser, fileStorage in node backend) is used to fetch the sessionIDInfo + * @returns Promise<{@link DanSessionParamsPayload}> - session parameters. + * + */ +export const getDanSessionTxParams = async ( + conditionalSession: SessionSearchParam, + chain: Chain, + correspondingIndex?: number | null | undefined +): Promise => { + const defaultedCorrespondingIndex = Array.isArray(correspondingIndex) + ? correspondingIndex[0] + : correspondingIndex + const resumedSession = await resumeSession(conditionalSession) + // if correspondingIndex is null then use the last session. + const allSessions = + await resumedSession.sessionStorageClient.getAllSessionData() + + const sessionID = didProvideFullSession(conditionalSession) + ? (conditionalSession as Session).sessionIDInfo[ + defaultedCorrespondingIndex ?? 0 + ] + : allSessions[defaultedCorrespondingIndex ?? allSessions.length - 1] + .sessionID + + const matchingLeafDatum = allSessions.find((s) => s.sessionID === sessionID) + + if (!sessionID) throw new Error(ERROR_MESSAGES.MISSING_SESSION_ID) + if (!matchingLeafDatum) throw new Error(ERROR_MESSAGES.NO_LEAF_FOUND) + if (!matchingLeafDatum.danModuleInfo) + throw new Error(ERROR_MESSAGES.NO_DAN_MODULE_INFO) + const chainIdsMatch = chain.id === matchingLeafDatum?.danModuleInfo?.chainId + if (!chainIdsMatch) throw new Error(ERROR_MESSAGES.CHAIN_ID_MISMATCH) + + return { params: { sessionID } } + +} + +/** + * + * signMessage + * + * @description This function is used to sign a message using the Distributed Account Network (DAN) module. + * + * @param message - The message to sign + * @param danParams {@link DanModuleInfo} - The DAN module information required to sign the message + * @returns signedResponse - Hex + * + * @example + * + * ```ts + * import { signMessage } from "@biconomy/account"; + * const objectToSign: DanSignatureObject = { + * userOperation: UserOperationStruct, + * entryPointVersion: "v0.6.0", + * entryPointAddress: "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789", + * chainId + * } + * + * const messageToSign = JSON.stringify(objectToSign) + * const signature: Hex = await signMessage(messageToSign, sessionSignerData.danModuleInfo); // From the generateSessionKey helper + * ``` + * + */ +export const signMessage = async (message: string, danParams: DanModuleInfo): Promise => { + + const { jwt, eoaAddress, threshold, partiesNumber, chainId, mpcKeyId } = danParams; + + if (!message) throw new Error("Missing message") + if ( + !jwt || + !eoaAddress || + !threshold || + !partiesNumber || + !chainId || + !mpcKeyId + ) { + throw new Error("Missing params from danModuleInfo") + } + + const wpClient = new WalletProviderServiceClient({ + walletProviderId: "WalletProvider", + walletProviderUrl: DAN_BACKEND_URL + }) + + const ephSK = hexToUint8Array(jwt) + + const authModule = new EphAuth(eoaAddress, ephSK) + + const sdk = new NetworkSigner( + wpClient, + threshold, + partiesNumber, + authModule + ) + + const reponse: Awaited> = await sdk.authenticateAndSign(mpcKeyId, message); + + const v = reponse.recid + const sigV = v === 0 ? "1b" : "1c" + + const signature: Hex = `0x${reponse.sign}${sigV}` + + return signature +}; + +export const danSDK = { + generateSessionKey, + signMessage +} + +export default danSDK; \ No newline at end of file diff --git a/src/modules/sessions/sessionSmartAccountClient.ts b/src/modules/sessions/sessionSmartAccountClient.ts index 0a657d604..0f7817b03 100644 --- a/src/modules/sessions/sessionSmartAccountClient.ts +++ b/src/modules/sessions/sessionSmartAccountClient.ts @@ -13,6 +13,7 @@ import type { UserOpResponse } from "../../bundler/index.js"; import { type SessionSearchParam, createBatchedSessionRouterModule, + createDANSessionKeyManagerModule, createSessionKeyManagerModule, type getSingleSessionTxParams, resumeSession, @@ -144,11 +145,17 @@ export const createSessionSmartAccountClient = async ( sessionKeyManagerModule: sessionModule, }, ); + const danSessionValidationModule = await createDANSessionKeyManagerModule({ + smartAccountAddress: biconomySmartAccountConfig.accountAddress, + sessionStorageClient, + }); const activeValidationModule = defaultedSessionType === "BATCHED" ? batchedSessionValidationModule - : sessionModule + : defaultedSessionType === "STANDARD" + ? sessionModule + : danSessionValidationModule; return await createSmartAccountClient({ ...biconomySmartAccountConfig, diff --git a/src/modules/utils/Helper.ts b/src/modules/utils/Helper.ts index 78806c6a4..4904d7f5e 100644 --- a/src/modules/utils/Helper.ts +++ b/src/modules/utils/Helper.ts @@ -1,3 +1,4 @@ +import type { IBrowserWallet, TypedData } from "@silencelaboratories/walletprovider-sdk" import { type Address, type ByteArray, @@ -22,10 +23,8 @@ import { import type { ChainInfo, HardcodedReference, - IBrowserWallet, Session, SignerData, - TypedData, } from "../../index.js" import type { ISessionStorage } from "../interfaces/ISessionStorage" import { getDefaultStorageClient } from "../session-storage/utils" diff --git a/src/modules/utils/Types.ts b/src/modules/utils/Types.ts index af50c362d..8cec17e56 100644 --- a/src/modules/utils/Types.ts +++ b/src/modules/utils/Types.ts @@ -95,7 +95,7 @@ export type StrictSessionParams = { export type DanModuleInfo = { /** Ephemeral sk */ - hexEphSKWithout0x: string + jwt: string /** eoa address */ eoaAddress: Hex /** threshold */ diff --git a/tests/modules/write.test.ts b/tests/modules/write.test.ts index 2bdf86970..aea3b7c92 100644 --- a/tests/modules/write.test.ts +++ b/tests/modules/write.test.ts @@ -10,10 +10,10 @@ import { parseEther, parseUnits, slice, - toFunctionSelector, -} from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { beforeAll, describe, expect, test } from "vitest"; + toFunctionSelector +} from "viem" +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { beforeAll, describe, expect, test } from "vitest" import { type BiconomySmartAccountV2, type Transaction, @@ -24,24 +24,33 @@ import { } from "../../src/account"; import { Logger, getChain } from "../../src/account"; import { + BrowserWallet, type CreateSessionDataParams, DEFAULT_BATCHED_SESSION_ROUTER_MODULE, DEFAULT_ECDSA_OWNERSHIP_MODULE, DEFAULT_MULTICHAIN_MODULE, DEFAULT_SESSION_KEY_MANAGER_MODULE, + DanModuleInfo, ECDSA_OWNERSHIP_MODULE_ADDRESSES_BY_VERSION, + NodeWallet, + type PolicyLeaf, + createECDSAOwnershipValidationModule, createMultiChainValidationModule, createSessionKeyManagerModule, + createSessionWithDistributedKey, + danSDK, getABISVMSessionKeyData, resumeSession, } 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 { ECDSAModuleAbi } from "../../src/account/abi/ECDSAModule" +import { DANSessionKeyManagerModule } from "../../src/modules/DANSessionKeyManagerModule" +import { SessionMemoryStorage } from "../../src/modules/session-storage/SessionMemoryStorage" +import { createSessionKeyEOA } from "../../src/modules/session-storage/utils" import { type Policy, PolicyHelpers, + RuleHelpers, createABISessionDatum, createSession, getSingleSessionTxParams, @@ -126,15 +135,15 @@ describe("Modules:Write", () => { chainId, signer: client, bundlerUrl, - paymasterUrl, - }), - ), - ); - [smartAccountAddress, smartAccountAddressTwo] = await Promise.all( - [smartAccount, smartAccountTwo].map((account) => - account.getAccountAddress(), - ), - ); + paymasterUrl + }) + ) + ) + ;[smartAccountAddress, smartAccountAddressTwo] = await Promise.all( + [smartAccount, smartAccountTwo].map((account) => + account.getAccountAddress() + ) + ) smartAccountThree = await createSmartAccountClient({ signer: walletClient, @@ -215,8 +224,8 @@ describe("Modules:Write", () => { paymasterUrl, chainId, }, - smartAccountAddressThree, // Storage client, full Session or smartAccount address if using default storage - ); + "DEFAULT_STORE" // Storage client, full Session or smartAccount address if using default storage + ) const sessionSmartAccountThreeAddress = await smartAccountThreeWithSession.getAccountAddress(); @@ -252,7 +261,7 @@ describe("Modules:Write", () => { ); expect(nftBalanceAfter - nftBalanceBefore).toBe(1n); - }); + }, 50000); // User must be connected with a wallet to grant permissions test("should create a batch session on behalf of a user", async () => { @@ -328,9 +337,9 @@ describe("Modules:Write", () => { paymasterUrl, chainId, }, - smartAccountAddressFour, // Storage client, full Session or smartAccount address if using default storage - true, // if batching - ); + "DEFAULT_STORE", // Storage client, full Session or smartAccount address if using default storage + true // if batching + ) const sessionSmartAccountFourAddress = await smartAccountFourWithSession.getAccountAddress(); @@ -898,9 +907,9 @@ describe("Modules:Write", () => { paymasterUrl, chainId: chain.id, }, - sessionStorageClient, // Storage client, full Session or smartAccount address if using default storage - true, - ); + "DEFAULT_STORE", // Storage client, full Session or smartAccount address if using default storage + true + ) const submitCancelTx: Transaction = { to: DUMMY_CONTRACT_ADDRESS, @@ -1049,8 +1058,8 @@ describe("Modules:Write", () => { chainId, index: 25, // Increasing index to not conflict with other test cases and use a new smart account }, - sessionStorageClient, - ); + "DEFAULT_STORE" + ) const submitCancelTx: Transaction = { to: DUMMY_CONTRACT_ADDRESS, @@ -1098,7 +1107,7 @@ describe("Modules:Write", () => { const { success: txSuccess } = await waitForSetMerkleRoot(); expect(txSuccess).toBe("true"); - const sessionDataAfter = await sessionStorageClient.getAllSessionData(); + const sessionDataAfter = await sessionStorageClient.getAllSessionData() const revokedSession = sessionDataAfter.find( (session) => session.status === "REVOKED", ); @@ -1384,8 +1393,8 @@ describe("Modules:Write", () => { paymasterUrl, chainId: chain.id, }, - smartAccountAddress, - ); + "DEFAULT_STORE" + ) const approvalTx = { to: preferredToken, @@ -1448,8 +1457,209 @@ describe("Modules:Write", () => { ); expect( - balanceOfPreferredTokenBefore - balanceOfPreferredTokenAfter, - ).toBeGreaterThan(0); - }, 80000); + balanceOfPreferredTokenBefore - balanceOfPreferredTokenAfter + ).toBeGreaterThan(0) + }, 80000) + + test("should create and use an DAN session on behalf of a user (abstracted)", async () => { + + const policy: PolicyLeaf[] = [ + { + contractAddress: nftAddress, + functionSelector: "safeMint(address)", + rules: [ + { + offset: RuleHelpers.OffsetByIndex(0), + condition: RuleHelpers.Condition("EQUAL"), + referenceValue: smartAccountAddress + } + ], + interval: PolicyHelpers.Indefinitely, + valueLimit: PolicyHelpers.NoValueLimit + } + ] + + const { wait } = await createSessionWithDistributedKey({ smartAccountClient: smartAccount, policy }) + + const { success } = await wait() + expect(success).toBe("true") + + const nftMintTx: Transaction = { + to: nftAddress, + data: encodeFunctionData({ + abi: parseAbi(["function safeMint(address _to)"]), + functionName: "safeMint", + args: [smartAccountAddress] + }) + } + + const nftBalanceBefore = await checkBalance(smartAccountAddress, nftAddress) + + const smartAccountWithSession = await createSessionSmartAccountClient( + { + accountAddress: smartAccountAddress, // Set the account address on behalf of the user + bundlerUrl, + paymasterUrl, + chainId + }, + "DEFAULT_STORE", + "DISTRIBUTED_KEY" + ) + + const { wait: waitForMint } = await smartAccountWithSession.sendTransaction( + nftMintTx, + withSponsorship, + { leafIndex: "LAST_LEAF" } + ) + + const { + success: mintSuccess, + receipt: { transactionHash } + } = await waitForMint() + + expect(mintSuccess).toBe("true") + expect(transactionHash).toBeTruthy() + + const nftBalanceAfter = await checkBalance(smartAccountAddress, nftAddress) + expect(nftBalanceAfter - nftBalanceBefore).toBe(1n) + + }, 50000) + + + test("should create and use a DAN session on behalf of a user (deconstructed)", async () => { + + // To begin with, ensure that the regular validation module is set + smartAccount = smartAccount.setActiveValidationModule( + await createECDSAOwnershipValidationModule({ signer: walletClient }) + ) + + // Create a new storage client + const memoryStore = new SessionMemoryStorage(smartAccountAddress); + + // Get the module for activation later + const sessionsModule = await DANSessionKeyManagerModule.create({ + smartAccountAddress, + sessionStorageClient: memoryStore + }) + + // Set the ttl for the session + const duration = 60 * 60 + + // Get the session key from the dan network + const danModuleInfo = await danSDK.generateSessionKey({ + smartAccountClient: smartAccount, + browserWallet: new NodeWallet(walletClient), + duration + }) + + // create the policy to be signed over by the user + const policy: Policy[] = [{ + contractAddress: nftAddress, + functionSelector: "safeMint(address)", + sessionKeyAddress: danModuleInfo.sessionKeyEOA, // Add the session key address from DAN + rules: [ + { + offset: RuleHelpers.OffsetByIndex(0), + condition: RuleHelpers.Condition("EQUAL"), + referenceValue: smartAccountAddress + } + ], + interval: { + validAfter: 0, + validUntil: Math.round(Date.now() / 1000) + duration // The duration is set to 1 hour + }, + valueLimit: PolicyHelpers.NoValueLimit + }]; + + // Create the session data using the information retrieved from DAN. Keep the danModuleInfo for later use in a session leaf + const { data: policyData, sessionIDInfo: sessionIDs } = + await sessionsModule.createSessionData(policy.map(p => createABISessionDatum({ ...p, danModuleInfo }))) + + // Cconstruct the session transaction + const permitTx = { + to: DEFAULT_SESSION_KEY_MANAGER_MODULE, + data: policyData + } + + const txs: Transaction[] = [] + + // Ensure the module is enabled + const isDeployed = await smartAccount.isAccountDeployed() + const enableSessionTx = await smartAccount.getEnableModuleData( + DEFAULT_SESSION_KEY_MANAGER_MODULE + ) + + // Ensure the smart account is deployed + if (isDeployed) { + // Add the enable module transaction if it is not enabled + const enabled = await smartAccount.isModuleEnabled( + DEFAULT_SESSION_KEY_MANAGER_MODULE + ) + if (!enabled) { + txs.push(enableSessionTx) + } + } else { + txs.push(enableSessionTx) + } + + // Add the permit transaction + txs.push(permitTx) + + // User must sign over the policy to grant the relevant permissions + const { wait } = await smartAccount.sendTransaction(txs, { ...withSponsorship, nonceOptions }); + const { success } = await wait(); + + expect(success).toBe("true"); + + // Now let's use the session, assuming we have no user-connected smartAccountClient. + const randomWalletClient = createWalletClient({ + account: privateKeyToAccount(generatePrivateKey()), + chain, + transport: http() + }); + + // Now assume that the users smart account address and the storage client are the only known values + let unconnectedSmartAccount = await createSmartAccountClient({ + accountAddress: smartAccountAddress, // Set the account address on behalf of the user + signer: randomWalletClient, // This signer is irrelevant and will not be used + bundlerUrl, + paymasterUrl, + chainId + }); + + // Set the active validation module to the DAN session module + unconnectedSmartAccount = unconnectedSmartAccount.setActiveValidationModule(sessionsModule); + + // Use the session to submit a tx relevant to the policy + const nftMintTx = { + to: nftAddress, + data: encodeFunctionData({ + abi: parseAbi(["function safeMint(address _to)"]), + functionName: "safeMint", + args: [smartAccountAddress] + }) + } + + // Assume we know that the relevant session leaf to the transaction is the last one... + const allLeaves = await memoryStore.getAllSessionData(); + const relevantLeaf = allLeaves[allLeaves.length - 1]; + + const sessionID = relevantLeaf.sessionID; + // OR + const sameSessionID = sessionIDs[0]; // Usually only available when the session is created + + const nftBalanceBefore = await checkBalance(smartAccountAddress, nftAddress); + + // Now use the sessionID to send the transaction + const { wait: waitForMint } = await unconnectedSmartAccount.sendTransaction(nftMintTx, { ...withSponsorship, params: { sessionID } }); + + // Check for success + const { success: mintSuccess } = await waitForMint(); + const nftBalanceAfter = await checkBalance(smartAccountAddress, nftAddress); + + expect(nftBalanceAfter - nftBalanceBefore).toBe(1n); + expect(mintSuccess).toBe("true"); + + }, 50000) -}); +}) diff --git a/tests/playground/read.test.ts b/tests/playground/read.test.ts deleted file mode 100644 index c754b00f5..000000000 --- a/tests/playground/read.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { http, type Hex, createPublicClient, createWalletClient } from "viem" -import { privateKeyToAccount } from "viem/accounts" -import { beforeAll, describe, expect, test } from "vitest" -import { - type BiconomySmartAccountV2, - createSmartAccountClient -} from "../../src/account" -import { getConfig } from "../utils" - -describe("Playground:Read", () => { - const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e" - const { - chain, - chainId, - privateKey, - privateKeyTwo, - bundlerUrl, - paymasterUrl - } = getConfig() - const account = privateKeyToAccount(`0x${privateKey}`) - const accountTwo = privateKeyToAccount(`0x${privateKeyTwo}`) - const sender = account.address - const recipient = accountTwo.address - const publicClient = createPublicClient({ - chain, - transport: http() - }) - let [smartAccount, smartAccountTwo]: BiconomySmartAccountV2[] = [] - let [smartAccountAddress, smartAccountAddressTwo]: Hex[] = [] - - const [walletClient, walletClientTwo] = [ - createWalletClient({ - account, - chain, - transport: http() - }), - createWalletClient({ - account: accountTwo, - chain, - transport: http() - }) - ] - - beforeAll(async () => { - ;[smartAccount, smartAccountTwo] = await Promise.all( - [walletClient, walletClientTwo].map((client) => - createSmartAccountClient({ - chainId, - signer: client, - bundlerUrl, - paymasterUrl - }) - ) - ) - ;[smartAccountAddress, smartAccountAddressTwo] = await Promise.all( - [smartAccount, smartAccountTwo].map((account) => - account.getAccountAddress() - ) - ) - }) - - test.concurrent( - "should quickly run a read test in the playground ", - async () => { - const addresses = await Promise.all([ - walletClient.account.address, - smartAccountAddress, - walletClientTwo.account.address, - smartAccountAddressTwo - ]) - expect(addresses.every(Boolean)).toBe(true) - }, - 30000 - ) -}) diff --git a/tests/playground/write.test.ts b/tests/playground/write.test.ts index 1ee45fc2b..4d2941f01 100644 --- a/tests/playground/write.test.ts +++ b/tests/playground/write.test.ts @@ -1,75 +1,113 @@ -import { http, type Hex, createWalletClient } from "viem" +import { http, type Hex, createWalletClient, encodeFunctionData, parseAbi } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { polygonAmoy } from "viem/chains" import { beforeAll, describe, expect, test } from "vitest" +import { PaymasterMode, type PolicyLeaf } from "../../src" import { type BiconomySmartAccountV2, - createSmartAccountClient + createSmartAccountClient, + getChain, + getCustomChain } from "../../src/account" -import { getConfig } from "../utils" +import { createSession } from "../../src/modules/sessions/abi" +import { createSessionSmartAccountClient } from "../../src/modules/sessions/sessionSmartAccountClient" +import { getBundlerUrl, getConfig, getPaymasterUrl } from "../utils" + +const withSponsorship = { + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, +}; describe("Playground:Write", () => { - // const nftAddress = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e" - // const token = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a" - const { - chain, - chainId, - privateKey, - privateKeyTwo, - bundlerUrl, - paymasterUrl - } = getConfig() - const account = privateKeyToAccount(`0x${privateKey}`) - const accountTwo = privateKeyToAccount(`0x${privateKeyTwo}`) - - let [smartAccount, smartAccountTwo]: BiconomySmartAccountV2[] = [] - let [smartAccountAddress, smartAccountAddressTwo]: Hex[] = [] - - const [walletClient, walletClientTwo, walletClientRandom] = [ - createWalletClient({ - account, - chain, - transport: http() - }), - createWalletClient({ - account: accountTwo, - chain, - transport: http() - }), - createWalletClient({ - account: privateKeyToAccount(generatePrivateKey()), - chain, - transport: http() - }) - ] - - beforeAll(async () => { - ;[smartAccount, smartAccountTwo] = await Promise.all( - [walletClient, walletClientTwo].map((client) => - createSmartAccountClient({ - chainId, - signer: client, - bundlerUrl, - paymasterUrl - }) - ) - ) - ;[smartAccountAddress, smartAccountAddressTwo] = await Promise.all( - [smartAccount, smartAccountTwo].map((account) => - account.getAccountAddress() - ) - ) - }) test.concurrent( "should quickly run a write test in the playground ", async () => { - const addresses = await Promise.all([ - walletClient.account.address, - smartAccountAddress, - walletClientTwo.account.address, - smartAccountAddressTwo - ]) - expect(addresses.every(Boolean)).toBe(true) + + const { privateKey } = getConfig(); + const incrementCountContractAdd = "0xcf29227477393728935BdBB86770f8F81b698F1A"; + + // const customChain = getCustomChain( + // "Bera", + // 80084, + // "https://bartio.rpc.b-harvest.io", + // "https://bartio.beratrail.io/tx" + // ) + + // Switch to this line to test against Amoy + const customChain = polygonAmoy; + const chainId = customChain.id; + const bundlerUrl = getBundlerUrl(chainId); + + const paymasterUrls = { + 80002: getPaymasterUrl(chainId, "_sTfkyAEp.552504b5-9093-4d4b-94dd-701f85a267ea"), + 80084: getPaymasterUrl(chainId, "9ooHeMdTl.aa829ad6-e07b-4fcb-afc2-584e3400b4f5") + } + + const paymasterUrl = paymasterUrls[chainId]; + const account = privateKeyToAccount(`0x${privateKey}`); + + const walletClientWithCustomChain = createWalletClient({ + account, + chain: customChain, + transport: http() + }) + + const smartAccount = await createSmartAccountClient({ + signer: walletClientWithCustomChain, + bundlerUrl, + paymasterUrl, + customChain + }) + + const smartAccountAddress: Hex = await smartAccount.getAddress(); + + const [balance] = await smartAccount.getBalances(); + if (balance.amount <= 0) console.warn("Smart account balance is zero"); + + const policy: PolicyLeaf[] = [ + { + contractAddress: incrementCountContractAdd, + functionSelector: "increment()", + rules: [], + interval: { + validUntil: 0, + validAfter: 0, + }, + valueLimit: BigInt(0), + }, + ]; + + const { wait } = await createSession(smartAccount, policy, null, withSponsorship); + const { success } = await wait(); + + expect(success).toBe("true"); + + const smartAccountWithSession = await createSessionSmartAccountClient( + { + accountAddress: smartAccountAddress, // Set the account address on behalf of the user + bundlerUrl, + paymasterUrl, + chainId, + }, + "DEFAULT_STORE" // Storage client, full Session or smartAccount address if using default storage + ); + + const { wait: mintWait } = await smartAccountWithSession.sendTransaction( + { + to: incrementCountContractAdd, + data: encodeFunctionData({ + abi: parseAbi(["function increment()"]), + functionName: "increment", + args: [], + }), + }, + { paymasterServiceData: { mode: PaymasterMode.SPONSORED } }, + { leafIndex: "LAST_LEAF" }, + ); + + const { success: mintSuccess, receipt } = await mintWait(); + expect(mintSuccess).toBe("true"); + }, 30000 ) diff --git a/tests/utils.ts b/tests/utils.ts index 614ea2e4a..ce682624a 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -187,5 +187,7 @@ export const topUp = async ( } } -export const getBundlerUrl = (chainId: number) => - `https://bundler.biconomy.io/api/v2/${chainId}/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f14` +export const getBundlerUrl = (chainId: number, apiKey?: string) => + `https://bundler.biconomy.io/api/v2/${chainId}/${apiKey ?? "nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f14"}` + +export const getPaymasterUrl = (chainId: number, apiKey: string) => `https://paymaster.biconomy.io/api/v1/${chainId}/${apiKey}`