From 907a2dc70de4122f5b84d4f146b6676979272fdb Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Tue, 6 Aug 2024 16:33:16 +0100 Subject: [PATCH 1/6] chore: release v4.5.5 --- CHANGELOG.md | 8 +++++++- package.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34d09d9f..f2d4b4aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,17 @@ # @biconomy/account -## 4.5.4 +## 4.5.5 ### Patch Changes - Fix CJS builds +## 4.5.4 + +### Patch Changes + +- Distributed Session Keys + ## 4.5.3 ### Minor Changes diff --git a/package.json b/package.json index 6163ac91..1181cd17 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "sideEffects": false, "name": "@biconomy/account", "author": "Biconomy", - "version": "4.5.4", + "version": "4.5.5", "description": "SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.", "keywords": [ "erc-7579", From 41140a7f1ff582facad3ab24b4114240a4da1b4d Mon Sep 17 00:00:00 2001 From: Vasile Gabriel Marian <56271768+VGabriel45@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:04:44 +0300 Subject: [PATCH 2/6] fix: remove sign typed data (#548) * fix: remove signTypedData implementation * fix: fixed build issues --------- Co-authored-by: GabiDev --- src/account/BaseSmartContractAccount.ts | 14 ++++++++------ src/account/utils/Types.ts | 4 ++-- tests/account/read.test.ts | 9 ++------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/account/BaseSmartContractAccount.ts b/src/account/BaseSmartContractAccount.ts index 6a01e541..7d3c24cb 100644 --- a/src/account/BaseSmartContractAccount.ts +++ b/src/account/BaseSmartContractAccount.ts @@ -132,7 +132,7 @@ export abstract class BaseSmartContractAccount< * @param _params -- Typed Data params to sign */ async signTypedData(_params: SignTypedDataParams): Promise<`0x${string}`> { - return this.signer.signTypedData(_params) + throw new Error("signTypedData not supported") } /** @@ -158,14 +158,16 @@ export abstract class BaseSmartContractAccount< * @param params -- Typed Data params to sign */ async signTypedDataWith6492( + // @ts-ignore params: SignTypedDataParams ): Promise<`0x${string}`> { - const [isDeployed, signature] = await Promise.all([ - this.isAccountDeployed(), - this.signTypedData(params) - ]) + throw new Error("signTypedDataWith6492 not supported") + // const [isDeployed, signature] = await Promise.all([ + // this.isAccountDeployed(), + // this.signTypedData(params) + // ]) - return this.create6492Signature(isDeployed, signature) + // return this.create6492Signature(isDeployed, signature) } /** diff --git a/src/account/utils/Types.ts b/src/account/utils/Types.ts index 8f7d2f52..5328427a 100644 --- a/src/account/utils/Types.ts +++ b/src/account/utils/Types.ts @@ -558,7 +558,7 @@ export interface ISmartContractAccount< * @param params - {@link SignTypedDataParams} * @returns the signed hash for the message passed */ - signTypedData(params: SignTypedDataParams): Promise; + // signTypedData(params: SignTypedDataParams): Promise; /** * If the account is not deployed, it will sign the message and then wrap it in 6492 format @@ -574,7 +574,7 @@ export interface ISmartContractAccount< * @param params - {@link SignTypedDataParams} * @returns the signed hash for the params passed in wrapped in 6492 format */ - signTypedDataWith6492(params: SignTypedDataParams): Promise; + // signTypedDataWith6492(params: SignTypedDataParams): Promise; /** * @returns the address of the account diff --git a/tests/account/read.test.ts b/tests/account/read.test.ts index ecb5eae9..20c74fb9 100644 --- a/tests/account/read.test.ts +++ b/tests/account/read.test.ts @@ -967,12 +967,7 @@ describe("Account:Read", () => { } } - const signature = await smartAccount.signTypedData(typedData) - const isVerified = await publicClient.verifyTypedData({ - address: account.address, - signature, - ...typedData - }) - expect(isVerified).toBeTruthy() + const signature = smartAccount.signTypedData(typedData) + expect(signature).rejects.toThrowError("signTypedData not supported") }) }) From 3d5fa496ccd8d8cef9a725d082ccd1c9d1b93a9c Mon Sep 17 00:00:00 2001 From: joepegler Date: Sat, 10 Aug 2024 11:47:49 +0100 Subject: [PATCH 3/6] chore: get signer address (#551) * chore: get signer address * chore: types fix * chore: cont --- src/account/utils/convertSigner.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/account/utils/convertSigner.ts b/src/account/utils/convertSigner.ts index a4a2180a..f9a89569 100644 --- a/src/account/utils/convertSigner.ts +++ b/src/account/utils/convertSigner.ts @@ -1,5 +1,6 @@ import { http, + type Hex, type PrivateKeyAccount, type WalletClient, createWalletClient @@ -101,3 +102,29 @@ export const convertSigner = async ( } return { signer: resolvedSmartAccountSigner, rpcUrl, chainId } } +/* + This function is used to get the signer's address, it can be used to get the signer's address from different types of signers. + The function takes a signer as an argument and returns the signer's address. + The function checks the type of the signer and returns the signer's address based on the type of the signer. + The function throws an error if the signer is not supported. +*/ +export const getSignerAddress = async (signer: SupportedSigner): Promise => { + if (isEthersSigner(signer)) { + const result = await (signer as Signer)?.getAddress(); + if (result) return result as Hex + throw new Error("Unsupported signer"); + } if (isWalletClient(signer)) { + const result = ((signer as WalletClient)?.account?.address); + if (result) return result as Hex + throw new Error("Unsupported signer"); + } if (isPrivateKeyAccount(signer)) { + const result = ((signer as PrivateKeyAccount)?.address); + if (result) return result as Hex + throw new Error("Unsupported signer"); + } if (isAlchemySigner(signer)) { + const result = ((signer as SmartAccountSigner)?.inner?.address); + if (result) return result as Hex + throw new Error("Unsupported signer"); + } + throw new Error("Unsupported signer"); +} \ No newline at end of file From 7b0f899290a1fea1136ac04d538f8b3ae58a54ba Mon Sep 17 00:00:00 2001 From: joepegler Date: Mon, 12 Aug 2024 13:30:46 +0100 Subject: [PATCH 4/6] chore: distributed session keys (#555) * feat: distributed session keys --------- Co-authored-by: livingrockrises <90545960+livingrockrises@users.noreply.github.com> --- .env.example | 3 +- .github/workflows/build.yml | 2 - .github/workflows/coverage.yml | 2 - .github/workflows/docs.yml | 2 - .github/workflows/pr-lint.yml | 2 - .github/workflows/size-report.yml | 2 - .github/workflows/test-read.yml | 2 - .github/workflows/test-write.yml | 2 - .size-limit.json | 8 +- CHANGELOG.md | 3 +- biome.json | 3 +- bun.lockb | Bin 214544 -> 213930 bytes bunfig.toml | 2 - package.json | 9 +- src/account/BiconomySmartAccountV2.ts | 227 ++++----- src/account/utils/Types.ts | 449 +++++++++--------- src/account/utils/convertSigner.ts | 3 +- src/modules/DANSessionKeyManagerModule.ts | 375 +++++++++++++++ src/modules/index.ts | 3 + src/modules/sessions/abi.ts | 261 +++++----- src/modules/sessions/dan.ts | 370 ++++++++++++++- .../sessions/sessionSmartAccountClient.ts | 9 +- src/modules/utils/Helper.ts | 3 +- src/modules/utils/Types.ts | 2 +- tests/modules/write.test.ts | 278 +++++++++-- tests/playground/read.test.ts | 75 --- tests/playground/write.test.ts | 160 ++++--- tests/utils.ts | 6 +- 28 files changed, 1584 insertions(+), 679 deletions(-) delete mode 100644 bunfig.toml create mode 100644 src/modules/DANSessionKeyManagerModule.ts delete mode 100644 tests/playground/read.test.ts diff --git a/.env.example b/.env.example index 4c5a02a1..f3592f0c 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 f264edf5..08c18cd3 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 4f2c6aad..949c9eb0 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 63ed3848..078b083b 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 028022ec..3e77f783 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 63e396f3..60d32b4e 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 14f88a31..f6338828 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 446fb63d..d5ebe9dd 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 52596234..4b0c64b5 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 f2d4b4aa..2229bbcc 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 4e9fc4e3..38251fac 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 d07f063258fc33764a4b3a31210c80f4c204a197..0e8882e575de288f6efbff7277111470c0459f7c 100755 GIT binary patch delta 47368 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 delta 47643 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 { 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 5328427a..28ccf1ae 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 f9a89569..e4370eb1 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 00000000..b9de9233 --- /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 c5d7f827..50877d08 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 faa1c078..1b974156 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 8b0fd807..d303a3fc 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 0a657d60..0f7817b0 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 78806c6a..4904d7f5 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 af50c362..8cec17e5 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 2bdf8697..aea3b7c9 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 c754b00f..00000000 --- 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 1ee45fc2..4d2941f0 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 614ea2e4..ce682624 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}` From ebe6ffc884b65af52bf463265f91d99963f13fcc Mon Sep 17 00:00:00 2001 From: joepegler Date: Mon, 12 Aug 2024 14:31:49 +0100 Subject: [PATCH 5/6] chore: nurse verification gas limit in tests (#557) --- tests/modules/write.test.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/modules/write.test.ts b/tests/modules/write.test.ts index aea3b7c9..d60a6314 100644 --- a/tests/modules/write.test.ts +++ b/tests/modules/write.test.ts @@ -479,8 +479,18 @@ describe("Modules:Write", () => { }; const [partialUserOp1, partialUserOp2] = await Promise.all([ - baseAccount.buildUserOp([transaction], withSponsorship), - polygonAccount.buildUserOp([transaction], withSponsorship), + baseAccount.buildUserOp([transaction], { + ...withSponsorship, gasOffset: { + verificationGasLimitOffsetPct: 100, + preVerificationGasOffsetPct: 50 + } + }), + polygonAccount.buildUserOp([transaction], { + ...withSponsorship, gasOffset: { + verificationGasLimitOffsetPct: 100, + preVerificationGasOffsetPct: 50 + } + }), ]); expect(partialUserOp1.paymasterAndData).not.toBe("0x"); From c997163be40a2722526bef0fc50b0db9f01d5ce5 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Mon, 12 Aug 2024 14:32:29 +0100 Subject: [PATCH 6/6] chore: remove base paymaster (aa33) --- tests/modules/write.test.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/modules/write.test.ts b/tests/modules/write.test.ts index d60a6314..4369a906 100644 --- a/tests/modules/write.test.ts +++ b/tests/modules/write.test.ts @@ -397,7 +397,7 @@ describe("Modules:Write", () => { expect(nftBalanceAfter - nftBalanceBefore).toBe(1n); }, 50000); - test("should use MultichainValidationModule to mint an NFT on two chains with sponsorship", async () => { + test.skip("should use MultichainValidationModule to mint an NFT on two chains with sponsorship", async () => { const nftAddress: Hex = "0x1758f42Af7026fBbB559Dc60EcE0De3ef81f665e"; const chainIdBase = 84532; @@ -478,19 +478,17 @@ describe("Modules:Write", () => { data: encodedCall, }; + const options = { + ...withSponsorship, gasOffset: { + verificationGasLimitOffsetPct: 100, + preVerificationGasOffsetPct: 50, + }, + nonceOptions + } + const [partialUserOp1, partialUserOp2] = await Promise.all([ - baseAccount.buildUserOp([transaction], { - ...withSponsorship, gasOffset: { - verificationGasLimitOffsetPct: 100, - preVerificationGasOffsetPct: 50 - } - }), - polygonAccount.buildUserOp([transaction], { - ...withSponsorship, gasOffset: { - verificationGasLimitOffsetPct: 100, - preVerificationGasOffsetPct: 50 - } - }), + baseAccount.buildUserOp([transaction], options), + polygonAccount.buildUserOp([transaction], options), ]); expect(partialUserOp1.paymasterAndData).not.toBe("0x");