diff --git a/.github/workflows/test-playground.yml b/.github/workflows/test-playground.yml new file mode 100644 index 00000000..e7fdf877 --- /dev/null +++ b/.github/workflows/test-playground.yml @@ -0,0 +1,25 @@ +name: test-playground +on: + workflow_dispatch: + push: + branches: + - test/* +jobs: + test-playground: + name: test-playground + permissions: write-all + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + uses: ./.github/actions/install-dependencies + + - name: Run the tests + run: bun run test:ci -t=Playground + env: + E2E_PRIVATE_KEY_ONE: ${{ secrets.E2E_PRIVATE_KEY_ONE }} + TESTING: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 2229bbcc..d8f95d7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # @biconomy/account +## 4.6.2 + +### Patch Changes + +- Temporary removal of DAN + +## 4.6.1 + +### Patch Changes + +- Taiko testnet fix + ## 4.5.5 ### Patch Changes @@ -13,6 +25,7 @@ - Distributed Session Keys ## 4.5.3 + ## 4.6.0 ### Minor Changes diff --git a/bun.lockb b/bun.lockb index 0e8882e5..4307e05c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 30354390..3f618bbd 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "sideEffects": false, "name": "@biconomy/account", "author": "Biconomy", - "version": "4.6.0", + "version": "4.6.2", "description": "SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.", "keywords": [ "erc-7579", @@ -120,7 +120,6 @@ "commit-msg": "npx --no -- commitlint --edit ${1}" }, "dependencies": { - "@silencelaboratories/walletprovider-sdk": "^0.1.0", "merkletreejs": "^0.4.0" } } diff --git a/src/account/BaseSmartContractAccount.ts b/src/account/BaseSmartContractAccount.ts index 7d3c24cb..799ef04b 100644 --- a/src/account/BaseSmartContractAccount.ts +++ b/src/account/BaseSmartContractAccount.ts @@ -28,8 +28,7 @@ export enum DeploymentState { export abstract class BaseSmartContractAccount< TSigner extends SmartAccountSigner = SmartAccountSigner -> implements ISmartContractAccount -{ +> implements ISmartContractAccount { protected factoryAddress: Address protected deploymentState: DeploymentState = DeploymentState.UNDEFINED @@ -209,6 +208,7 @@ export abstract class BaseSmartContractAccount< } async getInitCode(): Promise { + if (this.deploymentState === DeploymentState.DEPLOYED) { return "0x" } @@ -228,12 +228,14 @@ export abstract class BaseSmartContractAccount< } async getAddress(): Promise
{ + if (!this.accountAddress) { const initCode = await this._getAccountInitCode() Logger.log("[BaseSmartContractAccount](getAddress) initCode: ", initCode) try { await this.entryPoint.simulate.getSenderAddress([initCode]) } catch (err: any) { + Logger.log( "[BaseSmartContractAccount](getAddress) getSenderAddress err: ", err diff --git a/src/account/BiconomySmartAccountV2.ts b/src/account/BiconomySmartAccountV2.ts index 059878b7..5816d63f 100644 --- a/src/account/BiconomySmartAccountV2.ts +++ b/src/account/BiconomySmartAccountV2.ts @@ -5,6 +5,7 @@ import { type GetContractReturnType, type Hex, type PublicClient, + type WalletClient, concat, concatHex, createPublicClient, @@ -21,6 +22,7 @@ import { toBytes, toHex, } from "viem"; +import { taikoHekla } from "viem/chains"; import type { IBundler } from "../bundler/IBundler.js"; import { Bundler, @@ -35,7 +37,6 @@ import { type SessionType, createECDSAOwnershipValidationModule, getBatchSessionTxParams, - getDanSessionTxParams, getSingleSessionTxParams } from "../modules" import type { ISessionStorage } from "../modules/interfaces/ISessionStorage.js" @@ -75,6 +76,7 @@ import { MAGIC_BYTES, NATIVE_TOKEN_ALIAS, PROXY_CREATION_CODE, + TAIKO_FACTORY_ADDRESS, } from "./utils/Constants.js"; import type { BalancePayload, @@ -146,23 +148,31 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { private constructor( readonly biconomySmartAccountConfig: BiconomySmartAccountV2ConfigConstructorProps, ) { + + const chain = biconomySmartAccountConfig.viemChain ?? + biconomySmartAccountConfig.customChain ?? + getChain(biconomySmartAccountConfig.chainId) + + const rpcClient = biconomySmartAccountConfig.rpcUrl || + getChain(biconomySmartAccountConfig.chainId).rpcUrls.default.http[0] + + const defaultedFactoryAddress = chain?.id === taikoHekla.id ? TAIKO_FACTORY_ADDRESS : + biconomySmartAccountConfig.factoryAddress ?? + DEFAULT_BICONOMY_FACTORY_ADDRESS; + + const isDefaultFactory = [DEFAULT_BICONOMY_FACTORY_ADDRESS, TAIKO_FACTORY_ADDRESS].some(address => addressEquals(defaultedFactoryAddress, address)); + super({ ...biconomySmartAccountConfig, - chain: - biconomySmartAccountConfig.viemChain ?? - biconomySmartAccountConfig.customChain ?? - getChain(biconomySmartAccountConfig.chainId), - rpcClient: - biconomySmartAccountConfig.rpcUrl || - getChain(biconomySmartAccountConfig.chainId).rpcUrls.default.http[0], + chain, + rpcClient, entryPointAddress: (biconomySmartAccountConfig.entryPointAddress as Hex) ?? DEFAULT_ENTRYPOINT_ADDRESS, accountAddress: (biconomySmartAccountConfig.accountAddress as Hex) ?? undefined, factoryAddress: - biconomySmartAccountConfig.factoryAddress ?? - DEFAULT_BICONOMY_FACTORY_ADDRESS, + defaultedFactoryAddress, }); this.sessionData = biconomySmartAccountConfig.sessionData @@ -194,10 +204,12 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { this.bundler = biconomySmartAccountConfig.bundler; + const defaultFallbackHandlerAddress = - this.factoryAddress === DEFAULT_BICONOMY_FACTORY_ADDRESS + isDefaultFactory ? DEFAULT_FALLBACK_HANDLER_ADDRESS : biconomySmartAccountConfig.defaultFallbackHandler; + if (!defaultFallbackHandlerAddress) { throw new Error("Default Fallback Handler address is not provided"); } @@ -212,14 +224,8 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { biconomySmartAccountConfig.activeValidationModule!; this.provider = createPublicClient({ - chain: - biconomySmartAccountConfig.viemChain ?? - biconomySmartAccountConfig.customChain ?? - getChain(biconomySmartAccountConfig.chainId), - transport: http( - biconomySmartAccountConfig.rpcUrl || - getChain(biconomySmartAccountConfig.chainId).rpcUrls.default.http[0] - ) + chain, + transport: http(rpcClient) }) this.scanForUpgradedAccountsFromV1 = @@ -705,10 +711,7 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { } } - const counterFactualAddressV2 = await this.getCounterFactualAddressV2({ - validationModule, - index, - }); + const counterFactualAddressV2 = await this.getCounterFactualAddressV2({ validationModule, index }); return counterFactualAddressV2; } @@ -720,13 +723,31 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { const index = params?.index ?? this.index; try { + + const owner = validationModule.getAddress() + const moduleSetupData = (await validationModule.getInitData()) as Hex + + if (this.chainId === taikoHekla.id) { + + const factoryContract = getContract({ + address: TAIKO_FACTORY_ADDRESS, + abi: BiconomyFactoryAbi, + client: { public: this.provider, wallet: this.getSigner() as unknown as WalletClient } + }) + + const smartAccountAddressFromContracts = + await factoryContract.read.getAddressForCounterFactualAccount([owner, moduleSetupData, BigInt(index)]) + + return smartAccountAddressFromContracts + } + const initCalldata = encodeFunctionData({ abi: BiconomyAccountAbi, functionName: "init", args: [ this.defaultFallbackHandlerAddress, - validationModule.getAddress() as Hex, - (await validationModule.getInitData()) as Hex, + owner, + moduleSetupData, ], }); @@ -845,15 +866,15 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { * Return the value to put into the "initCode" field, if the account is not yet deployed. * This value holds the "factory" address, followed by this account's information */ - async getAccountInitCode(): Promise { + public override async getAccountInitCode(): Promise { + this.isDefaultValidationModuleDefined(); if (await this.isAccountDeployed()) return "0x"; - return concatHex([ - this.factoryAddress as Hex, - (await this.getFactoryData()) ?? "0x", - ]); + const factoryData = await this.getFactoryData() as Hex; + + return concatHex([this.factoryAddress, factoryData]); } /** @@ -1317,6 +1338,7 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { userOp: Partial, stateOverrideSet?: StateOverrideSet, ): Promise> { + if (!this.bundler) throw new Error("Bundler is not provided"); const requiredFields: UserOperationKey[] = [ "sender", @@ -1336,12 +1358,17 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { maxFeePerGas, maxPriorityFeePerGas, } = await this.bundler.estimateUserOpGas(userOp, stateOverrideSet); + + + // if neither user sent gas fee nor the bundler, estimate gas from provider if ( !userOp.maxFeePerGas && !userOp.maxPriorityFeePerGas && (!maxFeePerGas || !maxPriorityFeePerGas) ) { + + const feeData = await this.provider.estimateFeesPerGas(); if (feeData.maxFeePerGas?.toString()) { finalUserOp.maxFeePerGas = `0x${feeData.maxFeePerGas.toString( @@ -1609,11 +1636,11 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { if (!defaultedChain) throw new Error("Chain is not provided") if (this.sessionType === "DISTRIBUTED_KEY") { - return getDanSessionTxParams( - defaultedConditionalSession, - defaultedChain, - correspondingIndex - ) + // return getDanSessionTxParams( + // defaultedConditionalSession, + // defaultedChain, + // correspondingIndex + // ) } if (this.sessionType === "BATCHED") { return getBatchSessionTxParams( @@ -1694,6 +1721,8 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { dummySignatureFetchPromise, ]); + const sender = await this.getAccountAddress() as Hex + if (transactions.length === 0) { throw new Error("Transactions array cannot be empty"); } @@ -1708,7 +1737,7 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { } let userOp: Partial = { - sender: (await this.getAccountAddress()) as Hex, + sender, nonce: toHex(nonceFromFetch), initCode, callData, @@ -2115,14 +2144,13 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount { this.isDefaultValidationModuleDefined(); + const defaultValidationModuleAddress = this.defaultValidationModule.getAddress(); + const defaultValidationInitData = await this.defaultValidationModule.getInitData(); + return encodeFunctionData({ abi: BiconomyFactoryAbi, functionName: "deployCounterFactualAccount", - args: [ - this.defaultValidationModule.getAddress() as Hex, - (await this.defaultValidationModule.getInitData()) as Hex, - BigInt(this.index), - ], + args: [defaultValidationModuleAddress, defaultValidationInitData, BigInt(this.index)], }); } diff --git a/src/account/utils/Constants.ts b/src/account/utils/Constants.ts index df309580..06d2f042 100644 --- a/src/account/utils/Constants.ts +++ b/src/account/utils/Constants.ts @@ -1,7 +1,6 @@ import type { Hex } from "viem" import type { BiconomyFactories, - BiconomyFactoriesByVersion, BiconomyImplementations, BiconomyImplementationsByVersion, EntryPointAddresses, @@ -22,6 +21,7 @@ export const ENTRYPOINT_ADDRESSES: EntryPointAddresses = { } // will always be latest factory address +export const TAIKO_FACTORY_ADDRESS = "0x000008B3078bA5ed444FFf7658F76385F6004e7A"; //https://biconomyworkspace.slack.com/archives/C061BSA9279/p1721234773541039 export const DEFAULT_BICONOMY_FACTORY_ADDRESS = "0x000000a56Aaca3e9a4C479ea6b6CD0DbcB6634F5" export const DEFAULT_FALLBACK_HANDLER_ADDRESS = @@ -47,11 +47,6 @@ export const ENTRYPOINT_ADDRESSES_BY_VERSION: EntryPointAddressesByVersion = { V0_0_6: "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789" } -export const BICONOMY_FACTORY_ADDRESSES_BY_VERSION: BiconomyFactoriesByVersion = - Object.fromEntries( - Object.entries(BICONOMY_FACTORY_ADDRESSES).map(([k, v]) => [v, k]) - ) - export const BICONOMY_IMPLEMENTATION_ADDRESSES_BY_VERSION: BiconomyImplementationsByVersion = Object.fromEntries( Object.entries(BICONOMY_IMPLEMENTATION_ADDRESSES).map(([k, v]) => [v, k]) diff --git a/src/modules/DANSessionKeyManagerModule.ts b/src/modules/DANSessionKeyManagerModule.ts deleted file mode 100644 index b9de9233..00000000 --- a/src/modules/DANSessionKeyManagerModule.ts +++ /dev/null @@ -1,375 +0,0 @@ -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/ECDSAOwnershipValidationModule.ts b/src/modules/ECDSAOwnershipValidationModule.ts index 0d472c3e..1e6b4b86 100644 --- a/src/modules/ECDSAOwnershipValidationModule.ts +++ b/src/modules/ECDSAOwnershipValidationModule.ts @@ -77,14 +77,13 @@ export class ECDSAOwnershipValidationModule extends BaseValidationModule { // Note: other modules may need additional attributes to build init data async getInitData(): Promise { const ecdsaOwnerAddress = await this.signer.getAddress() - const moduleRegistryParsedAbi = parseAbi([ - "function initForSmartAccount(address owner)" - ]) + const moduleRegistryParsedAbi = parseAbi(["function initForSmartAccount(address owner)"]) const ecdsaOwnershipInitData = encodeFunctionData({ abi: moduleRegistryParsedAbi, functionName: "initForSmartAccount", args: [ecdsaOwnerAddress] }) + return ecdsaOwnershipInitData } diff --git a/src/modules/index.ts b/src/modules/index.ts index 50877d08..57582607 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -12,10 +12,8 @@ export * from "./session-validation-modules/ERC20SessionValidationModule.js" export * from "./sessions/abi.js" export * from "./sessions/erc20.js" export * from "./sessions/batch.js" -export * from "./sessions/dan.js" export * from "./sessions/sessionSmartAccountClient.js" export * from "./session-storage/index.js" -import { DANSessionKeyManagerModule } from "./DANSessionKeyManagerModule.js" import { BatchedSessionRouterModule, ECDSAOwnershipValidationModule, @@ -30,8 +28,6 @@ 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/dan.ts b/src/modules/sessions/dan.ts deleted file mode 100644 index d303a3fc..00000000 --- a/src/modules/sessions/dan.ts +++ /dev/null @@ -1,410 +0,0 @@ -import { EOAAuth, EphAuth, type IBrowserWallet, NetworkSigner, WalletProviderServiceClient, computeAddress } from "@silencelaboratories/walletprovider-sdk" -import type { Chain, Hex } from "viem" -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 { 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 * 24 * 365 // 1 year -export const QUORUM_PARTIES = 5 -export const QUORUM_THRESHOLD = 3 - -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 | null, - /** The build userop dto */ - buildUseropDto?: BuildUserOpOptions, - /** The chain ID */ - chainId?: number, - /** Optional. The user's {@link IBrowserWallet} instance. Default will be the signer associated with the smart account. */ - 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 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. */ - threshold: number; - /** The eoa that was used to create the session */ - eoaAddress: Hex; - /** the chainId is relevant only to the */ - chainId: number -} - -export type DanSessionKeyRequestParams = { - /** Relevant smart account */ - smartAccountClient: BiconomySmartAccountV2; - /** Optional browser wallet. If using wagmi can be set to connector.getProvider() from useAccount hook */ - browserWallet?: IBrowserWallet; - /** Optional hardcoded values if required */ - hardcodedValues?: Partial; - /** Optional duration of the session key in seconds. Default is 3600 seconds. */ - duration?: number; - /** Optional chainId. Will be inferred if left unset. */ - 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 = { - params: { - 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 0f7817b0..97d3f660 100644 --- a/src/modules/sessions/sessionSmartAccountClient.ts +++ b/src/modules/sessions/sessionSmartAccountClient.ts @@ -13,7 +13,6 @@ import type { UserOpResponse } from "../../bundler/index.js"; import { type SessionSearchParam, createBatchedSessionRouterModule, - createDANSessionKeyManagerModule, createSessionKeyManagerModule, type getSingleSessionTxParams, resumeSession, @@ -145,17 +144,11 @@ export const createSessionSmartAccountClient = async ( sessionKeyManagerModule: sessionModule, }, ); - const danSessionValidationModule = await createDANSessionKeyManagerModule({ - smartAccountAddress: biconomySmartAccountConfig.accountAddress, - sessionStorageClient, - }); const activeValidationModule = defaultedSessionType === "BATCHED" ? batchedSessionValidationModule - : defaultedSessionType === "STANDARD" - ? sessionModule - : danSessionValidationModule; + : sessionModule; return await createSmartAccountClient({ ...biconomySmartAccountConfig, diff --git a/src/modules/utils/Helper.ts b/src/modules/utils/Helper.ts index 4904d7f5..fdbbcdf6 100644 --- a/src/modules/utils/Helper.ts +++ b/src/modules/utils/Helper.ts @@ -1,11 +1,8 @@ -import type { IBrowserWallet, TypedData } from "@silencelaboratories/walletprovider-sdk" import { type Address, type ByteArray, type Chain, - type EIP1193Provider, type Hex, - type WalletClient, encodeAbiParameters, isAddress, keccak256, @@ -254,40 +251,4 @@ export const hexToUint8Array = (hex: string) => { return array } -// Sign data using the secret key stored on Browser Wallet -// It creates a popup window, presenting the human readable form of `request` -// Throws an error if User rejected signature -export class BrowserWallet implements IBrowserWallet { - provider: EIP1193Provider - constructor(provider: EIP1193Provider) { - this.provider = provider - } - - async signTypedData( - from: string, - request: TypedData - ): Promise { - return await this.provider.request({ - method: "eth_signTypedData_v4", - // @ts-ignore - params: [from, JSON.stringify(request)] - }) - } -} - -// Sign data using the secret key stored on Browser Wallet -// It creates a popup window, presenting the human readable form of `request` -// Throws an error if User rejected signature -export class NodeWallet implements IBrowserWallet { - walletClient: WalletClient - - constructor(walletClient: WalletClient) { - this.walletClient = walletClient - } - - async signTypedData(_: string, request: TypedData): Promise { - // @ts-ignore - return await this.walletClient.signTypedData(request) - } -} diff --git a/tests/account/write.test.ts b/tests/account/write.test.ts index d771f161..61f10f23 100644 --- a/tests/account/write.test.ts +++ b/tests/account/write.test.ts @@ -8,7 +8,7 @@ import { parseAbi } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" -import { arbitrumSepolia, polygonAmoy } from "viem/chains" +import { arbitrumSepolia, polygonAmoy, taikoHekla } from "viem/chains" import { beforeAll, describe, expect, test } from "vitest" import { type BiconomySmartAccountV2, @@ -668,4 +668,92 @@ describe("Account:Write", async () => { const response = await wait() expect(response.success).toBe("true") }, 50000) + + test.concurrent.skip( + "should check taiko network", + async () => { + + const { privateKey, privateKeyTwo } = getConfig(); + + const customChain = taikoHekla + const chainId = customChain.id; + const bundlerUrl = getBundlerUrl(chainId); + + const account = privateKeyToAccount(`0x${privateKey}`); + const recipientAccount = privateKeyToAccount(`0x${privateKeyTwo}`); + + const walletClientWithCustomChain = createWalletClient({ + account, + chain: customChain, + transport: http(), + }) + + const publicClient = createPublicClient({ + chain: customChain, + transport: http(), + }) + + const smartAccount = await createSmartAccountClient({ + signer: walletClientWithCustomChain, + bundlerUrl, + customChain, + }) + + const smartAccountAddress: Hex = await smartAccount.getAddress(); + const [balance] = await smartAccount.getBalances(); + if (balance.amount <= 1n) console.warn("Insufficient balance in smart account") + + const recipientBalanceBefore = await checkBalance(recipientAccount.address, undefined, customChain); + + const userOp = await smartAccount.buildUserOp([ + { + to: recipientAccount.address, + value: 1n + } + ]) + + userOp.signature = undefined + + const signedUserOp = await smartAccount.signUserOp(userOp) + + const entrypointContract = getContract({ + address: DEFAULT_ENTRYPOINT_ADDRESS, + abi: EntryPointAbi, + client: { public: publicClient, wallet: walletClientWithCustomChain } + }) + + const hash = await entrypointContract.write.handleOps([ + [ + { + sender: signedUserOp.sender as Hex, + nonce: BigInt(signedUserOp.nonce ?? 0), + callGasLimit: BigInt(signedUserOp.callGasLimit ?? 0), + verificationGasLimit: BigInt(signedUserOp.verificationGasLimit ?? 0), + preVerificationGas: BigInt(signedUserOp.preVerificationGas ?? 0), + maxFeePerGas: BigInt(signedUserOp.maxFeePerGas ?? 0), + maxPriorityFeePerGas: BigInt(signedUserOp.maxPriorityFeePerGas ?? 0), + initCode: signedUserOp.initCode as Hex, + callData: signedUserOp.callData as Hex, + paymasterAndData: signedUserOp.paymasterAndData as Hex, + signature: signedUserOp.signature as Hex + } + ], + account.address + ]) + + const { status, transactionHash } = + await publicClient.waitForTransactionReceipt({ hash }) + + const recipientBalanceAfter = await checkBalance(recipientAccount.address, undefined, customChain); + + expect(status).toBe("success") + expect(transactionHash).toBeTruthy() + + expect(recipientBalanceAfter).toBe(recipientBalanceBefore + 1n) + + }, + 70000 + ) + + }) diff --git a/tests/modules/write.test.ts b/tests/modules/write.test.ts index 4369a906..0a77f160 100644 --- a/tests/modules/write.test.ts +++ b/tests/modules/write.test.ts @@ -12,7 +12,7 @@ import { slice, toFunctionSelector } from "viem" -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { privateKeyToAccount } from "viem/accounts" import { beforeAll, describe, expect, test } from "vitest" import { type BiconomySmartAccountV2, @@ -24,27 +24,19 @@ 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 { DANSessionKeyManagerModule } from "../../src/modules/DANSessionKeyManagerModule" import { SessionMemoryStorage } from "../../src/modules/session-storage/SessionMemoryStorage" import { createSessionKeyEOA } from "../../src/modules/session-storage/utils" import { @@ -1468,206 +1460,6 @@ describe("Modules:Write", () => { 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/write.test.ts b/tests/playground/write.test.ts index 4d2941f0..0d4001b3 100644 --- a/tests/playground/write.test.ts +++ b/tests/playground/write.test.ts @@ -1,114 +1,77 @@ -import { http, type Hex, createWalletClient, encodeFunctionData, parseAbi } from "viem" -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" -import { polygonAmoy } from "viem/chains" +import { http, type Hex, createPublicClient, createWalletClient } from "viem"; +import { 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, - getChain, - getCustomChain -} from "../../src/account" -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", () => { - - test.concurrent( - "should quickly run a write test in the playground ", - async () => { - - 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 - ) -}) +import { type BiconomySmartAccountV2, createSmartAccountClient } from "../../src" +import { getBundlerUrl, getConfig } from "../utils"; + + +describe("Playground", async () => { + const { privateKey, chain } = getConfig() + const customChain = chain ?? polygonAmoy; + + /* + // Alternatively, you can use the following custom chain configuration... + + const customChain = getCustomChain( + "Bera", + 80084, + "https://bartio.rpc.b-harvest.io", + "https://bartio.beratrail.io/tx" + ) + + */ + + const chainId = customChain.id; + const bundlerUrl = getBundlerUrl(chainId); + const account = privateKeyToAccount(`0x${privateKey}`); + + const publicClient = createPublicClient({ + chain: customChain, + transport: http() + }) + + const walletClient = createWalletClient({ + account, + chain: customChain, + transport: http() + }) + + let smartAccount: BiconomySmartAccountV2; + let smartAccountAddress: Hex; + + beforeAll(async () => { + smartAccount = await createSmartAccountClient({ + signer: walletClient, + bundlerUrl, + customChain + }) + + smartAccountAddress = await smartAccount.getAddress(); + + const [balance] = await smartAccount.getBalances(); + const walletClientBalance = await publicClient.getBalance({ address: account.address }); + + console.log("account.address: ", account.address); + console.log("smartAccountAddress: ", smartAccountAddress); + + if (balance.amount <= 0) console.warn("Smart account balance is zero"); + if (walletClientBalance <= 0) console.warn("Wallet client balance is zero"); + }) + + + test("should send some native token for the configured chain", async () => { + + const { wait } = await smartAccount.sendTransaction( + { + to: "0x3079B249DFDE4692D7844aA261f8cf7D927A0DA5", + value: BigInt(1) + }, + ); + + const { success } = await wait(); + expect(success).toBe("true"); + + }, 30000) + +}) \ No newline at end of file diff --git a/tests/utils.ts b/tests/utils.ts index ce682624..ba0e14ef 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -13,16 +13,10 @@ import { extractChainIdFromBundlerUrl, extractChainIdFromPaymasterUrl } from "../src/bundler" +import { PaymasterMode } from "../src/paymaster" export const getEnvVars = () => { - const fields = [ - "BUNDLER_URL", - "E2E_PRIVATE_KEY_ONE", - "E2E_PRIVATE_KEY_TWO", - "E2E_BICO_PAYMASTER_KEY_AMOY", - "E2E_BICO_PAYMASTER_KEY_BASE", - "CHAIN_ID" - ] + const fields = ["E2E_PRIVATE_KEY_ONE"] const errorFields = fields.filter((field) => !process?.env?.[field]) if (errorFields.length) { @@ -191,3 +185,7 @@ 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}` + +export const withSponsorship = { + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, +};