diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 9267e8bfb..c20b2d6b6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,7 +33,7 @@ jobs: with: json-summary-path: ./coverage/coverage-summary.json json-final-path: "./coverage/coverage-final.json" - vite-config-path: ./tests/vitest.config.ts + vite-config-path: ./test/vitest.config.ts github-token: ${{ secrets.GITHUB_TOKEN }} - name: Upload coverage reports to Codecov diff --git a/.size-limit.json b/.size-limit.json index 35bf1e815..7b8636597 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -14,16 +14,16 @@ }, { "name": "bundler (tree-shaking)", - "path": "./dist/_esm/bundler/index.js", + "path": "./dist/_esm/clients/createBicoBundlerClient.js", "limit": "15 kB", - "import": "{ createBundler }", + "import": "{ createBicoBundlerClient }", "ignore": ["node:fs", "fs"] }, { "name": "paymaster (tree-shaking)", - "path": "./dist/_esm/paymaster/index.js", + "path": "./dist/_esm/clients/createBicoPaymasterClient.js", "limit": "15 kB", - "import": "{ toPaymaster }", + "import": "{ createBicoPaymasterClient }", "ignore": ["node:fs", "fs"] } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index e751d84a3..0b7ebd05f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -164,7 +164,7 @@ Particle Auth Fix - E2E tests for session validation modules ([4ad7ea7](https://github.com/bcnmy/biconomy-client-sdk/pull/401/commits/4ad7ea7f8eb6a28854dcce83834b2b7ff9ad3287)) - Added [TSDoc](https://bcnmy.github.io/biconomy-client-sdk) ([638dae](https://github.com/bcnmy/biconomy-client-sdk/pull/401/commits/638daee0ed6924f67c5151a2d0e5a02d32e4bf23)) - Make txs more typesafe and default with valueOrData ([b1e5b5e](https://github.com/bcnmy/biconomy-client-sdk/pull/401/commits/b1e5b5e02ab3a7fb99faa1d45b55e3cbe8d6bc93)) -- Added createSmartAccountClient alias ([232472](https://github.com/bcnmy/biconomy-client-sdk/pull/401/commits/232472c788bed0619cf6295ade076b6ec3a9e0f8)) +- Added createNexusClient alias ([232472](https://github.com/bcnmy/biconomy-client-sdk/pull/401/commits/232472c788bed0619cf6295ade076b6ec3a9e0f8)) - Improve dx of using paymster to build userOps ([bb54888](https://github.com/bcnmy/biconomy-client-sdk/pull/401/commits/bb548884e76a94a20329e49b18994503de9e3888)) - Add ethers v6 signer support ([9727fd](https://github.com/bcnmy/biconomy-client-sdk/pull/401/commits/9727fd51e47d62904399d17d48f5c9d6b9e591e5)) - Improved dx of using gas payments with erc20 ([741806](https://github.com/bcnmy/biconomy-client-sdk/pull/401/commits/741806da68457eba262e1a49be77fcc24360ba36)) diff --git a/README.md b/README.md index 88e5fabf8..82ac2024c 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ bun i ``` ```typescript -import { createSmartAccountClient } from "@biconomy/account"; +import { createNexusClient } from "@biconomy/account"; -const smartAccount = await createSmartAccountClient({ +const smartAccount = await createNexusClient({ signer: viemWalletOrEthersSigner, bundlerUrl: "", // From dashboard.biconomy.io paymasterUrl: "", // From dashboard.biconomy.io diff --git a/bun.lockb b/bun.lockb index 951562036..db0ee96a0 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 843f9d48e..7a488570e 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,9 @@ "dev": "bun link && concurrently \"bun run esm:watch\" \"bun run cjs:watch\" \"bun run esm:watch:aliases\" \"bun run cjs:watch:aliases\"", "build": "bun run clean && bun run build:cjs && bun run build:esm && bun run build:types", "clean": "rimraf ./dist/_esm ./dist/_cjs ./dist/_types ./dist/tsconfig", - "test": "vitest -c ./tests/vitest.config.ts", + "test": "vitest -c ./src/test/vitest.config.ts", "test:watch": "bun run test dev", - "playground": "RUN_PLAYGROUND=true vitest -c ./tests/vitest.config.ts -t=playground", + "playground": "RUN_PLAYGROUND=true vitest -c ./src/test/vitest.config.ts -t=playground", "playground:watch": "RUN_PLAYGROUND=true bun run test -t=playground --watch", "size": "size-limit", "docs": "typedoc --tsconfig ./tsconfig/tsconfig.esm.json", @@ -107,12 +107,13 @@ "tsc-alias": "^1.8.8", "tslib": "^2.6.3", "typedoc": "^0.25.9", + "viem": "2.21.6", "vitest": "^1.3.1", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5", - "viem": "^2" + "viem": "^2.20.0" }, "commitlint": { "extends": [ diff --git a/scripts/fetch:deployment.ts b/scripts/fetch:deployment.ts index 44b92b7ca..81a0fb0ee 100644 --- a/scripts/fetch:deployment.ts +++ b/scripts/fetch:deployment.ts @@ -52,7 +52,7 @@ export const getDeployments = async () => { const tsAbiPath = isForCore ? `${__dirname}/../src/__contracts/abi/${name}Abi.ts` - : `${__dirname}/../tests/src/__contracts/abi/${name}Abi.ts` + : `${__dirname}/../test/__contracts/abi/${name}Abi.ts` fs.writeFileSync(tsAbiPath, tsAbiContent) @@ -63,7 +63,7 @@ export const getDeployments = async () => { } // Write ABI index file... - const abiIndexContent = `export * from "./UniActionPolicyAbi"\nexport * from "./EntryPointABI"\n${coreFiles + const abiIndexContent = `export * from "./EIP1271Abi"\nexport * from "./UniActionPolicyAbi"\nexport * from "./EntryPointABI"\n${coreFiles .map((file) => `export * from "./${file}Abi"`) .join("\n")}` @@ -76,15 +76,15 @@ export const getDeployments = async () => { const abiIndexPath = `${__dirname}/../src/__contracts/abi/index.ts` fs.writeFileSync(abiIndexPath, abiIndexContent) - const testAbiIndexPath = `${__dirname}/../tests/src/__contracts/abi/index.ts` + const testAbiIndexPath = `${__dirname}/../test/__contracts/abi/index.ts` fs.writeFileSync(testAbiIndexPath, testAbiIndexContent) // Write addresses to src folder const writeAddressesPath = `${__dirname}/../src/__contracts/addresses.ts` - const writeAddressesPathTest = `${__dirname}/../tests/src/__contracts/mockAddresses.ts` + const writeAddressesPathTest = `${__dirname}/../test/__contracts/mockAddresses.ts` const addressesContent = `// The contents of this folder is auto-generated. Please do not edit as your changes are likely to be overwritten\n - import type { Hex } from "viem"\nexport const addresses: Record = ${JSON.stringify( + export const addresses = ${JSON.stringify( Object.keys(deployedContracts) .filter((key) => coreFiles.includes(key)) .reduce((acc, key) => { @@ -96,7 +96,7 @@ export const getDeployments = async () => { )} as const;\nexport default addresses\n` const testAddressesContent = `// The contents of this folder is auto-generated. Please do not edit as your changes are likely to be overwritten\n - import type { Hex } from "viem"\nexport const mockAddresses: Record = ${JSON.stringify( + export const mockAddresses = ${JSON.stringify( Object.keys(deployedContracts) .filter((key) => testFiles.includes(key)) .reduce((acc, key) => { diff --git a/scripts/send:userOp.ts b/scripts/send:userOp.ts index badbbfdd9..0b18d941b 100644 --- a/scripts/send:userOp.ts +++ b/scripts/send:userOp.ts @@ -1,10 +1,7 @@ -import { http, createWalletClient, parseEther } from "viem" +import { http, type PublicClient, parseEther } from "viem" import { privateKeyToAccount } from "viem/accounts" -import { - createK1ValidatorModule, - createSmartAccountClient, - getChain -} from "../src" +import { getChain } from "../src/sdk/account/utils/getChain" +import { createNexusClient } from "../src/sdk/clients/createNexusClient" const k1ValidatorAddress = "0x663E709f60477f07885230E213b8149a7027239B" const factoryAddress = "0x887Ca6FaFD62737D0E79A2b8Da41f0B15A864778" @@ -51,37 +48,40 @@ const account = privateKeyToAccount(`0x${privateKey}`) const accountTwo = privateKeyToAccount(`0x${privateKeyTwo}`) const recipient = accountTwo.address -const [walletClient] = [ - createWalletClient({ - account, - chain, - transport: http() - }) -] - -const smartAccount = await createSmartAccountClient({ +const nexusClient = await createNexusClient({ + holder: account, chain, - signer: walletClient, - bundlerUrl, + transport: http(), + bundlerTransport: http(bundlerUrl), k1ValidatorAddress, factoryAddress }) -const sendUserOperation = async () => { - const transaction = { - to: recipient, // NFT address - data: "0x", - value: parseEther("0.0001") - } +const main = async () => { console.log( "Your smart account will be deployed at address, make sure it has some funds to pay for user ops: ", - await smartAccount.getAddress() + await nexusClient.account.getAddress() ) - const response = await smartAccount.sendTransaction([transaction]) + const hash = await nexusClient.sendTransaction({ + calls: [ + { + to: recipient, + value: parseEther("0.0001") + } + ] + }) - const receipt = await response.wait() - console.log("Receipt: ", receipt) + const receipt = await ( + nexusClient.account.client as PublicClient + ).waitForTransactionReceipt({ hash }) } -sendUserOperation() +main() + .then(() => { + process.exit(0) + }) + .catch((error) => { + console.error(error) + process.exitCode = 1 + }) diff --git a/scripts/viem:bundler.ts b/scripts/viem:bundler.ts new file mode 100644 index 000000000..1daf93806 --- /dev/null +++ b/scripts/viem:bundler.ts @@ -0,0 +1,147 @@ +import { config } from "dotenv" +import { http, type PublicClient, createPublicClient } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import { toNexusAccount } from "../src/sdk/account/toNexusAccount" +import { getChain } from "../src/sdk/account/utils/getChain" +import { createBicoBundlerClient } from "../src/sdk/clients/createBicoBundlerClient" + +config() + +const k1ValidatorAddress = "0x663E709f60477f07885230E213b8149a7027239B" +const factoryAddress = "0x887Ca6FaFD62737D0E79A2b8Da41f0B15A864778" + +const GAS_ESTIMATE = 1.25 + +const safeMultiplier = (bI: bigint, multiplier: number): bigint => + BigInt(Math.round(Number(bI) * multiplier)) + +export const getEnvVars = () => { + return { + bundlerUrl: process.env.BUNDLER_URL || "", + privateKey: process.env.E2E_PRIVATE_KEY_ONE || "", + privateKeyTwo: process.env.E2E_PRIVATE_KEY_TWO || "", + paymasterUrl: process.env.PAYMASTER_URL || "", + chainId: process.env.CHAIN_ID || "0" + } +} + +export const getConfig = () => { + const { + paymasterUrl, + bundlerUrl, + chainId: chainIdFromEnv, + privateKey, + privateKeyTwo + } = getEnvVars() + + const chainId = Number.parseInt(chainIdFromEnv) + + const chain = getChain(chainId) + + return { + chain, + chainId, + paymasterUrl, + bundlerUrl, + privateKey, + privateKeyTwo + } +} + +const { chain, privateKey, privateKeyTwo, bundlerUrl } = getConfig() + +if ([chain, privateKey, privateKeyTwo, bundlerUrl].every(Boolean) !== true) + throw new Error("Missing env vars") + +const account = privateKeyToAccount(`0x${privateKey}`) +const accountTwo = privateKeyToAccount(`0x${privateKeyTwo}`) +const recipient = accountTwo.address + +const publicClient = createPublicClient({ + chain, + transport: http() +}) + +const main = async () => { + const nexusAccount = await toNexusAccount({ + holder: account, + chain, + transport: http(), + k1ValidatorAddress, + factoryAddress + }) + + const bicoBundler = createBicoBundlerClient({ + bundlerUrl, + account: nexusAccount, + userOperation: { + estimateFeesPerGas: async (parameters) => { + const feeData = await ( + parameters?.account?.client as PublicClient + )?.estimateFeesPerGas?.() + const gas = { + maxFeePerGas: safeMultiplier(feeData.maxFeePerGas, GAS_ESTIMATE), + maxPriorityFeePerGas: safeMultiplier( + feeData.maxPriorityFeePerGas, + GAS_ESTIMATE + ) + } + return gas + } + } + }) + + const usesAltoBundler = process.env.BUNDLER_URL?.includes("pimlico") + console.time("read methods") + const results = await Promise.allSettled([ + bicoBundler.getChainId(), + bicoBundler.getSupportedEntryPoints(), + bicoBundler.prepareUserOperation({ + sender: account.address, + nonce: 0n, + data: "0x", + signature: "0x", + verificationGasLimit: 1n, + preVerificationGas: 1n, + callData: "0x", + callGasLimit: 1n, + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + account: nexusAccount + }) + ]) + console.timeEnd("read methods") + + const successCount = results.filter((result) => result.status === "fulfilled") + console.log( + `running the ${usesAltoBundler ? "Alto" : "Bico"} bundler with ${ + successCount.length + } successful calls` + ) + + console.time("write methods") + const hash = await bicoBundler.sendUserOperation({ + calls: [ + { + to: account.address, + value: 1n + } + ], + account: nexusAccount + }) + const userOpReceipt = await bicoBundler.waitForUserOperationReceipt({ hash }) + const { transactionHash } = await publicClient.waitForTransactionReceipt({ + hash: userOpReceipt.receipt.transactionHash + }) + console.timeEnd("write methods") + console.log({ transactionHash }) +} + +main() + .then(() => { + process.exit(0) + }) + .catch((error) => { + console.error(error) + process.exitCode = 1 + }) diff --git a/src/account/BaseSmartContractAccount.ts b/src/account/BaseSmartContractAccount.ts deleted file mode 100644 index 92d774138..000000000 --- a/src/account/BaseSmartContractAccount.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { - http, - type Address, - type GetContractReturnType, - type Hash, - type Hex, - type PublicClient, - createPublicClient, - getContract, - trim -} from "viem" -import { EntrypointAbi } from "../__contracts/abi/EntryPointABI.js" -import contracts from "../__contracts/index.js" -import { Logger, type SmartAccountSigner } from "./index.js" -import type { MODE_MODULE_ENABLE, MODE_VALIDATION } from "./utils/Constants.js" -import type { - BaseSmartContractAccountProps, - BatchUserOperationCallData, - ISmartContractAccount, - Transaction -} from "./utils/Types.js" -import { wrapSignatureWith6492 } from "./utils/Utils.js" - -export enum DeploymentState { - UNDEFINED = "0x0", - NOT_DEPLOYED = "0x1", - DEPLOYED = "0x2" -} - -export abstract class BaseSmartContractAccount< - TSigner extends SmartAccountSigner = SmartAccountSigner -> implements ISmartContractAccount -{ - protected factoryAddress: Address - - protected deploymentState: DeploymentState = DeploymentState.UNDEFINED - - protected accountAddress?: Address - - protected accountInitCode?: Hex - - protected signer: TSigner - - protected entryPoint: GetContractReturnType< - typeof contracts.entryPoint.abi, - PublicClient - > - - protected entryPointAddress: Address - - public publicClient: PublicClient - - constructor(params: BaseSmartContractAccountProps) { - this.entryPointAddress = - params.entryPointAddress ?? contracts.entryPoint.address - - this.publicClient = createPublicClient({ - chain: params.chain, - transport: http(params.rpcUrl) - }) - - this.accountAddress = params.accountAddress - this.factoryAddress = params.factoryAddress - this.signer = params.signer as TSigner - this.accountInitCode = params.initCode - - this.entryPoint = getContract({ - address: this.entryPointAddress, - abi: EntrypointAbi, - client: this.publicClient as PublicClient - }) - } - - //#region abstract-methods - - /** - * This method should return a signature that will not `revert` during validation. - * It does not have to pass validation, just not cause the contract to revert. - * This is required for gas estimation so that the gas estimate are accurate. - * - */ - abstract getDummySignature(): Hash - - /** - * this method should return the abi encoded function data for a call to your contract's `execute` method - * - * @param target -- equivalent to `to` in a normal transaction - * @param value -- equivalent to `value` in a normal transaction - * @param data -- equivalent to `data` in a normal transaction - * @returns abi encoded function data for a call to your contract's `execute` method - */ - abstract encodeExecute( - transaction: Transaction, - useExecutor: boolean - ): Promise - - /** - * this should return an ERC-191 compliant message and is used to sign UO Hashes - * - * @param msg -- the message to sign - */ - abstract signMessage(msg: string | Uint8Array): Promise - - /** - * this should return the init code that will be used to create an account if one does not exist. - * This is the concatenation of the account's factory address and the abi encoded function data of the account factory's `createAccount` method. - * https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/core/SenderCreator.sol#L12 - */ - protected abstract getAccountInitCode(): Promise - - //#endregion abstract-methods - - //#region optional-methods - - /** - * If your account handles 1271 signatures of personal_sign differently - * than it does UserOperations, you can implement two different approaches to signing - * - * @param uoHash -- The hash of the UserOperation to sign - * @returns the signature of the UserOperation - */ - async signUserOperationHash(uoHash: Hash): Promise { - return this.signMessage(uoHash) - } - - /** - * If your contract supports signing and verifying typed data, - * you should implement this method. - * - * @param _params -- Typed Data params to sign - */ - abstract signTypedData(typedData: any): Promise<`0x${string}`> - - /** - * This method should wrap the result of `signMessage` as per - * [EIP-6492](https://eips.ethereum.org/EIPS/eip-6492) - * - * @param msg -- the message to sign - */ - async signMessageWith6492(msg: string | Uint8Array): Promise<`0x${string}`> { - const [isDeployed, signature] = await Promise.all([ - this.isAccountDeployed(), - this.signMessage(msg) - ]) - - return this.create6492Signature(isDeployed, signature) - } - - /** - * Similar to the signMessageWith6492 method above, - * this method should wrap the result of `signTypedData` as per - * [EIP-6492](https://eips.ethereum.org/EIPS/eip-6492) - * - * @param params -- Typed Data params to sign - */ - async signTypedDataWith6492(typedData: any): Promise<`0x${string}`> { - const [isDeployed, signature] = await Promise.all([ - this.isAccountDeployed(), - this.signTypedData(typedData) - ]) - - return this.create6492Signature(isDeployed, signature) - } - - /** - * Not all contracts support batch execution. - * If your contract does, this method should encode a list of - * transactions into the call data that will be passed to your - * contract's batch execution method. - * - * @param _txs -- the transactions to batch execute - */ - async encodeBatchExecute( - _txs: BatchUserOperationCallData - ): Promise<`0x${string}`> { - throw new Error("Batch execution not supported") - } - - /** - * If your contract supports UUPS, you can implement this method which can be - * used to upgrade the implementation of the account. - * - * @param upgradeToImplAddress -- the implementation address of the contract you want to upgrade to - * @param upgradeToInitData -- the initialization data required by that account - */ - encodeUpgradeToAndCall = async ( - _upgradeToImplAddress: Address, - _upgradeToInitData: Hex - ): Promise => { - throw new Error("Upgrade ToAndCall Not Supported") - } - //#endregion optional-methods - - // Extra implementations - abstract getNonce( - validationMode?: typeof MODE_VALIDATION | typeof MODE_MODULE_ENABLE - ): Promise - - private async _isDeployed(): Promise { - const contractCode = await this.publicClient.getBytecode({ - address: await this.getAddress() - }) - return (contractCode?.length ?? 0) > 2 - } - - async getInitCode(): Promise { - if (this.deploymentState === DeploymentState.DEPLOYED) { - return "0x" - } - - const isDeployed = await this._isDeployed() - - if (isDeployed) { - this.deploymentState = DeploymentState.DEPLOYED - return "0x" - } - - this.deploymentState = DeploymentState.NOT_DEPLOYED - - return this._getAccountInitCode() - } - - 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 - ) - - if (err.cause?.data?.errorName === "SenderAddressResult") { - this.accountAddress = err.cause.data.args[0] as Address - Logger.log( - "[BaseSmartContractAccount](getAddress) entryPoint.getSenderAddress result:", - this.accountAddress - ) - return this.accountAddress - } - - if (err.details === "Invalid URL") { - throw new Error("Invalid URL") - } - } - - throw new Error("Failed to get counterfactual account address") - } - - return this.accountAddress - } - - extend = (fn: (self: this) => R): this & R => { - const extended = fn(this) as any - // this should make it so extensions can't overwrite the base methods - for (const key in this) { - delete extended[key] - } - return Object.assign(this, extended) - } - - getSigner(): TSigner { - return this.signer - } - - getFactoryAddress(): Address { - return this.factoryAddress - } - - getEntryPointAddress(): Address { - return this.entryPointAddress - } - - async isAccountDeployed(): Promise { - return (await this.getDeploymentState()) === DeploymentState.DEPLOYED - } - - async getDeploymentState(): Promise { - if (this.deploymentState === DeploymentState.UNDEFINED) { - const initCode = await this.getInitCode() - return initCode === "0x" - ? DeploymentState.DEPLOYED - : DeploymentState.NOT_DEPLOYED - } - if (this.deploymentState === DeploymentState.NOT_DEPLOYED) { - if (await this._isDeployed()) { - this.deploymentState = DeploymentState.DEPLOYED - } - } - return this.deploymentState - } - /** - * https://eips.ethereum.org/EIPS/eip-4337#first-time-account-creation - * The initCode field (if non-zero length) is parsed as a 20-byte address, - * followed by calldata to pass to this address. - * The factory address is the first 40 char after the 0x, and the callData is the rest. - */ - protected async parseFactoryAddressFromAccountInitCode(): Promise< - [Address, Hex] - > { - const initCode = await this._getAccountInitCode() - const factoryAddress = `0x${initCode.substring(2, 42)}` as Address - const factoryCalldata = `0x${initCode.substring(42)}` as Hex - return [factoryAddress, factoryCalldata] - } - - protected async getImplementationAddress(): Promise<"0x0" | Address> { - const accountAddress = await this.getAddress() - - const storage = await this.publicClient.getStorageAt({ - address: accountAddress, - // This is the default slot for the implementation address for Proxies - slot: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" - }) - - if (storage == null) { - throw new Error( - "Failed to get storage slot 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" - ) - } - - return trim(storage) - } - - private async _getAccountInitCode(): Promise { - return this.accountInitCode ?? this.getAccountInitCode() - } - - private async create6492Signature( - isDeployed: boolean, - signature: Hash - ): Promise { - if (isDeployed) { - return signature - } - - const [factoryAddress, factoryCalldata] = - await this.parseFactoryAddressFromAccountInitCode() - - Logger.log( - `[BaseSmartContractAccount](create6492Signature)\ - factoryAddress: ${factoryAddress}, factoryCalldata: ${factoryCalldata}` - ) - - return wrapSignatureWith6492({ - factoryAddress, - factoryCalldata, - signature - }) - } -} diff --git a/src/account/NexusSmartAccount.ts b/src/account/NexusSmartAccount.ts deleted file mode 100644 index 9d3ad3ff2..000000000 --- a/src/account/NexusSmartAccount.ts +++ /dev/null @@ -1,2174 +0,0 @@ -import { - type AbiParameter, - type Address, - type GetContractReturnType, - type Hash, - type Hex, - type PublicClient, - type TypedDataDefinition, - type WalletClient, - concat, - concatHex, - decodeFunctionData, - domainSeparator, - encodeAbiParameters, - encodeFunctionData, - encodePacked, - formatUnits, - getAddress, - getContract, - getTypesForEIP712Domain, - keccak256, - pad, - parseAbi, - parseAbiParameters, - toBytes, - toHex, - validateTypedData -} from "viem" -import contracts from "../__contracts" -import { NexusAbi } from "../__contracts/abi" -import { - Bundler, - type GetUserOperationGasPriceReturnType, - type UserOpReceipt, - type UserOpResponse -} from "../bundler/index.js" -import type { IBundler } from "../bundler/interfaces/IBundler.js" -import { EXECUTE_BATCH, EXECUTE_SINGLE } from "../bundler/utils/Constants.js" -import type { BaseExecutionModule } from "../modules/base/BaseExecutionModule.js" -import { BaseValidationModule } from "../modules/base/BaseValidationModule.js" -import { - type Execution, - type Module, - type ModuleInfo, - type SendUserOpParams, - createK1ValidatorModule, - createValidationModule, - moduleTypeIds -} from "../modules/index.js" -import type { K1ValidatorModule } from "../modules/validators/K1ValidatorModule.js" -import { - type FeeQuotesOrDataDto, - type FeeQuotesOrDataResponse, - type IHybridPaymaster, - type IPaymaster, - Paymaster, - type SponsorUserOperationDto -} from "../paymaster/index.js" -import { - BaseSmartContractAccount, - DeploymentState -} from "./BaseSmartContractAccount.js" -import { - Logger, - type SmartAccountSigner, - type StateOverrideSet, - type UserOperationStruct, - convertSigner -} from "./index.js" -import { - GENERIC_FALLBACK_SELECTOR, - type MODE_MODULE_ENABLE, - MODE_VALIDATION, - PARENT_TYPEHASH -} from "./utils/Constants.js" -import { - ADDRESS_ZERO, - ERC20_ABI, - ERROR_MESSAGES, - MAGIC_BYTES, - NATIVE_TOKEN_ALIAS, - SENTINEL_ADDRESS -} from "./utils/Constants.js" -import type { - BalancePayload, - BiconomyTokenPaymasterRequest, - BigNumberish, - BuildUserOpOptions, - CounterFactualAddressParam, - NexusSmartAccountConfig, - NexusSmartAccountConfigConstructorProps, - NonceOptions, - PaymasterUserOperationDto, - SupportedToken, - Transaction, - TransferOwnershipCompatibleModule, - WithdrawalRequest -} from "./utils/Types.js" -import { eip712WrapHash, typeToString } from "./utils/Utils.js" -import { getAccountDomainStructFields } from "./utils/Utils.js" -import { addressEquals, isNullOrUndefined, packUserOp } from "./utils/Utils.js" - -export class NexusSmartAccount extends BaseSmartContractAccount { - private index: bigint - - private chainId: number - - paymaster?: IPaymaster - - bundler?: IBundler - - private accountContract?: GetContractReturnType< - typeof NexusAbi, - PublicClient | WalletClient - > - - // private scanForUpgradedAccountsFromV1!: boolean - - // private maxIndexForScan!: bigint - - // Validation module responsible for account deployment initCode. This acts as a default authorization module. - defaultValidationModule!: BaseValidationModule - - // Deployed Smart Account can have more than one module enabled. When sending a transaction activeValidationModule is used to prepare and validate userOp signature. - activeValidationModule!: BaseValidationModule - - installedExecutors: BaseExecutionModule[] = [] - activeExecutionModule?: BaseExecutionModule - k1ValidatorAddress: Address - - private constructor( - readonly nexusSmartAccountConfig: NexusSmartAccountConfigConstructorProps - ) { - const resolvedEntryPointAddress = - (nexusSmartAccountConfig.entryPointAddress as Hex) ?? - contracts.entryPoint.address - - super({ - ...nexusSmartAccountConfig, - entryPointAddress: resolvedEntryPointAddress, - accountAddress: - (nexusSmartAccountConfig.accountAddress as Hex) ?? undefined, - factoryAddress: nexusSmartAccountConfig.factoryAddress - }) - - this.k1ValidatorAddress = nexusSmartAccountConfig.k1ValidatorAddress - const chain = nexusSmartAccountConfig.chain - - this.defaultValidationModule = - nexusSmartAccountConfig.defaultValidationModule - this.activeValidationModule = nexusSmartAccountConfig.activeValidationModule - - this.index = nexusSmartAccountConfig.index ?? 0n - this.chainId = chain.id - this.bundler = nexusSmartAccountConfig.bundler - - if (nexusSmartAccountConfig.paymasterUrl) { - this.paymaster = new Paymaster({ - paymasterUrl: nexusSmartAccountConfig.paymasterUrl - }) - } else { - this.paymaster = nexusSmartAccountConfig.paymaster - } - - this.bundler = nexusSmartAccountConfig.bundler - - // Added bang operator to avoid null check as the constructor have these params as optional - this.defaultValidationModule = - // biome-ignore lint/style/noNonNullAssertion: - nexusSmartAccountConfig.defaultValidationModule! - this.activeValidationModule = - // biome-ignore lint/style/noNonNullAssertion: - nexusSmartAccountConfig.activeValidationModule! - } - - /** - * Creates a new instance of NexusSmartAccount - * - * This method will create a NexusSmartAccount instance but will not deploy the Smart Account - * Deployment of the Smart Account will be donewith the first user operation. - * - * - Docs: https://docs.biconomy.io/account/integration#integration-1 - * - * @param nexusSmartAccountConfig - Configuration for initializing the NexusSmartAccount instance {@link NexusSmartAccountConfig}. - * @returns A promise that resolves to a new instance of NexusSmartAccount. - * @throws An error if something is wrong with the smart account instance creation. - * - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient, NexusSmartAccount } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonAmoy } from "viem/chains"; - * - * const signer = createWalletClient({ - * account, - * chain: polygonAmoy, - * transport: http(), - * }); - * - * const bundlerUrl = "" // Retrieve bundler url from dashboard - * - * const smartAccountFromStaticCreate = await NexusSmartAccount.create({ signer, bundlerUrl }); - * - * // Is the same as... - * - * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl }); - * - */ - public static async create( - nexusSmartAccountConfig: NexusSmartAccountConfig - ): Promise { - let resolvedSmartAccountSigner!: SmartAccountSigner - const defaultedRpcUrl = - nexusSmartAccountConfig.rpcUrl ?? - nexusSmartAccountConfig.chain.rpcUrls.default.http[0] - - const defaultedEntryPointAddress = - (nexusSmartAccountConfig.entryPointAddress ?? - contracts.entryPoint.address) as Hex - - // Signer needs to be initialised here before defaultValidationModule is set - if (nexusSmartAccountConfig.signer) { - const signerResult = await convertSigner( - nexusSmartAccountConfig.signer, - nexusSmartAccountConfig.rpcUrl - ) - resolvedSmartAccountSigner = signerResult.signer - } - - const bundler: IBundler = - nexusSmartAccountConfig.bundler ?? - new Bundler({ - // biome-ignore lint/style/noNonNullAssertion: always required - bundlerUrl: nexusSmartAccountConfig.bundlerUrl!, - chain: nexusSmartAccountConfig.chain - }) - - let defaultValidationModule = - nexusSmartAccountConfig.defaultValidationModule - - const k1ValidatorAddress = - nexusSmartAccountConfig.k1ValidatorAddress ?? - (contracts.k1Validator.address as Hex) - const factoryAddress = - nexusSmartAccountConfig.factoryAddress ?? - (contracts.k1ValidatorFactory.address as Hex) - - if (!defaultValidationModule) { - const newModule = await createK1ValidatorModule( - resolvedSmartAccountSigner, - k1ValidatorAddress - ) - defaultValidationModule = newModule as K1ValidatorModule - } - const activeValidationModule = - nexusSmartAccountConfig?.activeValidationModule ?? defaultValidationModule - if (!resolvedSmartAccountSigner) { - resolvedSmartAccountSigner = activeValidationModule.getSigner() - } - if (!resolvedSmartAccountSigner) { - throw new Error("signer required") - } - - const config: NexusSmartAccountConfigConstructorProps = { - ...nexusSmartAccountConfig, - factoryAddress, - k1ValidatorAddress, - defaultValidationModule, - activeValidationModule, - rpcUrl: defaultedRpcUrl, - bundler, - signer: resolvedSmartAccountSigner, - entryPointAddress: defaultedEntryPointAddress - } - - const smartAccount = new NexusSmartAccount(config) - await smartAccount.getDeploymentState() - return smartAccount - } - - override async getAddress(params?: CounterFactualAddressParam): Promise { - if (isNullOrUndefined(this.accountAddress)) { - const signerAddress = await this.signer.getAddress() - const index = params?.index ?? this.index - this.accountAddress = (await this.publicClient.readContract({ - address: this.factoryAddress, - abi: contracts.k1ValidatorFactory.abi, - functionName: "computeAccountAddress", - args: [signerAddress, index, [], 0] - })) as Hex - } - return this.accountAddress - } - public getAccountAddress = this.getAddress - - /** - * Returns an upper estimate for the gas spent on a specific user operation - * - * This method will fetch an approximate gas estimate for the user operation, given the current state of the network. - * It is regularly an overestimate, and the actual gas spent will likely be lower. - * It is unlikely to be an underestimate unless the network conditions rapidly change. - * - * @param transactions Array of {@link Transaction} to be sent. - * @param buildUseropDto {@link BuildUserOpOptions}. - * @returns Promise - The estimated gas cost in wei. - * - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonAmoy } from "viem/chains"; - * - * const signer = createWalletClient({ - * account, - * chain: polygonAmoy, - * transport: http(), - * }); - * - * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl, paymasterUrl }); // Retrieve bundler/paymaster url from dashboard - * const encodedCall = encodeFunctionData({ - * abi: CounterAbi, - * functionName: "incrementNumber", - * args: ["0x..."], - * }); - * - * const tx = { - * to: mockAddresses.Counter, - * data: encodedCall - * } - * - * const amountInWei = await smartAccount.getGasEstimates([tx, tx], { - * paymasterServiceData: { - * mode: "SPONSORED", - * }, - * }); - * - * console.log(amountInWei.toString()); - * - */ - public async getGasEstimate( - transactions: Transaction[], - buildUseropDto?: BuildUserOpOptions - ): Promise { - const { - callGasLimit, - preVerificationGas, - verificationGasLimit, - maxFeePerGas - } = await this.buildUserOp(transactions, buildUseropDto) - - const _callGasLimit = BigInt(callGasLimit || 0) - const _preVerificationGas = BigInt(preVerificationGas || 0) - const _verificationGasLimit = BigInt(verificationGasLimit || 0) - const _maxFeePerGas = BigInt(maxFeePerGas || 0) - - if (!buildUseropDto?.paymasterServiceData?.mode) { - return ( - (_callGasLimit + _preVerificationGas + _verificationGasLimit) * - _maxFeePerGas - ) - } - return ( - (_callGasLimit + - BigInt(3) * _verificationGasLimit + - _preVerificationGas) * - _maxFeePerGas - ) - } - - /** - * Returns balances for the smartAccount instance. - * - * This method will fetch tokens info given an array of token addresses for the smartAccount instance. - * The balance of the native token will always be returned as the last element in the reponse array, with the address set to 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE. - * - * @param addresses - Optional. Array of asset addresses to fetch the balances of. If not provided, the method will return only the balance of the native token. - * @returns Promise> - An array of token balances (plus the native token balance) of the smartAccount instance. - * - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonAmoy } from "viem/chains"; - * - * const signer = createWalletClient({ - * account, - * chain: polygonAmoy, - * transport: http(), - * }); - * - * const token = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a"; - * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl }); - * const [tokenBalanceFromSmartAccount, nativeTokenBalanceFromSmartAccount] = await smartAccount.getBalances([token]); - * - * console.log(tokenBalanceFromSmartAccount); - * // { - * // amount: 1000000000000000n, - * // decimals: 6, - * // address: "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a", - * // formattedAmount: "1000000", - * // chainId: 11155111 - * // } - * - * // or to get the nativeToken balance - * - * const [nativeTokenBalanceFromSmartAccount] = await smartAccount.getBalances(); - * - * console.log(nativeTokenBalanceFromSmartAccount); - * // { - * // amount: 1000000000000000n, - * // decimals: 18, - * // address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", - * // formattedAmount: "1", - * // chainId: 11155111 - * // } - * - */ - public async getBalances( - addresses?: Array - ): Promise> { - const accountAddress = await this.getAddress() - const result: BalancePayload[] = [] - - if (addresses) { - const tokenContracts = addresses.map((address) => - getContract({ - address, - abi: parseAbi(ERC20_ABI), - client: this.publicClient - }) - ) - - const balancePromises = tokenContracts.map((tokenContract) => - tokenContract.read.balanceOf([accountAddress]) - ) as Promise[] - const decimalsPromises = tokenContracts.map((tokenContract) => - tokenContract.read.decimals() - ) as Promise[] - const [balances, decimalsPerToken] = await Promise.all([ - Promise.all(balancePromises), - Promise.all(decimalsPromises) - ]) - - balances.forEach((amount, index) => - result.push({ - amount, - decimals: decimalsPerToken[index], - address: addresses[index], - formattedAmount: formatUnits(amount, decimalsPerToken[index]), - chainId: this.chainId - }) - ) - } - - const balance = await this.publicClient.getBalance({ - address: accountAddress - }) - - result.push({ - amount: balance, - decimals: 18, - address: NATIVE_TOKEN_ALIAS, - formattedAmount: formatUnits(balance, 18), - chainId: this.chainId - }) - - return result - } - - /** - * Transfers funds from Smart Account to recipient (usually EOA) - * @param recipient - Address of the recipient - * @param withdrawalRequests - Array of withdrawal requests {@link WithdrawalRequest}. If withdrawal request is an empty array, it will transfer the balance of the native token. Using a paymaster will ensure no dust remains in the smart account. - * @param buildUseropDto - Optional. {@link BuildUserOpOptions} - * - * @returns Promise - An object containing the status of the transaction. - * - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient, NATIVE_TOKEN_ALIAS } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonMumbai } from "viem/chains"; - * - * const token = "0x747A4168DB14F57871fa8cda8B5455D8C2a8e90a"; - * const signer = createWalletClient({ - * account, - * chain: polygonMumbai, - * transport: http(), - * }); - * - * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl, biconomyPaymasterApiKey }); - * - * const { wait } = await smartAccount.withdraw( - * [ - * { address: token }, // omit the amount to withdraw the full balance - * { address: NATIVE_TOKEN_ALIAS, amount: BigInt(1) } - * ], - * account.address, // Default recipient used if no recipient is present in the withdrawal request - * { - * paymasterServiceData: { mode: "SPONSORED" }, - * } - * ); - * - * // OR to withdraw all of the native token, leaving no dust in the smart account - * - * const { wait } = await smartAccount.withdraw([], account.address, { - * paymasterServiceData: { mode: "SPONSORED" }, - * }); - * - * const { success } = await wait(); - */ - public async withdraw( - withdrawalRequests?: WithdrawalRequest[] | null, - defaultRecipient?: Hex | null, - buildUseropDto?: BuildUserOpOptions - ): Promise { - const accountAddress = this.accountAddress ?? (await this.getAddress()) - - if ( - !defaultRecipient && - withdrawalRequests?.some(({ recipient }) => !recipient) - ) { - throw new Error(ERROR_MESSAGES.NO_RECIPIENT) - } - - // Remove the native token from the withdrawal requests - let tokenRequests = - withdrawalRequests?.filter( - ({ address }) => !addressEquals(address, NATIVE_TOKEN_ALIAS) - ) ?? [] - - // Check if the amount is not present in all withdrawal requests - const shouldFetchMaxBalances = tokenRequests.some(({ amount }) => !amount) - - // Get the balances of the tokens if the amount is not present in the withdrawal requests - if (shouldFetchMaxBalances) { - const balances = await this.getBalances( - tokenRequests.map(({ address }) => address) - ) - tokenRequests = tokenRequests.map(({ amount, address }, i) => ({ - address, - amount: amount ?? balances[i].amount - })) - } - - // Create the transactions - const txs: Transaction[] = tokenRequests.map( - ({ address, amount, recipient: recipientFromRequest }) => ({ - to: address, - data: encodeFunctionData({ - abi: parseAbi(ERC20_ABI), - functionName: "transfer", - args: [recipientFromRequest || defaultRecipient, amount] - }) - }) - ) - - // Check if eth alias is present in the original withdrawal requests - const nativeTokenRequest = withdrawalRequests?.find(({ address }) => - addressEquals(address, NATIVE_TOKEN_ALIAS) - ) - const hasNoRequests = !withdrawalRequests?.length - if (!!nativeTokenRequest || hasNoRequests) { - // Check that an amount is present in the withdrawal request, if no paymaster service data is present, as max amounts cannot be calculated without a paymaster. - if ( - !nativeTokenRequest?.amount && - !buildUseropDto?.paymasterServiceData?.mode - ) { - throw new Error(ERROR_MESSAGES.NATIVE_TOKEN_WITHDRAWAL_WITHOUT_AMOUNT) - } - - // get eth balance if not present in withdrawal requests - const nativeTokenAmountToWithdraw = - nativeTokenRequest?.amount ?? - (await this.publicClient.getBalance({ address: accountAddress })) - - txs.push({ - to: (nativeTokenRequest?.recipient ?? defaultRecipient) as Hex, - value: nativeTokenAmountToWithdraw - }) - } - - return this.sendTransaction(txs, buildUseropDto) - } - - async _getAccountContract(): Promise< - GetContractReturnType - > { - if (await this.isAccountDeployed()) { - if (!this.accountContract) { - this.accountContract = getContract({ - address: await this.getAddress(), - abi: NexusAbi, - client: this.publicClient as PublicClient - }) - } - return this.accountContract - } - throw new Error(ERROR_MESSAGES.ACCOUNT_NOT_DEPLOYED) - } - - isActiveValidationModuleDefined(): boolean { - if (!this.activeValidationModule) - throw new Error("Must provide an instance of active validation module.") - return true - } - - isDefaultValidationModuleDefined(): boolean { - if (!this.defaultValidationModule) - throw new Error("Must provide an instance of default validation module.") - return true - } - - /** - * Sets the active validation module on the NexusSmartAccount instance - * @param validationModule - BaseValidationModule instance - * - * @returns Promise - The BaseValidationModule instance. - */ - setActiveValidationModule( - validationModule: BaseValidationModule - ): BaseValidationModule { - if (validationModule instanceof BaseValidationModule) { - this.activeValidationModule = validationModule - } - return this.activeValidationModule - } - - /** - * Sets the active validation module on the NexusSmartAccount instance - * @param validationModuleAddress - Address of the validation module - * @param data - Initialization data for the validation module - * - * @returns Promise - The BaseValidationModule instance. - */ - async setActiveValidationModuleByAddress({ - validationModuleAddress, - data - }: { - validationModuleAddress: Address - data: Hex - }): Promise { - if (validationModuleAddress) { - this.activeValidationModule = await createValidationModule( - this.signer, - validationModuleAddress, - data - ) - return this.activeValidationModule - } - throw new Error("Validation module address is required") - } - - /** - * Sets the active executor module on the NexusSmartAccount instance - * @param executorModule - Address of the executor module - * @param data - Initialization data for the executor module - * - * @returns Promise - The BaseExecutionModule instance. - */ - setActiveExecutionModule( - executorModule: BaseExecutionModule - ): BaseExecutionModule { - this.activeExecutionModule = executorModule - return this.activeExecutionModule - } - - setDefaultValidationModule( - validationModule: BaseValidationModule - ): NexusSmartAccount { - if (validationModule instanceof BaseValidationModule) { - this.defaultValidationModule = validationModule - } - return this - } - - // async getV1AccountsUpgradedToV2( - // params: QueryParamsForAddressResolver - // ): Promise { - // const maxIndexForScan = params.maxIndexForScan ?? this.maxIndexForScan - - // const addressResolver = getContract({ - // address: ADDRESS_RESOLVER_ADDRESS, - // abi: AccountResolverAbi, - // client: { - // public: this.publicClient as PublicClient - // } - // }) - // // Note: depending on moduleAddress and moduleSetupData passed call this. otherwise could call resolveAddresses() - - // if (params.moduleAddress && params.moduleSetupData) { - // const result = await addressResolver.read.resolveAddressesFlexibleForV2([ - // params.eoaAddress, - // Number.parseInt(maxIndexForScan.toString()), // TODO: SHOULD BE A BIGINT BUT REQUIRED TO BE A NUMBER FOR THE CONTRACT - // params.moduleAddress, - // params.moduleSetupData - // ]) - - // const desiredV1Account = result.find( - // (smartAccountInfo: { - // factoryVersion: string - // currentVersion: string - // deploymentIndex: { toString: () => string } - // }) => - // smartAccountInfo.factoryVersion === "v1" && - // smartAccountInfo.currentVersion === "2.0.0" && - // smartAccountInfo.deploymentIndex === params.index - // ) - - // if (desiredV1Account) { - // const smartAccountAddress = desiredV1Account.accountAddress - // return smartAccountAddress - // } - // return ADDRESS_ZERO - // } - // return ADDRESS_ZERO - // } - - /** - * 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 - */ - public override async getAccountInitCode(): Promise { - this.isDefaultValidationModuleDefined() - - if (await this.isAccountDeployed()) return "0x" - - const factoryData = (await this.getFactoryData()) as Hex - - return concatHex([this.factoryAddress, factoryData]) - } - - /** - * - * @param to { target } address of transaction - * @param value represents amount of native tokens - * @param data represent data associated with transaction - * @returns encoded data for execute function - */ - async encodeExecute(transaction: Transaction): Promise { - const mode = EXECUTE_SINGLE - const executionCalldata = encodePacked( - ["address", "uint256", "bytes"], - [ - transaction.to as Hex, - BigInt(transaction.value ?? 0n), - (transaction.data as Hex) ?? ("0x" as Hex) - ] - ) - return encodeFunctionData({ - abi: parseAbi([ - "function execute(bytes32 mode, bytes calldata executionCalldata) external" - ]), - functionName: "execute", - args: [mode, executionCalldata] - }) - } - - /** - * - * @param to { target } array of addresses in transaction - * @param value represents array of amount of native tokens associated with each transaction - * @param data represent array of data associated with each transaction - * @returns encoded data for executeBatch function - */ - async encodeExecuteBatch(transactions: Transaction[]): Promise { - const executionAbiParams: AbiParameter = { - type: "tuple[]", - components: [ - { name: "target", type: "address" }, - { name: "value", type: "uint256" }, - { name: "callData", type: "bytes" } - ] - } - - const executions = transactions.map((tx) => ({ - target: tx.to, - callData: tx.data ?? "0x", - value: BigInt(tx.value ?? 0n) - })) - - const executionCalldataPrep = encodeAbiParameters( - [executionAbiParams], - [executions] - ) - - return encodeFunctionData({ - abi: parseAbi([ - "function execute(bytes32 mode, bytes calldata executionCalldata) external" - ]), - functionName: "execute", - args: [EXECUTE_BATCH, executionCalldataPrep] - }) - } - - // dummy signature depends on the validation module supplied. - async getDummySignatures(): Promise { - // const params = { ...(this.sessionData ? this.sessionData : {}), ..._params } - this.isActiveValidationModuleDefined() - return this.activeValidationModule.getDummySignature() as Hex - } - - // TODO: review this - getDummySignature(): Hex { - throw new Error("Method not implemented! Call getDummySignatures instead.") - } - - // Might use provided paymaster instance to get dummy data (from pm service) - getDummyPaymasterData(): string { - return "0x" - } - - validateUserOp( - // userOp: Partial, - // requiredFields: UserOperationKey[] - ): boolean { - // console.log(userOp, "userOp"); - // for (const field of requiredFields) { - // if (isNullOrUndefined(userOp[field])) { - // throw new Error(`${String(field)} is missing in the UserOp`) - // } - // } - return true - } - - async signUserOp( - userOp: Partial - ): Promise { - this.isActiveValidationModuleDefined() - // TODO REMOVE COMMENT AND CHECK FOR PIMLICO USER OP FIELDS - // const requiredFields: UserOperationKey[] = [ - // "sender", - // "nonce", - // "callGasLimit", - // "signature", - // "maxFeePerGas", - // "maxPriorityFeePerGas", - // ] - // this.validateUserOp(userOp, requiredFields) - const userOpHash = await this.getUserOpHash(userOp) - - const eoaSignature = (await this.activeValidationModule.signUserOpHash( - userOpHash - )) as Hex - - userOp.signature = eoaSignature - return userOp as UserOperationStruct - } - - getSignatureWithModuleAddress( - moduleSignature: Hex, - moduleAddress?: Hex - ): Hex { - const moduleAddressToUse = - moduleAddress ?? (this.activeValidationModule.getAddress() as Hex) - return encodePacked( - ["address", "bytes"], - [moduleAddressToUse, moduleSignature] - ) - } - - private async getPaymasterFeeQuotesOrData( - userOp: Partial, - feeQuotesOrData: FeeQuotesOrDataDto - ): Promise { - const paymaster = this - .paymaster as IHybridPaymaster - const tokenList = feeQuotesOrData?.preferredToken - ? [feeQuotesOrData?.preferredToken] - : feeQuotesOrData?.tokenList?.length - ? feeQuotesOrData?.tokenList - : [] - return paymaster.getPaymasterFeeQuotesOrData(userOp, { - ...feeQuotesOrData, - tokenList - }) - } - - /** - * - * @description This function will retrieve fees from the paymaster in erc20 mode - * - * @param manyOrOneTransactions Array of {@link Transaction} to be batched and sent. Can also be a single {@link Transaction}. - * @param buildUseropDto {@link BuildUserOpOptions}. - * @returns Promise - * - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonAmoy } from "viem/chains"; - * - * const signer = createWalletClient({ - * account, - * chain: polygonAmoy, - * transport: http(), - * }); - * - * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl }); // Retrieve bundler url from dashboard - * const encodedCall = encodeFunctionData({ - * abi: CounterAbi, - * functionName: "incrementNumber", - * args: ["0x..."], - * }); - * - * const transaction = { - * to: mockAddresses.Counter, - * data: encodedCall - * } - * - * const feeQuotesResponse: FeeQuotesOrDataResponse = await smartAccount.getTokenFees(transaction, { paymasterServiceData: { mode: "ERC20" } }); - * - * const userSeletedFeeQuote = feeQuotesResponse.feeQuotes?.[0]; - * - * const { wait } = await smartAccount.sendTransaction(transaction, { - * paymasterServiceData: { - * mode: "ERC20", - * feeQuote: userSeletedFeeQuote, - * spender: feeQuotesResponse.tokenPaymasterAddress, - * }, - * }); - * - * const { success, receipt } = await wait(); - * - */ - public async getTokenFees( - manyOrOneTransactions: Transaction | Transaction[], - buildUseropDto: BuildUserOpOptions - ): Promise { - const txs = Array.isArray(manyOrOneTransactions) - ? manyOrOneTransactions - : [manyOrOneTransactions] - const userOp = await this.buildUserOp(txs, buildUseropDto) - if (!buildUseropDto.paymasterServiceData) - throw new Error("paymasterServiceData was not provided") - return this.getPaymasterFeeQuotesOrData( - userOp, - buildUseropDto.paymasterServiceData - ) - } - - /** - * - * @description This function will return an array of supported tokens from the erc20 paymaster associated with the Smart Account - * @returns Promise<{@link SupportedToken}> - * - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonAmoy } from "viem/chains"; - * - * const signer = createWalletClient({ - * account, - * chain: polygonAmoy, - * transport: http(), - * }); - * - * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl, biconomyPaymasterApiKey }); // Retrieve bundler url from dashboard - * const tokens = await smartAccount.getSupportedTokens(); - * - * // [ - * // { - * // symbol: "USDC", - * // tokenAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", - * // decimal: 6, - * // logoUrl: "https://assets.coingecko.com/coins/images/279/large/usd-coin.png?1595353707", - * // premiumPercentage: 0.1, - * // } - * // ] - * - */ - public async getSupportedTokens(): Promise { - const feeQuotesResponse = await this.getTokenFees( - { - data: "0x", - value: BigInt(0), - to: await this.getAddress() - }, - { - paymasterServiceData: { mode: "ERC20" } - } - ) - - return await Promise.all( - (feeQuotesResponse?.feeQuotes ?? []).map(async (quote) => { - const [tokenBalance] = await this.getBalances([ - quote.tokenAddress as Hex - ]) - return { - symbol: quote.symbol, - tokenAddress: quote.tokenAddress, - decimal: quote.decimal, - logoUrl: quote.logoUrl, - premiumPercentage: quote.premiumPercentage, - balance: tokenBalance - } - }) - ) - } - - /** - * - * @param userOp - * @param params - * @description This function will take a user op as an input, sign it with the owner key, and send it to the bundler. - * @returns Promise - * Sends a user operation - * - * - Docs: https://docs.biconomy.io/account/methods#senduserop- - * - * @param userOp Partial<{@link UserOperationStruct}> the userOp params to be sent. - * @param params {@link SendUserOpParams}. - * @returns Promise<{@link Hash}> that you can use to track the user operation. - * - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonAmoy } from "viem/chains"; - * - * const signer = createWalletClient({ - * account, - * chain: polygonAmoy, - * transport: http(), - * }); - * - * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl }); // Retrieve bundler url from dashboard - * const encodedCall = encodeFunctionData({ - * abi: CounterAbi, - * functionName: "incrementNumber", - * args: ["0x..."], - * }); - * - * const transaction = { - * to: mockAddresses.Counter, - * data: encodedCall - * } - * - * const userOp = await smartAccount.buildUserOp([transaction]); - * - * const { wait } = await smartAccount.sendUserOp(userOp); - * const { success, receipt } = await wait(); - * - */ - async sendUserOp({ - signature, - ...userOpWithoutSignature - }: Partial): Promise { - const userOperation = await this.signUserOp(userOpWithoutSignature) - return await this.sendSignedUserOp(userOperation) - } - - /** - * - * @param userOp - The signed user operation to send - * @param simulationType - The type of simulation to perform ("validation" | "validation_and_execution") - * @description This function call will take 'signedUserOp' as input and send it to the bundler - * @returns - */ - async sendSignedUserOp(userOp: UserOperationStruct): Promise { - // TODO REMOVE COMMENT AND CHECK FOR PIMLICO USER OP FIELDS - // const requiredFields: UserOperationKey[] = [ - // "sender", - // "nonce", - // "verificationGasLimit", - // "preVerificationGas", - // "maxFeePerGas", - // "maxPriorityFeePerGas", - // "signature", - // ] - // this.validateUserOp(userOp, requiredFields) - if (!this.bundler) throw new Error("Bundler is not provided") - return await this.bundler.sendUserOp(userOp) - } - - /** - * Packs gas values into the format required by PackedUserOperation. - * @param callGasLimit Call gas limit. - * @param verificationGasLimit Verification gas limit. - * @param maxFeePerGas Maximum fee per gas. - * @param maxPriorityFeePerGas Maximum priority fee per gas. - * @returns An object containing packed gasFees and accountGasLimits. - */ - public packGasValues( - callGasLimit: BigNumberish, - verificationGasLimit: BigNumberish, - maxFeePerGas: BigNumberish, - maxPriorityFeePerGas: BigNumberish - ) { - const gasFees = concat([ - pad(toHex(maxPriorityFeePerGas ?? 0n), { - size: 16 - }), - pad(toHex(maxFeePerGas ?? 0n), { size: 16 }) - ]) - const accountGasLimits = concat([ - pad(toHex(verificationGasLimit ?? 0n), { - size: 16 - }), - pad(toHex(callGasLimit ?? 0n), { size: 16 }) - ]) - - return { gasFees, accountGasLimits } - } - - async getUserOpHash(userOp: Partial): Promise { - const packedUserOp = packUserOp(userOp) - const userOpHash = keccak256(packedUserOp as Hex) - const enc = encodeAbiParameters( - parseAbiParameters("bytes32, address, uint256"), - [userOpHash, this.entryPoint.address, BigInt(this.chainId)] - ) - return keccak256(enc) - } - - async estimateUserOpGas( - userOp: Partial, - stateOverrideSet?: StateOverrideSet - ): Promise> { - if (!this.bundler) throw new Error("Bundler is not provided") - const finalUserOp = userOp - - if (!userOp.maxFeePerGas && !userOp.maxPriorityFeePerGas) { - const feeData = await this.publicClient.estimateFeesPerGas() - if (feeData.maxFeePerGas?.toString()) { - finalUserOp.maxFeePerGas = feeData.maxFeePerGas - } else if (feeData.gasPrice?.toString()) { - finalUserOp.maxFeePerGas = feeData.gasPrice - } else { - finalUserOp.maxFeePerGas = await this.publicClient.getGasPrice() - } - - if (feeData.maxPriorityFeePerGas?.toString()) { - finalUserOp.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas - } else if (feeData.gasPrice?.toString()) { - finalUserOp.maxPriorityFeePerGas = feeData.gasPrice ?? 0n - } else { - finalUserOp.maxPriorityFeePerGas = await this.publicClient.getGasPrice() - } - } - - const { callGasLimit, verificationGasLimit, preVerificationGas } = - await this.bundler.estimateUserOpGas(userOp, stateOverrideSet) - finalUserOp.verificationGasLimit = - verificationGasLimit ?? userOp.verificationGasLimit - finalUserOp.callGasLimit = callGasLimit ?? userOp.callGasLimit - finalUserOp.preVerificationGas = - preVerificationGas ?? userOp.preVerificationGas - - return finalUserOp - } - - override async getNonce( - validationMode?: typeof MODE_VALIDATION | typeof MODE_MODULE_ENABLE - ): Promise { - try { - const vm = - this.activeValidationModule.moduleAddress ?? - this.defaultValidationModule.moduleAddress - const key = concat(["0x000000", validationMode ?? MODE_VALIDATION, vm]) - const accountAddress = await this.getAddress() - return (await this.entryPoint.read.getNonce([ - accountAddress, - BigInt(key) - ])) as bigint - } catch (e) { - return BigInt(0) - } - } - - private async getBuildUserOpNonce( - nonceOptions: NonceOptions | undefined - ): Promise { - let nonce = BigInt(0) - try { - if (nonceOptions?.nonceOverride) { - nonce = BigInt(nonceOptions?.nonceOverride) - } else { - nonce = await this.getNonce(nonceOptions?.validationMode) - } - } catch (error) { - // Not throwing this error as nonce would be 0 if this.getNonce() throw exception, which is expected flow for undeployed account - Logger.warn( - "Error while getting nonce for the account. This is expected for undeployed accounts set nonce to 0" - ) - } - return nonce - } - - /** - * Transfers ownership of the smart account to a new owner. - * @param newOwner The address of the new owner. - * @param moduleAddress {@link TransferOwnershipCompatibleModule} The address of the validation module (ECDSA Ownership Module or Multichain Validation Module). - * @param buildUseropDto {@link BuildUserOpOptions}. Optional parameter - * @returns A Promise that resolves to a Hash or rejects with an Error. - * @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, - chain: baseSepolia, - transport: http() - }); - - let smartAccount = await createSmartAccountClient({ - signer: walletClient, - paymasterUrl: "https://paymaster.biconomy.io/api/v1/...", - bundlerUrl: `https://bundler.biconomy.io/api/v2/84532/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44`, - chainId: 84532 - }); - const response = await smartAccount.transferOwnership(newOwner, DEFAULT_ECDSA_OWNERSHIP_MODULE, {paymasterServiceData: {mode: "SPONSORED"}}); - - walletClient = createWalletClient({ - newOwnerAccount, - chain: baseSepolia, - transport: http() - }) - - smartAccount = await createSmartAccountClient({ - signer: walletClient, - paymasterUrl: "https://paymaster.biconomy.io/api/v1/...", - bundlerUrl: `https://bundler.biconomy.io/api/v2/84532/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44`, - chainId: 84532, - accountAddress: await smartAccount.getAddress() - }) - * ``` - */ - async transferOwnership( - newOwner: Address, - moduleAddress: TransferOwnershipCompatibleModule, - buildUseropDto?: BuildUserOpOptions - ): Promise { - const encodedCall = encodeFunctionData({ - abi: parseAbi(["function transferOwnership(address newOwner) public"]), - functionName: "transferOwnership", - args: [newOwner] - }) - const transaction = { - to: moduleAddress, - data: encodedCall - } - const userOpResponse: UserOpResponse = await this.sendTransaction( - transaction, - buildUseropDto - ) - return userOpResponse - } - - /** - * Sends a transaction (builds and sends a user op in sequence) - * - * - Docs: https://docs.biconomy.io/account/methods#sendtransaction- - * - * @param manyOrOneTransactions Array of {@link Transaction} to be batched and sent. Can also be a single {@link Transaction}. - * @param buildUseropDto {@link BuildUserOpOptions}. - * @returns Promise<{@link Hash}> that you can use to track the user operation. - * - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonAmoy } from "viem/chains"; - * - * const signer = createWalletClient({ - * account, - * chain: polygonAmoy, - * transport: http(), - * }); - * - * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl }); // Retrieve bundler url from dashboard - * const encodedCall = encodeFunctionData({ - * abi: CounterAbi, - * functionName: "incrementNumber", - * args: ["0x..."], - * }); - * - * const transaction = { - * to: mockAddresses.Counter, - * data: encodedCall - * } - * - * const { waitForTxHash } = await smartAccount.sendTransaction(transaction); - * const { transactionHash, userOperationReceipt } = await wait(); - * - * @remarks - * This example shows how to increase the estimated gas values for a transaction using `gasOffset` parameter. - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonAmoy } from "viem/chains"; - * - * const signer = createWalletClient({ - * account, - * chain: polygonAmoy, - * transport: http(), - * }); - * - * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl }); // Retrieve bundler url from dashboard - * const encodedCall = encodeFunctionData({ - * abi: CounterAbi, - * functionName: "incrementNumber", - * args: ["0x..."], - * }); - * - * const transaction = { - * to: mockAddresses.Counter, - * 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 { transactionHash, userOperationReceipt } = await wait(); - * - */ - async sendTransaction( - manyOrOneTransactions: Transaction | Transaction[], - buildUseropDto?: BuildUserOpOptions - ): Promise { - const userOp = await this.buildUserOp( - Array.isArray(manyOrOneTransactions) - ? manyOrOneTransactions - : [manyOrOneTransactions], - buildUseropDto - ) - const response = await this.sendUserOp(userOp) - this.setDeploymentState(response) // don't wait for this to finish... - return response - } - - public async setDeploymentState({ wait }: UserOpResponse) { - if (this.deploymentState === DeploymentState.DEPLOYED) return - const { success } = await wait() - if (success) { - this.deploymentState = DeploymentState.DEPLOYED - } - } - - async sendTransactionWithExecutor( - manyOrOneTransactions: Transaction | Transaction[], - ownedAccountAddress?: Address - // buildUseropDto?: BuildUserOpOptions - ): Promise { - return await this.executeFromExecutor( - Array.isArray(manyOrOneTransactions) - ? manyOrOneTransactions - : [manyOrOneTransactions], - ownedAccountAddress - // buildUseropDto - ) - } - - /** - * Builds a user operation - * - * This method will also simulate the validation and execution of the user operation, telling the user if the user operation will be successful or not. - * - * - Docs: https://docs.biconomy.io/account/methods#builduserop- - * - * @param transactions Array of {@link Transaction} to be sent. - * @param buildUseropDto {@link BuildUserOpOptions}. - * @returns Promise> the built user operation to be sent. - * - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonAmoy } from "viem/chains"; - * - * const signer = createWalletClient({ - * account, - * chain: polygonAmoy, - * transport: http(), - * }); - * - * const smartAccount = await createSmartAccountClient({ signer, bundlerUrl }); // Retrieve bundler url from dashboard - * const encodedCall = encodeFunctionData({ - * abi: CounterAbi, - * functionName: "incrementNumber", - * args: ["0x..."], - * }); - * - * const transaction = { - * to: mockAddresses.Counter, - * data: encodedCall - * } - * - * const userOp = await smartAccount.buildUserOp([{ to: "0x...", data: encodedCall }]); - * - */ - async buildUserOp( - transactions: Transaction[], - buildUseropDto?: BuildUserOpOptions - ): Promise> { - const dummySignatureFetchPromise = this.getDummySignatures() - const [nonceFromFetch, dummySignature] = await Promise.all([ - this.getBuildUserOpNonce(buildUseropDto?.nonceOptions), - dummySignatureFetchPromise, - this.getInitCode() // Not used, but necessary to determine if the account is deployed. Will return immediately if the account is already deployed - ]) - - if (transactions.length === 0) { - throw new Error("Transactions array cannot be empty") - } - let callData: Hex = "0x" - if (!buildUseropDto?.useEmptyDeployCallData) { - if (transactions.length > 1 || buildUseropDto?.forceEncodeForBatch) { - callData = await this.encodeExecuteBatch(transactions) - } else { - callData = await this.encodeExecute(transactions[0]) - } - } - - const factoryData = await this.getFactoryData() - let userOp: Partial = { - sender: (await this.getAddress()) as Hex, - nonce: nonceFromFetch, - factoryData, - callData - } - - if (!(await this.isAccountDeployed())) { - userOp.factory = this.factoryAddress - } - - userOp.signature = dummySignature - - const gasFeeValues: GetUserOperationGasPriceReturnType | undefined = - await this.bundler?.getGasFeeValues() - - userOp.maxFeePerGas = gasFeeValues?.fast.maxFeePerGas ?? 0n - userOp.maxPriorityFeePerGas = gasFeeValues?.fast.maxPriorityFeePerGas ?? 0n - - userOp = await this.estimateUserOpGas(userOp) - - if (buildUseropDto?.paymasterServiceData?.mode === "SPONSORED") { - userOp = await this.getPaymasterAndData( - userOp, - buildUseropDto?.paymasterServiceData - ) - } - - return userOp - } - - private async getPaymasterAndData( - userOp: Partial, - paymasterServiceData: PaymasterUserOperationDto - ): Promise { - const paymaster = this - .paymaster as IHybridPaymaster - const paymasterData = await paymaster.getPaymasterAndData( - userOp, - paymasterServiceData - ) - const userOpStruct = { - ...userOp, - ...paymasterData, - callGasLimit: BigInt(userOp.callGasLimit ?? 0n), - verificationGasLimit: BigInt(userOp.verificationGasLimit ?? 0n), - preVerificationGas: BigInt(userOp.preVerificationGas ?? 0n), - sender: (await this.getAddress()) as Hex, - paymasterAndData: undefined - // paymasterAndData: paymasterData?.paymasterAndData as Hex - } as UserOperationStruct - - return userOpStruct - } - - private validateUserOpAndPaymasterRequest( - userOp: Partial, - tokenPaymasterRequest: BiconomyTokenPaymasterRequest - ): void { - if (isNullOrUndefined(userOp.callData)) { - throw new Error("UserOp callData cannot be undefined") - } - - const feeTokenAddress = tokenPaymasterRequest?.feeQuote?.tokenAddress - Logger.warn("Requested fee token is ", feeTokenAddress) - - if (!feeTokenAddress || feeTokenAddress === ADDRESS_ZERO) { - throw new Error( - "Invalid or missing token address. Token address must be part of the feeQuote in tokenPaymasterRequest" - ) - } - - const spender = tokenPaymasterRequest?.spender - Logger.warn("Spender address is ", spender) - - if (!spender || spender === ADDRESS_ZERO) { - throw new Error( - "Invalid or missing spender address. Sepnder address must be part of tokenPaymasterRequest" - ) - } - } - - /** - * - * @param userOp partial user operation without signature and paymasterAndData - * @param tokenPaymasterRequest This dto provides information about fee quote. Fee quote is received from earlier request getFeeQuotesOrData() to the Biconomy paymaster. - * maxFee and token decimals from the quote, along with the spender is required to append approval transaction. - * @notice This method should be called when gas is paid in ERC20 token using TokenPaymaster - * @description Optional method to update the userOp.calldata with batched transaction which approves the paymaster spender with necessary amount(if required) - * @returns updated userOp with new callData, callGasLimit - */ - async buildTokenPaymasterUserOp( - userOp: Partial, - tokenPaymasterRequest: BiconomyTokenPaymasterRequest - ): Promise> { - this.validateUserOpAndPaymasterRequest(userOp, tokenPaymasterRequest) - try { - Logger.warn( - "Received information about fee token address and quote ", - tokenPaymasterRequest.toString() - ) - - if (this.paymaster && this.paymaster instanceof Paymaster) { - // Make a call to paymaster.buildTokenApprovalTransaction() with necessary details - - // Review: might request this form of an array of Transaction - const approvalRequest: Transaction = await ( - this.paymaster as IHybridPaymaster - ).buildTokenApprovalTransaction(tokenPaymasterRequest) - Logger.warn("ApprovalRequest is for erc20 token ", approvalRequest.to) - - if ( - approvalRequest.data === "0x" || - approvalRequest.to === ADDRESS_ZERO - ) { - return userOp - } - - if (isNullOrUndefined(userOp.callData)) { - throw new Error("UserOp callData cannot be undefined") - } - - const decodedSmartAccountData = decodeFunctionData({ - abi: NexusAbi, - data: (userOp.callData as Hex) ?? "0x" - }) - - if (!decodedSmartAccountData) { - throw new Error( - "Could not parse userOp call data for this smart account" - ) - } - - const smartAccountExecFunctionName = - decodedSmartAccountData.functionName - - Logger.warn( - `Originally an ${smartAccountExecFunctionName} method call for Biconomy Account V2` - ) - - const initialTransaction: Transaction = { - to: "0x", - data: "0x", - value: 0n - } - if ( - smartAccountExecFunctionName === "execute" || - smartAccountExecFunctionName === "executeFromExecutor" - ) { - const methodArgsSmartWalletExecuteCall = - decodedSmartAccountData.args ?? [] - const toOriginal = - (methodArgsSmartWalletExecuteCall[0] as Hex) ?? "0x" - const valueOriginal = methodArgsSmartWalletExecuteCall[1] ?? 0n - // @ts-ignore - const dataOriginal = methodArgsSmartWalletExecuteCall?.[2] ?? "0x" - - initialTransaction.to = toOriginal - initialTransaction.value = valueOriginal - initialTransaction.data = dataOriginal - } else { - throw new Error( - `Unsupported method call: ${smartAccountExecFunctionName}` - ) - } - - const finalUserOp: Partial = { - ...userOp, - callData: await this.encodeExecuteBatch([ - approvalRequest, - initialTransaction - ]) - } - - return finalUserOp - } - } catch (error) { - Logger.log("Failed to update userOp. Sending back original op") - Logger.error( - "Failed to update callData with error", - JSON.stringify(error) - ) - return userOp - } - return userOp - } - - async signUserOpHash(userOpHash: string, params?: ModuleInfo): Promise { - this.isActiveValidationModuleDefined() - const moduleSig = (await this.signUserOpHash(userOpHash, params)) as Hex - - return moduleSig - } - - /** - * Deploys the smart contract - * - * This method will deploy a Smart Account contract. It is useful for deploying in a moment when you know that gas prices are low, - * and you want to deploy the account before sending the first user operation. This step can otherwise be skipped, - * as the deployment will alternatively be bundled with the first user operation. - * - * @param buildUseropDto {@link BuildUserOpOptions}. - * @returns Promise<{@link Hash}> that you can use to track the user operation. - * @error Throws an error if the account has already been deployed. - * @error Throws an error if the account has not enough native token balance to deploy, if not using a paymaster. - * - * @example - * import { createClient } from "viem" - * import { createSmartAccountClient } from "@biconomy/account" - * import { createWalletClient, http } from "viem"; - * import { polygonAmoy } from "viem/chains"; - * - * const signer = createWalletClient({ - * account, - * chain: polygonAmoy, - * transport: http(), - * }); - * - * const smartAccount = await createSmartAccountClient({ - * signer, - * biconomyPaymasterApiKey, - * bundlerUrl - * }); - * - * // If you want to use a paymaster... - * const { wait } = await smartAccount.deploy({ - * paymasterServiceData: { mode: "SPONSORED" }, - * }); - * - * // Or if you can't use a paymaster send native token to this address: - * const counterfactualAddress = await smartAccount.getAddress(); - * - * // Then deploy the account - * const { wait } = await smartAccount.deploy(); - * - * const { success, receipt } = await wait(); - * - */ - public async deploy( - buildUseropDto?: BuildUserOpOptions - ): Promise { - const accountAddress = this.accountAddress ?? (await this.getAddress()) - - // Check that the account has not already been deployed - const byteCode = await this.publicClient?.getBytecode({ - address: accountAddress as Hex - }) - - if (byteCode !== undefined) { - throw new Error(ERROR_MESSAGES.ACCOUNT_ALREADY_DEPLOYED) - } - - // Check that the account has enough native token balance to deploy, if not using a paymaster - if (!buildUseropDto?.paymasterServiceData?.mode) { - const nativeTokenBalance = await this.publicClient?.getBalance({ - address: accountAddress - }) - - if (nativeTokenBalance === BigInt(0)) { - throw new Error(ERROR_MESSAGES.NO_NATIVE_TOKEN_BALANCE_DURING_DEPLOY) - } - } - - const useEmptyDeployCallData = true - - return this.sendTransaction( - { - to: accountAddress, - data: "0x" - }, - { ...buildUseropDto, useEmptyDeployCallData } - ) - } - - async getFactoryData() { - if (await this.isAccountDeployed()) return undefined - this.isDefaultValidationModuleDefined() - - const signerAddress = await this.signer.getAddress() - - return encodeFunctionData({ - abi: contracts.k1ValidatorFactory.abi, - functionName: "createAccount", - args: [signerAddress, this.index, [], 0] - }) - } - - async signMessage(message: string | Uint8Array): Promise { - let signature: Hex - this.isActiveValidationModuleDefined() - const dataHash = typeof message === "string" ? toBytes(message) : message - - signature = - (await this.activeValidationModule.signMessage(dataHash)) ?? - this.defaultValidationModule.signMessage(dataHash) - signature = encodePacked( - ["address", "bytes"], - [ - this.activeValidationModule.getAddress() ?? - this.defaultValidationModule.getAddress(), - signature - ] - ) - if (await this.isAccountDeployed()) { - return signature - } - // If the account is not deployed, follow ERC 6492 - const abiEncodedMessage = encodeAbiParameters( - [ - { - type: "address", - name: "create2Factory" - }, - { - type: "bytes", - name: "factoryCalldata" - }, - { - type: "bytes", - name: "originalERC1271Signature" - } - ], - [ - this.getFactoryAddress() ?? "0x", - (await this.getFactoryData()) ?? "0x", - signature - ] - ) - return concat([abiEncodedMessage, MAGIC_BYTES]) - } - - /** - * If your contract supports signing and verifying typed data, - * you should implement this method. - * - * @param _params -- Typed Data params to sign - */ - async signTypedData(typedData: any): Promise<`0x${string}`> { - const types = { - EIP712Domain: getTypesForEIP712Domain({ - domain: typedData.domain - }), - ...typedData.types - } - - validateTypedData({ - domain: typedData.domain, - message: typedData.message, - primaryType: typedData.primaryType, - types: types - } as TypedDataDefinition) - - const appDomainSeparator = domainSeparator({ - domain: { - name: typedData.domain.name, - version: typedData.domain.version, - chainId: typedData.domain.chainId, - verifyingContract: typedData.domain.verifyingContract - } - }) - - const accountDomainStructFields = await getAccountDomainStructFields( - this.publicClient, - await this.getAddress() - ) - - const parentStructHash = keccak256( - encodePacked( - ["bytes", "bytes"], - [ - encodeAbiParameters(parseAbiParameters(["bytes32, bytes32"]), [ - keccak256(toBytes(PARENT_TYPEHASH)), - typedData.message.stuff - ]), - accountDomainStructFields - ] - ) - ) - - const wrappedTypedHash = await eip712WrapHash( - parentStructHash, - appDomainSeparator - ) - - let signature = await this.activeValidationModule.signMessage( - toBytes(wrappedTypedHash) - ) - - const contentsType = toBytes(typeToString(types)[1]) - - const signatureData = concatHex([ - signature, - appDomainSeparator, - typedData.message.stuff, - toHex(contentsType), - toHex(contentsType.length, { size: 2 }) - ]) - - signature = encodePacked( - ["address", "bytes"], - [ - this.activeValidationModule.getAddress() ?? - this.defaultValidationModule.getAddress(), - signatureData - ] - ) - - return signature - } - - async getIsValidSignatureData( - messageHash: Hex, - signature: Hex - ): Promise { - return encodeFunctionData({ - abi: NexusAbi, - functionName: "isValidSignature", - args: [messageHash, signature] - }) - } - - async isModuleInstalled(module: Module) { - if (await this.isAccountDeployed()) { - const accountContract = await this._getAccountContract() - const result = await accountContract.read.isModuleInstalled([ - BigInt(moduleTypeIds[module.type]), - module.moduleAddress, - module.data ?? "0x" - ]) - return result - } - return false - } - - getSmartAccountOwner(): SmartAccountSigner { - return this.signer - } - - async installModule( - module: Module, - buildUserOpOptions?: BuildUserOpOptions - ): Promise { - let execution: Execution - switch (module.type) { - case "validator": - case "executor": - case "hook": - execution = await this._installModule({ - moduleAddress: module.moduleAddress, - type: module.type, - data: module.data - }) - return this.sendTransaction( - { - to: execution.target, - data: execution.callData, - value: execution.value - }, - buildUserOpOptions - ) - case "fallback": - if (!module.selector || !module.callType) { - throw new Error( - "Selector param is required for a Fallback Handler Module" - ) - } - execution = await this._uninstallFallback(module) - return this.sendTransaction( - { - to: execution.target, - data: execution.callData, - value: execution.value - }, - buildUserOpOptions - ) - default: - throw new Error(`Unknown module type ${module.type}`) - } - } - - async _installModule(module: Module): Promise { - const isInstalled = await this.isModuleInstalled(module) - - if (!isInstalled) { - const execution = { - target: await this.getAddress(), - value: BigInt(0), - callData: encodeFunctionData({ - functionName: "installModule", - abi: parseAbi([ - "function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external payable" - ]), - args: [ - BigInt(moduleTypeIds[module.type]), - module.moduleAddress, - module.data || "0x" - ] - }) - } - return execution - } - throw new Error("Module already installed") - } - - async uninstallModule( - module: Module, - buildUserOpOptions?: BuildUserOpOptions - ): Promise { - let execution: Execution - switch (module.type) { - case "validator": - case "executor": - case "hook": - execution = await this._uninstallModule(module) - return await this.sendTransaction( - { - to: execution.target, - data: execution.callData, - value: execution.value - }, - buildUserOpOptions - ) - case "fallback": - if (!module.selector) { - throw new Error( - `Selector param is required for module type ${module.type}` - ) - } - execution = await this._uninstallFallback(module) - return await this.sendTransaction( - { - to: execution.target, - data: execution.callData, - value: execution.value - }, - buildUserOpOptions - ) - default: - throw new Error(`Unknown module type ${module.type}`) - } - } - - private _getModuleIndex( - installedModules: - | Awaited> - | Awaited>, - module: Module - ): Hex { - const index = installedModules.indexOf(getAddress(module.moduleAddress)) - if (index === 0) { - return SENTINEL_ADDRESS - } - if (index > 0) { - // @ts-ignore: TODO: Gabi This looks wrong - return installedModules[index - 1] - } - throw new Error( - `Module ${module.moduleAddress} not found in installed modules` - ) - } - - async getPreviousModule(module: Module) { - if (module.type === "validator") { - const installedModules = await this.getInstalledValidators() - return this._getModuleIndex(installedModules, module) - } - if (module.type === "executor") { - const installedModules = await this.getInstalledExecutors() - return this._getModuleIndex(installedModules, module) - } - throw new Error(`Unknown module type ${module.type}`) - } - - private async _uninstallFallback(module: Module): Promise { - let execution: Execution - - const isInstalled = await this.isModuleInstalled(module) - - if (isInstalled) { - execution = { - target: await this.getAddress(), - value: BigInt(0), - callData: encodeFunctionData({ - functionName: "uninstallModule", - abi: parseAbi([ - "function uninstallModule(uint256 moduleTypeId, address module, bytes deInitData)" - ]), - args: [ - BigInt(moduleTypeIds[module.type]), - module.moduleAddress, - encodePacked( - ["bytes4", "bytes"], - [module.selector ?? "0x", module.data ?? "0x"] - ) - ] - }) - } - return execution - } - throw new Error("Module is not installed") - } - - private async _uninstallModule(module: Module): Promise { - let execution: Execution - const isInstalled = await this.isModuleInstalled(module) - - if (isInstalled) { - let moduleData = module.data || "0x" - if (module.type === "validator" || module.type === "executor") { - const prev = await this.getPreviousModule(module) - moduleData = encodeAbiParameters( - [ - { name: "prev", type: "address" }, - { name: "disableModuleData", type: "bytes" } - ], - [prev, moduleData] - ) - } - execution = { - target: await this.getAddress(), - value: BigInt(0), - callData: encodeFunctionData({ - functionName: "uninstallModule", - abi: parseAbi([ - "function uninstallModule(uint256 moduleTypeId, address module, bytes deInitData)" - ]), - args: [ - BigInt(moduleTypeIds[module.type]), - module.moduleAddress, - moduleData - ] - }) - } - return execution - } - throw new Error("Module is not installed") - } - - private async executeFromExecutor( - transactions: Transaction[], - ownedAccountAddress?: Address - // buildUseropDto?: BuildUserOpOptions - ): Promise { - if (this.activeExecutionModule) { - if (transactions.length > 1) { - const executions: { target: Hex; value: bigint; callData: Hex }[] = - transactions.map((tx) => { - return { - target: tx.to as Hex, - callData: (tx.data ?? "0x") as Hex, - value: BigInt(tx.value ?? 0n) - } - }) - return await this.activeExecutionModule?.execute( - executions, - ownedAccountAddress - ) - } - const execution = { - target: transactions[0].to as Hex, - callData: (transactions[0].data ?? "0x") as Hex, - value: BigInt(transactions[0].value ?? 0n) - } - return await this.activeExecutionModule.execute( - execution, - ownedAccountAddress - ) - } - throw new Error( - "Please set an active executor module before running this method." - ) - } - - /** - * Checks if the account contract supports a specific execution mode. - * @param mode - The execution mode to check, represented as a viem Address. - * @returns A promise that resolves to a boolean indicating whether the execution mode is supported. - */ - async supportsExecutionMode(mode: Address): Promise { - const accountContract = await this._getAccountContract() - return (await accountContract.read.supportsExecutionMode([mode])) as boolean - } - - async getInstalledValidators() { - const accountContract = await this._getAccountContract() - return await accountContract.read.getValidatorsPaginated([ - SENTINEL_ADDRESS, - 100n - ]) - } - - async getInstalledExecutors() { - const accountContract = await this._getAccountContract() - return await accountContract.read.getExecutorsPaginated([ - SENTINEL_ADDRESS, - 100n - ]) - } - - /** - * Retrieves all installed modules for the account, including validators, executors, active hook, and fallback handler. - * @returns A promise that resolves to an array of addresses representing all installed modules. - */ - async getInstalledModules() { - const validators = await this.getInstalledValidators() - const executors = await this.getInstalledExecutors() - const hook = await this.getActiveHook() - const fallbackHandler = await this.getFallbackBySelector() - - return [...validators, ...executors, hook, fallbackHandler] - .flat() - .filter(Boolean) - } - - /** - * Retrieves the active hook for the account. - * @returns A promise that resolves to the address of the active hook. - */ - async getActiveHook() { - const accountContract = await this._getAccountContract() - return await accountContract.read.getActiveHook() - } - - /** - * Retrieves the fallback handler for a given selector. - * @param selector - Optional hexadecimal selector. If not provided, uses a generic fallback selector. - * @returns A promise that resolves to the address of the fallback handler. - */ - async getFallbackBySelector(selector?: Hex) { - const accountContract = await this._getAccountContract() - return await accountContract.read.getFallbackHandlerBySelector([ - selector ?? GENERIC_FALLBACK_SELECTOR - ]) - } - - /** - * Checks if the account supports a specific module type. - * @param moduleType - The type of module to check for support. - * @returns A promise that resolves to a boolean indicating whether the module type is supported. - */ - async supportsModule(module: Module): Promise { - const accountContract = await this._getAccountContract() - const moduleIndex = - module.type === "validator" - ? 1n - : module.type === "executor" - ? 2n - : module.type === "fallback" - ? 3n - : 4n - return await accountContract.read.supportsModule([moduleIndex]) - } -} diff --git a/src/account/index.ts b/src/account/index.ts deleted file mode 100644 index 2a1325406..000000000 --- a/src/account/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NexusSmartAccount } from "./NexusSmartAccount.js" -import type { NexusSmartAccountConfig } from "./utils/Types.js" - -export * from "./NexusSmartAccount.js" -export * from "./utils/index.js" -export * from "./signers/local-account.js" -export * from "./signers/wallet-client.js" - -export const createSmartAccountClient = NexusSmartAccount.create - -export type SmartWalletConfig = NexusSmartAccountConfig diff --git a/src/account/signers/local-account.ts b/src/account/signers/local-account.ts deleted file mode 100644 index 65bf3c521..000000000 --- a/src/account/signers/local-account.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { - HDAccount, - Hex, - LocalAccount, - PrivateKeyAccount, - SignableMessage, - TypedData, - TypedDataDefinition -} from "viem" -import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts" -import type { SmartAccountSigner } from "../utils/Types.js" - -export class LocalAccountSigner< - T extends HDAccount | PrivateKeyAccount | LocalAccount -> implements SmartAccountSigner -{ - inner: T - signerType: string - - constructor(inner: T) { - this.inner = inner - this.signerType = inner.type // type: "local" - } - - readonly signMessage: (message: SignableMessage) => Promise<`0x${string}`> = ( - message - ) => { - return this.inner.signMessage({ message }) - } - - readonly signTypedData = async < - const TTypedData extends TypedData | { [key: string]: unknown }, - TPrimaryType extends string = string - >( - params: TypedDataDefinition - ): Promise => { - return this.inner.signTypedData(params) - } - - readonly getAddress: () => Promise<`0x${string}`> = async () => { - return this.inner.address - } - - static mnemonicToAccountSigner(key: string): LocalAccountSigner { - const signer = mnemonicToAccount(key) - return new LocalAccountSigner(signer) - } - - static privateKeyToAccountSigner( - key: Hex - ): LocalAccountSigner { - const signer = privateKeyToAccount(key) - return new LocalAccountSigner(signer) - } -} diff --git a/src/account/signers/wallet-client.ts b/src/account/signers/wallet-client.ts deleted file mode 100644 index 69c10fe5a..000000000 --- a/src/account/signers/wallet-client.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - type Hex, - type SignableMessage, - type TypedData, - type TypedDataDefinition, - type WalletClient, - getAddress -} from "viem" -import type { SmartAccountSigner } from "../utils/Types.js" - -export class WalletClientSigner implements SmartAccountSigner { - signerType: string - inner: WalletClient - - constructor(client: WalletClient, signerType: string) { - this.inner = client - if (!signerType) { - throw new Error(`InvalidSignerTypeError: ${signerType}`) - } - this.signerType = signerType - } - - getAddress: () => Promise<`0x${string}`> = async () => { - const addresses = await this.inner.getAddresses() - return getAddress(addresses[0]) - } - - readonly signMessage: (message: SignableMessage) => Promise<`0x${string}`> = - async (message) => { - const account = this.inner.account ?? (await this.getAddress()) - - return this.inner.signMessage({ message, account }) - } - - signTypedData = async < - const TTypedData extends TypedData | { [key: string]: unknown }, - TPrimaryType extends string = string - >( - typedData: TypedDataDefinition - ): Promise => { - const account = this.inner.account ?? (await this.getAddress()) - - return this.inner.signTypedData({ - account, - ...typedData - }) - } -} diff --git a/src/account/utils/EthersSigner.ts b/src/account/utils/EthersSigner.ts deleted file mode 100644 index 8af82cb10..000000000 --- a/src/account/utils/EthersSigner.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Hex, SignableMessage } from "viem" -import type { LightSigner, SmartAccountSigner } from "../utils/Types.js" - -export class EthersSigner - implements SmartAccountSigner -{ - signerType = "ethers" - - inner: T - - constructor(inner: T, signerType: string) { - this.inner = inner - this.signerType = signerType - } - - async getAddress() { - return (await this.inner.getAddress()) as Hex - } - - async signMessage(_message: SignableMessage): Promise { - const message = typeof _message === "string" ? _message : _message.raw - const signature = await this.inner?.signMessage(message) - return this.#correctSignature(signature as Hex) - } - - // biome-ignore lint/suspicious/noExplicitAny: - async signTypedData(_: any): Promise { - throw new Error("signTypedData is not supported for Ethers Signer") - } - - #correctSignature = (_signature: Hex): Hex => { - let signature = _signature - const potentiallyIncorrectV = Number.parseInt(signature.slice(-2), 16) - if (![27, 28].includes(potentiallyIncorrectV)) { - const correctV = potentiallyIncorrectV + 27 - signature = signature.slice(0, -2) + correctV.toString(16) - } - return signature as Hex - } -} - -export default EthersSigner diff --git a/src/account/utils/Helpers.ts b/src/account/utils/Helpers.ts deleted file mode 100644 index 38dc3e01d..000000000 --- a/src/account/utils/Helpers.ts +++ /dev/null @@ -1,16 +0,0 @@ -const VARS_T0_CHECK = [ - "BICONOMY_SDK_DEBUG", - "REACT_APP_BICONOMY_SDK_DEBUG", - "NEXT_PUBLIC_BICONOMY_SDK_DEBUG" -] - -export const isDebugging = (): boolean => { - try { - // @ts-ignore - return VARS_T0_CHECK.some( - (key) => process?.env?.[key]?.toString() === "true" - ) - } catch (e) { - return false - } -} diff --git a/src/account/utils/HttpRequests.ts b/src/account/utils/HttpRequests.ts deleted file mode 100644 index 0fcc51025..000000000 --- a/src/account/utils/HttpRequests.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { getAAError } from "../../bundler/utils/getAAError.js" -import { Logger } from "./Logger.js" -import type { Service } from "./Types.js" - -export enum HttpMethod { - Get = "get", - Post = "post", - Delete = "delete" -} - -export interface HttpRequest { - url: string - method: HttpMethod - // biome-ignore lint/suspicious/noExplicitAny: - body?: Record -} - -export async function sendRequest( - { url, method, body }: HttpRequest, - service: Service -): Promise { - const stringifiedBody = JSON.stringify(body) - - Logger.log(`${service} RPC Request`, { url, body: stringifiedBody }) - - const response = await fetch(url, { - method, - headers: { - Accept: "application/json", - "Content-Type": "application/json" - }, - body: stringifiedBody - }) - - // biome-ignore lint/suspicious/noExplicitAny: - let jsonResponse: any - try { - jsonResponse = await response.json() - Logger.log(`${service} RPC Response`, jsonResponse) - } catch (error) { - if (!response.ok) { - throw await getAAError(response.statusText, response.status, service) - } - } - - if (response.ok) { - return jsonResponse as T - } - if (jsonResponse.error) { - throw await getAAError( - `Error coming from ${service}: ${jsonResponse.error.message}` - ) - } - if (jsonResponse.message) { - throw await getAAError(jsonResponse.message, response.status, service) - } - if (jsonResponse.msg) { - throw await getAAError(jsonResponse.msg, response.status, service) - } - if (jsonResponse.data) { - throw await getAAError(jsonResponse.data, response.status, service) - } - if (jsonResponse.detail) { - throw await getAAError(jsonResponse.detail, response.status, service) - } - if (jsonResponse.message) { - throw await getAAError(jsonResponse.message, response.status, service) - } - if (jsonResponse.nonFieldErrors) { - throw await getAAError( - jsonResponse.nonFieldErrors, - response.status, - service - ) - } - if (jsonResponse.delegate) { - throw await getAAError(jsonResponse.delegate, response.status, service) - } - throw await getAAError(response.statusText, response.status, service) -} diff --git a/src/account/utils/Types.ts b/src/account/utils/Types.ts deleted file mode 100644 index d65e619e1..000000000 --- a/src/account/utils/Types.ts +++ /dev/null @@ -1,661 +0,0 @@ -import type { - Address, - Chain, - Hash, - Hex, - PrivateKeyAccount, - PublicClient, - SignTypedDataParameters, - SignableMessage, - TypedData, - TypedDataDefinition, - WalletClient -} from "viem" -import type { IBundler } from "../../bundler" -import type { ModuleInfo, ModuleType } from "../../modules" -import type { BaseValidationModule } from "../../modules/base/BaseValidationModule" -import type { - FeeQuotesOrDataDto, - IPaymaster, - PaymasterFeeQuote, - SmartAccountData, - SponsorUserOperationDto -} from "../../paymaster" -import type { MODE_MODULE_ENABLE, MODE_VALIDATION } from "../utils/Constants" - -export type EntryPointAddresses = Record -export type BiconomyFactories = Record -export type EntryPointAddressesByVersion = Record -export type BiconomyFactoriesByVersion = Record - -export type SmartAccountConfig = { - /** entryPointAddress: address of the entry point */ - entryPointAddress: Address - /** factoryAddress: address of the smart account factory */ - bundler?: IBundler -} - -export interface BalancePayload { - /** address: The address of the account */ - address: string - /** chainId: The chainId of the network */ - chainId: number - /** amount: The amount of the balance */ - amount: bigint - /** decimals: The number of decimals */ - decimals: number - /** formattedAmount: The amount of the balance formatted */ - formattedAmount: string -} - -export interface WithdrawalRequest { - /** The address of the asset */ - address: Hex - /** The amount to withdraw. Expects unformatted amount. Will use max amount if unset */ - 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 -} - -export interface GasOverheads { - /** fixed: fixed gas overhead */ - fixed: number - /** perUserOp: per user operation gas overhead */ - perUserOp: number - /** perUserOpWord: per user operation word gas overhead */ - perUserOpWord: number - /** zeroByte: per byte gas overhead */ - zeroByte: number - /** nonZeroByte: per non zero byte gas overhead */ - nonZeroByte: number - /** bundleSize: per signature bundleSize */ - bundleSize: number - /** sigSize: sigSize gas overhead */ - sigSize: number -} - -export type BaseSmartAccountConfig = { - /** index: helps to not conflict with other smart account instances */ - index?: bigint - /** provider: WalletClientSigner from viem */ - provider?: WalletClient - /** entryPointAddress: address of the smart account entry point */ - entryPointAddress?: string - /** accountAddress: address of the smart account, potentially counterfactual */ - accountAddress?: string - /** overheads: {@link GasOverheads} */ - overheads?: Partial - /** paymaster: {@link IPaymaster} interface */ - paymaster?: IPaymaster -} - -export type BiconomyTokenPaymasterRequest = { - /** feeQuote: {@link 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 - /** maxApproval: If set to true, the paymaster will approve the maximum amount of tokens required for the transaction. Not recommended */ - maxApproval?: boolean - /* skip option to patch callData if approval is already given to the paymaster */ - skipPatchCallData?: boolean -} - -export type RequireAtLeastOne = Pick< - T, - Exclude -> & - { - [K in Keys]-?: Required> & Partial>> - }[Keys] - -export type ConditionalBundlerProps = RequireAtLeastOne< - { - bundler: IBundler - bundlerUrl: string - }, - "bundler" | "bundlerUrl" -> -export type ResolvedBundlerProps = { - bundler: IBundler -} -export type ConditionalValidationProps = RequireAtLeastOne< - { - defaultValidationModule: BaseValidationModule - signer: SupportedSigner - }, - "defaultValidationModule" | "signer" -> - -export type ResolvedValidationProps = { - /** defaultValidationModule: {@link BaseValidationModule} */ - defaultValidationModule: BaseValidationModule - /** activeValidationModule: {@link BaseValidationModule}. The active validation module. Will default to the defaultValidationModule */ - activeValidationModule: BaseValidationModule - /** signer: ethers Wallet, viemWallet or alchemys SmartAccountSigner */ - signer: SmartAccountSigner - /** rpcUrl */ - rpcUrl: string -} - -export type ConfigurationAddresses = { - factoryAddress: Hex - k1ValidatorAddress: Hex - entryPointAddress: Hex -} - -export type NexusSmartAccountConfigBaseProps = { - /** chain: The chain from viem */ - chain: Chain - /** Factory address of biconomy factory contract or some other contract you have deployed on chain */ - factoryAddress?: Hex - /** K1Validator Address */ - k1ValidatorAddress?: 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 - /** implementation of smart contract address or some other contract you have deployed and want to override */ - implementationAddress?: Hex - /** defaultFallbackHandler: override the default fallback contract address */ - defaultFallbackHandler?: Hex - /** rpcUrl */ - rpcUrl?: string - /** paymasterUrl: The Paymaster URL retrieved from the Biconomy dashboard */ - paymasterUrl?: string - /** biconomyPaymasterApiKey: The API key retrieved from the Biconomy dashboard */ - biconomyPaymasterApiKey?: string - /** activeValidationModule: The active validation module. Will default to the defaultValidationModule */ - 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 - /** the index of SA the EOA have generated and till which indexes the upgraded SA should scan */ - maxIndexForScan?: bigint - /** The initial code to be used for the smart account */ - initCode?: Hex - /** Used for session key manager module */ - sessionData?: ModuleInfo -} -export type NexusSmartAccountConfig = NexusSmartAccountConfigBaseProps & - BaseSmartAccountConfig & - ConditionalBundlerProps & - ConditionalValidationProps - -export type NexusSmartAccountConfigConstructorProps = - NexusSmartAccountConfigBaseProps & - BaseSmartAccountConfig & - ResolvedBundlerProps & - ResolvedValidationProps & - ConfigurationAddresses - -/** - * Represents options for building a user operation. - * @typedef BuildUserOpOptions - * @property {GasOffset} [gasOffset] - Increment gas values by giving an offset, the given value will be an increment to the current estimated gas values, not an override. - * @property {ModuleInfo} [params] - Parameters relevant to the module, mostly relevant to sessions. - * @property {NonceOptions} [nonceOptions] - Options for overriding the nonce. - * @property {boolean} [forceEncodeForBatch] - Whether to encode the user operation for batch. - * @property {PaymasterUserOperationDto} [paymasterServiceData] - Options specific to transactions that involve a paymaster. - * @property {SimulationType} [simulationType] - Determine which parts of the transaction a bundler will simulate: "validation" | "validation_and_execution". - * @property {StateOverrideSet} [stateOverrideSet] - For overriding the state. - * @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 - useExecutor?: boolean -} - -export type NonceOptions = { - /** nonceKey: The key to use for nonce */ - nonceKey?: bigint - /** validationMode: Mode of the validation module */ - validationMode?: typeof MODE_VALIDATION | typeof MODE_MODULE_ENABLE - /** nonceOverride: The nonce to use for the transaction */ - nonceOverride?: bigint -} - -export type SimulationType = "validation" | "validation_and_execution" - -/** - * Represents an offset percentage value used for gas-related calculations. - * @remarks - * This type defines offset percentages for various gas-related parameters. Each percentage represents a proportion of the current estimated gas values. - * For example: - * - A value of `1` represents a 1% offset. - * - A value of `100` represents a 100% offset. - * @public - */ -/** - * Represents an object containing offset percentages for gas-related parameters. - * @typedef GasOffsetPct - * @property {number} [callGasLimitOffsetPct] - Percentage offset for the gas limit used by inner account execution. - * @property {number} [verificationGasLimitOffsetPct] - Percentage offset for the actual gas used by the validation of a UserOperation. - * @property {number} [preVerificationGasOffsetPct] - Percentage offset representing the gas overhead of a UserOperation. - * @property {number} [maxFeePerGasOffsetPct] - Percentage offset for the maximum fee per gas (similar to EIP-1559 max_fee_per_gas). - * @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 -} - -export type InitilizationData = { - accountIndex?: number - signerAddress?: string -} - -export type PaymasterUserOperationDto = SponsorUserOperationDto & - FeeQuotesOrDataDto & { - /** mode: sponsored or erc20 */ - mode: "SPONSORED" | "ERC20" - /** Always recommended, especially when using token paymaster */ - calculateGasLimits?: boolean - /** Expiry duration in seconds */ - expiryDuration?: number - /** Webhooks to be fired after user op is sent */ - // biome-ignore lint/suspicious/noExplicitAny: - webhookData?: Record - /** Smart account meta data */ - smartAccountInfo?: SmartAccountData - /** the fee-paying token address */ - feeTokenAddress?: string - /** The fee quote */ - feeQuote?: PaymasterFeeQuote - /** The address of the spender. This is usually set to FeeQuotesOrDataResponse.tokenPaymasterAddress */ - spender?: Hex - /** Not recommended */ - maxApproval?: boolean - /* skip option to patch callData if approval is already given to the paymaster */ - skipPatchCallData?: boolean - } - -export type InitializeV2Data = { - accountIndex?: number -} - -export type EstimateUserOpGasParams = { - userOp: Partial - /** paymasterServiceData: Options specific to transactions that involve a paymaster */ - paymasterServiceData?: SponsorUserOperationDto -} - -export interface TransactionDetailsForUserOp { - /** target: The address of the contract to call */ - target: string - /** data: The data to send to the contract */ - data: string - /** value: The value to send to the contract */ - value?: BigNumberish - /** gasLimit: The gas limit to use for the transaction */ - gasLimit?: BigNumberish - /** maxFeePerGas: The maximum fee per gas to use for the transaction */ - maxFeePerGas?: BigNumberish - /** maxPriorityFeePerGas: The maximum priority fee per gas to use for the transaction */ - maxPriorityFeePerGas?: BigNumberish - /** nonce: The nonce to use for the transaction */ - nonce?: BigNumberish -} - -export type CounterFactualAddressParam = { - index?: bigint - 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 - /** the index of SA the EOA have generated and till which indexes the upgraded SA should scan */ - maxIndexForScan?: bigint -} - -export type QueryParamsForAddressResolver = { - eoaAddress: Hex - index: bigint - moduleAddress: Hex - moduleSetupData: Hex - maxIndexForScan?: bigint -} - -export type SmartAccountInfo = { - /** accountAddress: The address of the smart account */ - accountAddress: Hex - /** factoryAddress: The address of the smart account factory */ - factoryAddress: Hex - /** currentImplementation: The address of the current implementation */ - currentImplementation: string - /** currentVersion: The version of the smart account */ - currentVersion: string - /** factoryVersion: The version of the factory */ - factoryVersion: string - /** deploymentIndex: The index of the deployment */ - deploymentIndex: BigNumberish -} - -export type ValueOrData = RequireAtLeastOne< - { - value: BigNumberish | string - data: string - }, - "value" | "data" -> -export type Transaction = { - to: string -} & ValueOrData - -export type SupportedToken = Omit< - PaymasterFeeQuote, - "maxGasFeeUSD" | "usdPayment" | "maxGasFee" | "validUntil" -> & { 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" -export type SupportedSigner = - | SmartAccountSigner - | WalletClient - | Signer - | LightSigner - | PrivateKeyAccount -export type Service = "Bundler" | "Paymaster" - -export interface LightSigner { - getAddress(): Promise - signMessage(message: string | Uint8Array): Promise -} - -export type StateOverrideSet = { - [key: string]: { - balance?: string - nonce?: string - code?: string - state?: object - stateDiff?: object - } -} - -export type BigNumberish = Hex | number | bigint -export type BytesLike = Uint8Array | Hex | string - -//#region UserOperationStruct -// based on @account-abstraction/common -// this is used for building requests -export type UserOperationStruct = { - sender: Address - nonce: bigint - factory?: Address - factoryData?: Hex - callData: Hex - callGasLimit: bigint - verificationGasLimit: bigint - preVerificationGas: bigint - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - paymaster?: Address - paymasterVerificationGasLimit?: bigint - paymasterPostOpGasLimit?: bigint - paymasterData?: Hex - signature: Hex - // initCode?: never - paymasterAndData?: never -} -//#endregion UserOperationStruct - -//#region SmartAccountSigner -/** - * A signer that can sign messages and typed data. - * - * @template Inner - the generic type of the inner client that the signer wraps to provide functionality such as signing, etc. - * - * @var signerType - the type of the signer (e.g. local, hardware, etc.) - * @var inner - the inner client of @type {Inner} - * - * @method getAddress - get the address of the signer - * @method signMessage - sign a message - * @method signTypedData - sign typed data - */ - -// biome-ignore lint/suspicious/noExplicitAny: -export interface SmartAccountSigner { - signerType: string - inner: Inner - - getAddress: () => Promise
- - signMessage: (message: SignableMessage) => Promise - - signTypedData: < - const TTypedData extends TypedData | { [key: string]: unknown }, - TPrimaryType extends string = string - >( - params: TypedDataDefinition - ) => Promise -} -//#endregion SmartAccountSigner - -//#region UserOperationCallData -export type UserOperationCallData = - | { - /* the target of the call */ - target: Address - /* the data passed to the target */ - data: Hex - /* the amount of native token to send to the target (default: 0) */ - value?: bigint - } - | Hex -//#endregion UserOperationCallData - -//#region BatchUserOperationCallData -export type BatchUserOperationCallData = Exclude[] -//#endregion BatchUserOperationCallData - -export type SignTypedDataParams = Omit - -export type BaseSmartContractAccountProps = - NexusSmartAccountConfigConstructorProps & { - /** chain: The chain from viem */ - chain: Chain - /** factoryAddress: The address of the factory */ - factoryAddress: Hex - /** entryPointAddress: The address of the entry point */ - entryPointAddress: Hex - /** accountAddress: The address of the account */ - accountAddress?: Address - } - -export interface ISmartContractAccount< - TSigner extends SmartAccountSigner = SmartAccountSigner -> { - /** - * The RPC provider the account uses to make RPC calls - */ - publicClient: PublicClient - - /** - * @returns the init code for the account - */ - getInitCode(): Promise - - /** - * This is useful for estimating gas costs. It should return a signature that doesn't cause the account to revert - * when validation is run during estimation. - * - * @returns a dummy signature that doesn't cause the account to revert during estimation - */ - getDummySignature(): Hex - - /** - * Encodes a call to the account's execute function. - * - * @param target - the address receiving the call data - * @param value - optionally the amount of native token to send - * @param data - the call data or "0x" if empty - */ - encodeExecute(transaction: Transaction, useExecutor: boolean): Promise - - /** - * Encodes a batch of transactions to the account's batch execute function. - * NOTE: not all accounts support batching. - * @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 - - /** - * @returns the nonce of the account - */ - getNonce( - validationMode?: typeof MODE_VALIDATION | typeof MODE_MODULE_ENABLE - ): Promise - - /** - * If your account handles 1271 signatures of personal_sign differently - * than it does UserOperations, you can implement two different approaches to signing - * - * @param uoHash -- The hash of the UserOperation to sign - * @returns the signature of the UserOperation - */ - signUserOperationHash(uoHash: Hash): Promise - - /** - * Returns a signed and prefixed message. - * - * @param msg - the message to sign - * @returns the signature of the message - */ - signMessage(msg: string | Uint8Array | Hex): Promise - - /** - * Signs a typed data object as per ERC-712 - * - * @param typedData - * @returns the signed hash for the message passed - */ - signTypedData(typedData: any): Promise - - /** - * If the account is not deployed, it will sign the message and then wrap it in 6492 format - * - * @param msg - the message to sign - * @returns ths signature wrapped in 6492 format - */ - 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 - * - * @param params - {@link SignTypedDataParams} - * @returns the signed hash for the params passed in wrapped in 6492 format - */ - signTypedDataWith6492(params: SignTypedDataParams): Promise - - /** - * @returns the address of the account - */ - getAddress(): Promise
- - /** - * @returns the current account signer instance that the smart account client - * operations are being signed with. - * - * 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 - - /** - * @returns the address of the factory contract for the smart account - */ - getFactoryAddress(): Address - - /** - * @returns the address of the entry point contract for the smart account - */ - getEntryPointAddress(): Address - - /** - * Allows you to add additional functionality and utility methods to this account - * via a decorator pattern. - * - * NOTE: this method does not allow you to override existing methods on the account. - * - * @example - * ```ts - * const account = new BaseSmartCobntractAccount(...).extend((account) => ({ - * readAccountState: async (...args) => { - * return this.rpcProvider.readContract({ - * address: await this.getAddress(), - * abi: ThisContractsAbi - * args: args - * }); - * } - * })); - * - * account.debugSendUserOperation(...); - * ``` - * - * @param extendFn -- this function gives you access to the created account instance and returns an object - * with the extension methods - * @returns -- the account with the extension methods added - */ - extend: (extendFn: (self: this) => R) => this & R - - encodeUpgradeToAndCall: ( - upgradeToImplAddress: Address, - upgradeToInitData: Hex - ) => Promise -} - -export type TransferOwnershipCompatibleModule = - | "0x0000001c5b32F37F5beA87BDD5374eB2aC54eA8e" - | "0x000000824dc138db84FD9109fc154bdad332Aa8E" - -export type ModuleInfoParams = { - moduleAddress: Address - moduleType: ModuleType - moduleSelector?: Hex - data?: Hex -} - -export type EIP712DomainReturn = [ - Hex, - string, - string, - bigint, - Address, - Hex, - bigint[] -] - -export enum CallType { - CALLTYPE_SINGLE = "0x00", - CALLTYPE_BATCH = "0x01", - CALLTYPE_STATIC = "0xFE", - CALLTYPE_DELEGATECALL = "0xFF" -} - -export type NEXUS_VERSION_TYPE = "1.0.0-beta" - -export type AccountMetadata = { - name: string - version: string - chainId: bigint -} - -export type WithRequired = Required> - -export type TypeField = { - name: string - type: string -} - -export type TypeDefinition = { - [key: string]: TypeField[] -} diff --git a/src/account/utils/convertSigner.ts b/src/account/utils/convertSigner.ts deleted file mode 100644 index 3dd8bfd59..000000000 --- a/src/account/utils/convertSigner.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - http, - type PrivateKeyAccount, - type WalletClient, - createWalletClient -} from "viem" -import { ERROR_MESSAGES, WalletClientSigner } from "../../account" -import type { Signer, SmartAccountSigner, SupportedSigner } from "../../account" -import { EthersSigner } from "./EthersSigner.js" - -interface SmartAccountResult { - signer: SmartAccountSigner -} - -function isPrivateKeyAccount( - signer: SupportedSigner -): signer is PrivateKeyAccount { - return (signer as PrivateKeyAccount).type === "local" -} - -export function isWalletClient( - signer: SupportedSigner -): signer is WalletClient { - return (signer as WalletClient).name === "Wallet Client" -} - -function isEthersSigner(signer: SupportedSigner): signer is Signer { - return (signer as Signer).provider !== undefined -} - -function isAlchemySigner( - signer: SupportedSigner -): signer is SmartAccountSigner { - return (signer as SmartAccountSigner).signerType !== undefined -} - -export const convertSigner = async ( - signer: SupportedSigner, - rpcUrl?: string -): Promise => { - let resolvedSmartAccountSigner: SmartAccountSigner - - if (!isAlchemySigner(signer)) { - if (isEthersSigner(signer)) { - const ethersSigner = signer as Signer - if (!rpcUrl) throw new Error(ERROR_MESSAGES.MISSING_RPC_URL) - if (!ethersSigner.provider) { - throw new Error("Cannot consume an ethers Wallet without a provider") - } - // convert ethers Wallet to alchemy's SmartAccountSigner under the hood - resolvedSmartAccountSigner = new EthersSigner(ethersSigner, "ethers") - } else if (isWalletClient(signer)) { - const walletClient = signer as WalletClient - if (!walletClient.account) { - throw new Error("Cannot consume a viem wallet without an account") - } - // convert viems walletClient to alchemy's SmartAccountSigner under the hood - resolvedSmartAccountSigner = new WalletClientSigner(walletClient, "viem") - } else if (isPrivateKeyAccount(signer)) { - if (!rpcUrl) throw new Error(ERROR_MESSAGES.MISSING_RPC_URL) - const walletClient = createWalletClient({ - account: signer as PrivateKeyAccount, - transport: http(rpcUrl) - }) - resolvedSmartAccountSigner = new WalletClientSigner(walletClient, "viem") - } else { - throw new Error("Unsupported signer") - } - } else { - if (!rpcUrl) throw new Error(ERROR_MESSAGES.MISSING_RPC_URL) - resolvedSmartAccountSigner = signer as SmartAccountSigner - } - return { - signer: resolvedSmartAccountSigner - } -} diff --git a/src/bundler/Bundler.ts b/src/bundler/Bundler.ts deleted file mode 100644 index bce6491bc..000000000 --- a/src/bundler/Bundler.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { http, type Hash, type PublicClient, createPublicClient } from "viem" -import contracts from "../__contracts/index.js" -import type { UserOperationStruct } from "../account" -import { HttpMethod, isNullOrUndefined, sendRequest } from "../account" -import type { IBundler } from "./interfaces/IBundler.js" -import { - UserOpReceiptIntervals, - UserOpReceiptMaxDurationIntervals, - UserOpWaitForTxHashIntervals, - UserOpWaitForTxHashMaxDurationIntervals -} from "./utils/Constants.js" -import { - decodeUserOperationError, - getTimestampInSeconds -} from "./utils/HelperFunction.js" -import type { - BundlerConfig, - BundlerConfigWithChainId, - BundlerEstimateUserOpGasResponse, - GetUserOpByHashResponse, - GetUserOperationGasPriceReturnType, - GetUserOperationReceiptResponse, - GetUserOperationStatusResponse, - UserOpByHashResponse, - UserOpGasResponse, - UserOpReceipt, - UserOpResponse, - UserOpStatus -} from "./utils/Types.js" -import { deepHexlify } from "./utils/Utils.js" - -/** - * This class implements IBundler interface. - * Implementation sends UserOperation to a bundler URL as per ERC4337 standard. - * Checkout the proposal for more details on Bundlers. - */ -export class Bundler implements IBundler { - private bundlerConfig: BundlerConfigWithChainId - - // eslint-disable-next-line no-unused-vars - UserOpReceiptIntervals!: { [key in number]?: number } - - UserOpWaitForTxHashIntervals!: { [key in number]?: number } - - UserOpReceiptMaxDurationIntervals!: { [key in number]?: number } - - UserOpWaitForTxHashMaxDurationIntervals!: { [key in number]?: number } - - private publicClient: PublicClient - - constructor(bundlerConfig: BundlerConfig) { - const parsedChainId: number = bundlerConfig.chain.id - // || extractChainIdFromBundlerUrl(bundlerConfig.bundlerUrl) - this.bundlerConfig = { ...bundlerConfig, chainId: parsedChainId } - - this.publicClient = createPublicClient({ - chain: bundlerConfig.chain, - transport: http() - }) - - this.UserOpReceiptIntervals = { - ...UserOpReceiptIntervals, - ...bundlerConfig.userOpReceiptIntervals - } - - this.UserOpWaitForTxHashIntervals = { - ...UserOpWaitForTxHashIntervals, - ...bundlerConfig.userOpWaitForTxHashIntervals - } - - this.UserOpReceiptMaxDurationIntervals = { - ...UserOpReceiptMaxDurationIntervals, - ...bundlerConfig.userOpReceiptMaxDurationIntervals - } - - this.UserOpWaitForTxHashMaxDurationIntervals = { - ...UserOpWaitForTxHashMaxDurationIntervals, - ...bundlerConfig.userOpWaitForTxHashMaxDurationIntervals - } - - this.bundlerConfig.entryPointAddress = - bundlerConfig.entryPointAddress || contracts.entryPoint.address - } - - public getBundlerUrl(): string { - return `${this.bundlerConfig.bundlerUrl}` - } - - /** - * @param userOpHash - * @description This function will fetch gasPrices from bundler - * @returns Promise - */ - async estimateUserOpGas( - _userOp: UserOperationStruct - ): Promise { - const bundlerUrl = this.getBundlerUrl() - - const response: { - result: BundlerEstimateUserOpGasResponse - error: { message: string } - } = await sendRequest( - { - url: bundlerUrl, - method: HttpMethod.Post, - body: { - method: "eth_estimateUserOperationGas", - params: [deepHexlify(_userOp), this.bundlerConfig.entryPointAddress], - id: getTimestampInSeconds(), - jsonrpc: "2.0" - } - }, - "Bundler" - ) - const userOpGasResponse = response - for (const key in userOpGasResponse.result) { - if ( - userOpGasResponse.result[key as keyof UserOpGasResponse] === null || - userOpGasResponse.result[key as keyof UserOpGasResponse] === undefined - ) { - throw new Error(`Got undefined ${key} from bundler`) - } - } - - if (isNullOrUndefined(response.result)) { - const decodedError = decodeUserOperationError( - JSON.stringify(response?.error?.message) - ) - throw new Error(`Error from Bundler: ${decodedError}`) - } - - return { - preVerificationGas: BigInt(response.result.preVerificationGas || 0), - verificationGasLimit: BigInt(response.result.verificationGasLimit || 0), - callGasLimit: BigInt(response.result.callGasLimit || 0), - paymasterVerificationGasLimit: response.result - .paymasterVerificationGasLimit - ? BigInt(response.result.paymasterVerificationGasLimit) - : undefined, - paymasterPostOpGasLimit: response.result.paymasterPostOpGasLimit - ? BigInt(response.result.paymasterPostOpGasLimit) - : undefined - } - } - - /** - * - * @param userOp - * @description This function will send signed userOp to bundler to get mined on chain - * @returns Promise - */ - async sendUserOp(_userOp: UserOperationStruct): Promise { - const chainId = this.bundlerConfig.chainId - const params = [deepHexlify(_userOp), this.bundlerConfig.entryPointAddress] - const bundlerUrl = this.getBundlerUrl() - const sendUserOperationResponse: { - result: Hash - error: { message: string } - } = await sendRequest( - { - url: bundlerUrl, - method: HttpMethod.Post, - body: { - method: "eth_sendUserOperation", - params: params, - id: getTimestampInSeconds(), - jsonrpc: "2.0" - } - }, - "Bundler" - ) - - if (isNullOrUndefined(sendUserOperationResponse.result)) { - throw new Error(sendUserOperationResponse.error.message) - } - - return { - userOpHash: sendUserOperationResponse.result, - wait: (confirmations?: number): Promise => { - // Note: maxDuration can be defined per chainId - const maxDuration = - this.UserOpReceiptMaxDurationIntervals[chainId] || 50000 // default 50 seconds - let totalDuration = 0 - - return new Promise((resolve, reject) => { - const intervalValue = this.UserOpReceiptIntervals[chainId] || 5000 // default 5 seconds - const intervalId = setInterval(async () => { - try { - const userOpResponse = await this.getUserOpReceipt( - sendUserOperationResponse.result - ) - if (userOpResponse?.receipt?.blockNumber) { - if (confirmations) { - const latestBlock = await this.publicClient.getBlockNumber() - const confirmedBlocks = - BigInt(latestBlock) - - BigInt(userOpResponse.receipt.blockNumber) - if (confirmations >= confirmedBlocks) { - clearInterval(intervalId) - resolve(userOpResponse) - return - } - } else { - clearInterval(intervalId) - resolve(userOpResponse) - return - } - } - } catch (error) { - clearInterval(intervalId) - reject(error) - return - } - - totalDuration += intervalValue - if (totalDuration >= maxDuration) { - clearInterval(intervalId) - reject( - new Error( - `Exceeded maximum duration (${ - maxDuration / 1000 - } sec) waiting to get receipt for userOpHash ${ - sendUserOperationResponse.result - }. Try getting the receipt manually using eth_getUserOperationReceipt rpc method on bundler` - ) - ) - } - }, intervalValue) - }) - } - } - } - - /** - * - * @param userOpHash - * @description This function will return userOpReceipt for a given userOpHash - * @returns Promise - */ - async getUserOpReceipt(userOpHash: string): Promise { - const bundlerUrl = this.getBundlerUrl() - const response: GetUserOperationReceiptResponse = await sendRequest( - { - url: bundlerUrl, - method: HttpMethod.Post, - body: { - method: "eth_getUserOperationReceipt", - params: [userOpHash], - id: getTimestampInSeconds(), - jsonrpc: "2.0" - } - }, - "Bundler" - ) - - if (isNullOrUndefined(response.result)) { - throw new Error(response?.error?.message) - } - - const userOpReceipt: UserOpReceipt = response.result - return userOpReceipt - } - - /** - * - * @param userOpHash - * @description This function will return userOpReceipt for a given userOpHash - * @returns Promise - */ - async getUserOpStatus(userOpHash: string): Promise { - const bundlerUrl = this.getBundlerUrl() - const response: GetUserOperationStatusResponse = await sendRequest( - { - url: bundlerUrl, - method: HttpMethod.Post, - body: { - method: "biconomy_getUserOperationStatus", - params: [userOpHash], - id: getTimestampInSeconds(), - jsonrpc: "2.0" - } - }, - "Bundler" - ) - - if (isNullOrUndefined(response.result)) { - throw new Error(response?.error?.message) - } - - const userOpStatus: UserOpStatus = response.result - return userOpStatus - } - - /** - * - * @param userOpHash - * @description this function will return UserOpByHashResponse for given UserOpHash - * @returns Promise - */ - async getUserOpByHash(userOpHash: string): Promise { - const bundlerUrl = this.getBundlerUrl() - const response: GetUserOpByHashResponse = await sendRequest( - { - url: bundlerUrl, - method: HttpMethod.Post, - body: { - method: "eth_getUserOperationByHash", - params: [userOpHash], - id: getTimestampInSeconds(), - jsonrpc: "2.0" - } - }, - "Bundler" - ) - - if (isNullOrUndefined(response.result)) { - throw new Error(response?.error?.message) - } - - const userOpByHashResponse: UserOpByHashResponse = response.result - return userOpByHashResponse - } - - /** - * @description This function will return the gas fee values - */ - async getGasFeeValues(): Promise { - const bundlerUrl = this.getBundlerUrl() - const response: { - result: GetUserOperationGasPriceReturnType - error: Error - } = await sendRequest( - { - url: bundlerUrl, - method: HttpMethod.Post, - body: { - method: process.env.BUNDLER_URL?.includes("pimlico") - ? "pimlico_getUserOperationGasPrice" - : "biconomy_getGasFeeValues", - params: [], - id: getTimestampInSeconds(), - jsonrpc: "2.0" - } - }, - "Bundler" - ) - - if (isNullOrUndefined(response.result)) { - throw new Error(response?.error?.message) - } - - return { - slow: { - maxFeePerGas: BigInt(response.result.slow.maxFeePerGas), - maxPriorityFeePerGas: BigInt(response.result.slow.maxPriorityFeePerGas) - }, - standard: { - maxFeePerGas: BigInt(response.result.standard.maxFeePerGas), - maxPriorityFeePerGas: BigInt( - response.result.standard.maxPriorityFeePerGas - ) - }, - fast: { - maxFeePerGas: BigInt(response.result.fast.maxFeePerGas), - maxPriorityFeePerGas: BigInt(response.result.fast.maxPriorityFeePerGas) - } - } - } - - public static async create(config: BundlerConfig): Promise { - return new Bundler(config) - } -} diff --git a/src/bundler/index.ts b/src/bundler/index.ts deleted file mode 100644 index 5986083f4..000000000 --- a/src/bundler/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Bundler } from "./Bundler.js" - -export * from "./interfaces/IBundler.js" -export * from "./Bundler.js" -export * from "./utils/Utils.js" -export * from "./utils/Types.js" - -export const createBundler = Bundler.create diff --git a/src/bundler/interfaces/IBundler.ts b/src/bundler/interfaces/IBundler.ts deleted file mode 100644 index bb465ee15..000000000 --- a/src/bundler/interfaces/IBundler.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { StateOverrideSet, UserOperationStruct } from "../../account" -import type { - GetUserOperationGasPriceReturnType, - UserOpByHashResponse, - UserOpGasResponse, - UserOpReceipt, - UserOpResponse, - UserOpStatus -} from "../utils/Types.js" - -export interface IBundler { - estimateUserOpGas( - _userOp: Partial, - stateOverrideSet?: StateOverrideSet - ): Promise - sendUserOp(_userOp: UserOperationStruct): Promise - getUserOpReceipt(_userOpHash: string): Promise - getUserOpByHash(_userOpHash: string): Promise - getGasFeeValues(): Promise - getUserOpStatus(_userOpHash: string): Promise - getBundlerUrl(): string -} diff --git a/src/bundler/utils/Constants.ts b/src/bundler/utils/Constants.ts deleted file mode 100644 index c615713ab..000000000 --- a/src/bundler/utils/Constants.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { concat, pad } from "viem" - -// define mode and exec type enums -export const CALLTYPE_SINGLE = "0x00" // 1 byte -export const CALLTYPE_BATCH = "0x01" // 1 byte -export const EXECTYPE_DEFAULT = "0x00" // 1 byte -export const EXECTYPE_TRY = "0x01" // 1 byte -export const EXECTYPE_DELEGATE = "0xFF" // 1 byte -export const MODE_DEFAULT = "0x00000000" // 4 bytes -export const UNUSED = "0x00000000" // 4 bytes -export const MODE_PAYLOAD = "0x00000000000000000000000000000000000000000000" // 22 bytes -export const ERC1271_MAGICVALUE = "0x1626ba7e" -export const ERC1271_INVALID = "0xffffffff" -export const GENERIC_FALLBACK_SELECTOR = "0xcb5baf0f" - -export const UserOpReceiptIntervals: { [key in number]?: number } = { - [1]: 10000 -} - -// Note: Default value is 500(0.5sec) -export const UserOpWaitForTxHashIntervals: { [key in number]?: number } = { - [1]: 1000 -} - -// Note: Default value is 30000 (30sec) -export const UserOpReceiptMaxDurationIntervals: { [key in number]?: number } = { - [1]: 300000, - [11155111]: 50000, - [137]: 60000, - [56]: 50000, - [97]: 50000, - [421613]: 50000, - [42161]: 50000, - [59140]: 50000 // linea testnet -} - -// Note: Default value is 20000 (20sec) -export const UserOpWaitForTxHashMaxDurationIntervals: { - [key in number]?: number -} = { - [1]: 20000 -} - -export const SDK_VERSION = "4.4.5" - -export const EXECUTE_SINGLE = concat([ - CALLTYPE_SINGLE, - EXECTYPE_DEFAULT, - MODE_DEFAULT, - UNUSED, - MODE_PAYLOAD -]) - -export const EXECUTE_BATCH = concat([ - CALLTYPE_BATCH, - EXECTYPE_DEFAULT, - MODE_DEFAULT, - UNUSED, - MODE_PAYLOAD -]) - -export const ACCOUNT_MODES = { - DEFAULT_SINGLE: concat([ - pad(EXECTYPE_DEFAULT, { size: 1 }), - pad(CALLTYPE_SINGLE, { size: 1 }), - pad(UNUSED, { size: 4 }), - pad(MODE_DEFAULT, { size: 4 }), - pad(MODE_PAYLOAD, { size: 22 }) - ]), - DEFAULT_BATCH: concat([ - pad(EXECTYPE_DEFAULT, { size: 1 }), - pad(CALLTYPE_BATCH, { size: 1 }), - pad(UNUSED, { size: 4 }), - pad(MODE_DEFAULT, { size: 4 }), - pad(MODE_PAYLOAD, { size: 22 }) - ]), - TRY_BATCH: concat([ - pad(EXECTYPE_TRY, { size: 1 }), - pad(CALLTYPE_BATCH, { size: 1 }), - pad(UNUSED, { size: 4 }), - pad(MODE_DEFAULT, { size: 4 }), - pad(MODE_PAYLOAD, { size: 22 }) - ]), - TRY_SINGLE: concat([ - pad(EXECTYPE_TRY, { size: 1 }), - pad(CALLTYPE_SINGLE, { size: 1 }), - pad(UNUSED, { size: 4 }), - pad(MODE_DEFAULT, { size: 4 }), - pad(MODE_PAYLOAD, { size: 22 }) - ]), - DELEGATE_SINGLE: concat([ - pad(EXECTYPE_DELEGATE, { size: 1 }), - pad(CALLTYPE_SINGLE, { size: 1 }), - pad(UNUSED, { size: 4 }), - pad(MODE_DEFAULT, { size: 4 }), - pad(MODE_PAYLOAD, { size: 22 }) - ]) -} diff --git a/src/bundler/utils/HelperFunction.ts b/src/bundler/utils/HelperFunction.ts deleted file mode 100644 index cf96b536c..000000000 --- a/src/bundler/utils/HelperFunction.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Will convert the userOp hex, bigInt and number values to hex strings -// export const transformUserOP = ( -// userOp: UserOperationStruct -// ): UserOperationStruct => { -// try { -// const userOperation = { ...userOp } -// const keys: (keyof UserOperationStruct)[] = [ -// "nonce", -// "callGasLimit", -// "verificationGasLimit", -// "preVerificationGas", -// "maxFeePerGas", -// "maxPriorityFeePerGas" -// ] -// for (const key of keys) { -// if (userOperation[key] && userOperation[key] !== "0x") { -// userOperation[key] = `0x${BigInt(userOp[key] as BigNumberish).toString( -// 16 -// )}` as `0x${string}` -// } -// } -// return userOperation -// } catch (error) { -// throw `Failed to transform user operation: ${error}` -// } -// } - -/** - * @description this function will return current timestamp in seconds - * @returns Number - */ -export const getTimestampInSeconds = (): number => { - return Math.floor(Date.now() / 1000) -} - -export function decodeUserOperationError(errorFromBundler: string) { - const prefix = "UserOperation reverted during simulation with reason: " - if (errorFromBundler.includes(prefix)) { - const errorCode = errorFromBundler - .slice(prefix.length) - .trim() - .replace(/"/g, "") - return decodeErrorCode(errorCode) - } - return errorFromBundler // Return original error if it doesn't match the expected format -} - -function decodeErrorCode(errorCode: string) { - const errorMap: { [key: string]: string } = { - "0xe7190273": - "NotSortedAndUnique: The owners array must contain unique addresses.", - "0xf91bd6f1000000000000000000000000da6959da394b1bddb068923a9a214dc0cd193d2e": - "NotInitialized: The module is not initialized on this smart account.", - "0xaabd5a09": - "InvalidThreshold: The threshold must be greater than or equal to the number of owners.", - "0x71448bfe000000000000000000000000bf2137a23f439ca5aa4360cc6970d70b24d07ea2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0": - "WrongContractSignatureFormat", - "0x40d3d1a40000000000000000000000004d8249d21c9553b1bd23cabf611011376dd3416a": - "LinkedList_EntryAlreadyInList", - "0x40d3d1a40000000000000000000000004b8306128aed3d49a9d17b99bf8082d4e406fa1f": - "LinkedList_EntryAlreadyInList", - "0x40d3d1a4000000000000000000000000d98238bbaea4f91683d250003799ead31d7f5c55": - "Error: Custom error message about the K1Validator contract" - // Add more error codes and their corresponding human-readable messages here - } - const decodedError = errorMap[errorCode] || errorCode - return `User operation reverted during simulation with reason: ${decodedError}` -} diff --git a/src/bundler/utils/Types.ts b/src/bundler/utils/Types.ts deleted file mode 100644 index 53688a28d..000000000 --- a/src/bundler/utils/Types.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { Address, Chain, Hash, Hex, Log } from "viem" -import type { UserOperationStruct } from "../../account" - -export type BundlerConfig = { - bundlerUrl: string - entryPointAddress?: string - chain: Chain - // eslint-disable-next-line no-unused-vars - userOpReceiptIntervals?: { [key in number]?: number } - userOpWaitForTxHashIntervals?: { [key in number]?: number } - userOpReceiptMaxDurationIntervals?: { [key in number]?: number } - userOpWaitForTxHashMaxDurationIntervals?: { [key in number]?: number } -} -export type BundlerConfigWithChainId = BundlerConfig & { chainId: number } - -export type TStatus = "success" | "reverted" - -export type UserOpReceiptTransaction = { - transactionHash: Hex - transactionIndex: bigint - blockHash: Hash - blockNumber: bigint - from: Address - to: Address | null - cumulativeGasUsed: bigint - status: TStatus - gasUsed: bigint - contractAddress: Address | null - logsBloom: Hex - effectiveGasPrice: bigint -} - -export type UserOpReceipt = { - userOpHash: Hash - entryPoint: Address - sender: Address - nonce: bigint - paymaster?: Address - actualGasUsed: bigint - actualGasCost: bigint - success: boolean - reason?: string - receipt: UserOpReceiptTransaction - logs: Log[] -} - -// review -export type UserOpStatus = { - state: string // for now // could be an enum - transactionHash?: string - userOperationReceipt?: UserOpReceipt -} - -// Converted to JsonRpcResponse with strict type -export type GetUserOperationReceiptResponse = { - jsonrpc: string - id: number - result: UserOpReceipt - error?: JsonRpcError -} - -export type GetUserOperationStatusResponse = { - jsonrpc: string - id: number - result: UserOpStatus - error?: JsonRpcError -} - -// Converted to JsonRpcResponse with strict type -export type SendUserOpResponse = { - jsonrpc: string - id: number - result: string - error?: JsonRpcError -} - -export type UserOpResponse = { - userOpHash: Hash - wait(_confirmations?: number): Promise -} - -// Converted to JsonRpcResponse with strict type -export type EstimateUserOpGasResponse = { - jsonrpc: string - id: number - result: UserOpGasResponse - error?: JsonRpcError -} - -export type UserOpGasResponse = { - preVerificationGas: bigint - verificationGasLimit: bigint - callGasLimit: bigint - paymasterVerificationGasLimit?: bigint - paymasterPostOpGasLimit?: bigint -} - -// Converted to JsonRpcResponse with strict type -export type GetUserOpByHashResponse = { - jsonrpc: string - id: number - result: UserOpByHashResponse - error?: JsonRpcError -} - -export type UserOpByHashResponse = UserOperationStruct & { - transactionHash: string - blockNumber: number - blockHash: string - entryPoint: string -} -/* eslint-disable @typescript-eslint/no-explicit-any */ -export type JsonRpcError = { - code: string - message: string - data: any -} - -export type GetGasFeeValuesResponse = { - jsonrpc: string - id: number - result: GasFeeValues - error?: JsonRpcError -} -export type GasFeeValues = { - maxPriorityFeePerGas: bigint - maxFeePerGas: bigint -} - -export type GetUserOperationGasPriceReturnType = { - slow: { - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - } - standard: { - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - } - fast: { - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - } -} - -export type BundlerEstimateUserOpGasResponse = { - preVerificationGas: Hex - verificationGasLimit: Hex - callGasLimit?: Hex | null - paymasterVerificationGasLimit?: Hex | null - paymasterPostOpGasLimit?: Hex | null -} diff --git a/src/bundler/utils/Utils.ts b/src/bundler/utils/Utils.ts deleted file mode 100644 index a5971ff93..000000000 --- a/src/bundler/utils/Utils.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { toHex } from "viem" - -export const extractChainIdFromBundlerUrl = (url: string): number => { - try { - const regex = /\/api\/v[2-3]\/(\d+)\/[a-zA-Z0-9.-]+$/ - // biome-ignore lint/style/noNonNullAssertion: - const match = regex.exec(url)! - return Number.parseInt(match[1]) - } catch (error) { - throw new Error("Invalid chain id") - } -} - -export const extractChainIdFromPaymasterUrl = (url: string): number => { - try { - const regex = /\/api\/v\d+\/(\d+)\// - const match = regex.exec(url) - if (!match) { - throw new Error("Invalid URL format") - } - return Number.parseInt(match[1]) - } catch (error) { - throw new Error("Invalid chain id") - } -} - -export function deepHexlify(obj: any): any { - if (typeof obj === "function") { - return undefined - } - if (obj == null || typeof obj === "string" || typeof obj === "boolean") { - return obj - } - - if (typeof obj === "bigint") { - return toHex(obj) - } - - if (obj._isBigNumber != null || typeof obj !== "object") { - return toHex(obj).replace(/^0x0/, "0x") - } - if (Array.isArray(obj)) { - return obj.map((member) => deepHexlify(member)) - } - return Object.keys(obj).reduce( - // biome-ignore lint/suspicious/noExplicitAny: it's a recursive function, so it's hard to type - (set: any, key: string) => { - set[key] = deepHexlify(obj[key]) - return set - }, - {} - ) -} diff --git a/src/modules/validators/K1ValidatorModule.ts b/src/modules/validators/K1ValidatorModule.ts deleted file mode 100644 index 19b26a282..000000000 --- a/src/modules/validators/K1ValidatorModule.ts +++ /dev/null @@ -1,24 +0,0 @@ -import addresses from "../../__contracts/addresses.js" -import type { SmartAccountSigner } from "../../account/index.js" -import { BaseValidationModule } from "../base/BaseValidationModule.js" -import type { Module } from "../utils/Types.js" - -export class K1ValidatorModule extends BaseValidationModule { - private constructor(moduleConfig: Module, signer: SmartAccountSigner) { - super(moduleConfig, signer) - } - - public static async create( - signer: SmartAccountSigner, - k1ValidatorAddress = addresses.K1Validator - ): Promise { - const module: Module = { - moduleAddress: k1ValidatorAddress, - type: "validator", - data: await signer.getAddress(), - additionalContext: "0x" - } - const instance = new K1ValidatorModule(module, signer) - return instance - } -} diff --git a/src/modules/validators/ValidationModule.ts b/src/modules/validators/ValidationModule.ts deleted file mode 100644 index 8390243a0..000000000 --- a/src/modules/validators/ValidationModule.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Address, Hex } from "viem" -import type { SmartAccountSigner } from "../../account/index.js" -import { BaseValidationModule } from "../base/BaseValidationModule.js" -import type { Module } from "../utils/Types.js" - -export class ValidationModule extends BaseValidationModule { - private constructor(moduleConfig: Module, signer: SmartAccountSigner) { - super(moduleConfig, signer) - } - - public static async create( - signer: SmartAccountSigner, - moduleAddress: Address, - data: Hex - ): Promise { - const module: Module = { - moduleAddress, - type: "validator", - data, - additionalContext: "0x" - } - const instance = new ValidationModule(module, signer) - return instance - } -} diff --git a/src/paymaster/Paymaster.ts b/src/paymaster/Paymaster.ts deleted file mode 100644 index f833ceee3..000000000 --- a/src/paymaster/Paymaster.ts +++ /dev/null @@ -1,410 +0,0 @@ -import { encodeFunctionData, parseAbi } from "viem" -import { - type BiconomyTokenPaymasterRequest, - HttpMethod, - Logger, - type Transaction, - type UserOperationStruct, - sendRequest -} from "../account/index.js" -import { deepHexlify } from "../bundler/index.js" -import type { IHybridPaymaster } from "./interfaces/IHybridPaymaster.js" -import { ADDRESS_ZERO, ERC20_ABI, MAX_UINT256 } from "./utils/Constants.js" -import { getTimestampInSeconds } from "./utils/Helpers.js" -import type { - FeeQuotesOrDataDto, - FeeQuotesOrDataResponse, - Hex, - JsonRpcResponse, - PaymasterAndDataResponse, - PaymasterConfig, - PaymasterFeeQuote, - SponsorUserOperationDto -} from "./utils/Types.js" - -const defaultPaymasterConfig: PaymasterConfig = { - paymasterUrl: "", - strictMode: false // Set your desired default value for strictMode here -} -/** - * @dev Hybrid - Generic Gas Abstraction paymaster - */ -export class Paymaster implements IHybridPaymaster { - paymasterConfig: PaymasterConfig - - constructor(config: PaymasterConfig) { - const mergedConfig: PaymasterConfig = { - ...defaultPaymasterConfig, - ...config - } - this.paymasterConfig = mergedConfig - } - - /** - * @dev Prepares the user operation by resolving properties and converting certain values to hexadecimal format. - * @param userOp The partial user operation. - * @returns A Promise that resolves to the prepared partial user operation. - */ - // private async prepareUserOperation( - // userOp: Partial - // ): Promise> { - // const userOperation = { ...userOp } - // try { - // const keys1: (keyof UserOperationStruct)[] = [ - // "nonce", - // "maxFeePerGas", - // "maxPriorityFeePerGas" - // ] - // for (const key of keys1) { - // if (userOperation[key] && userOperation[key] !== "0x") { - // userOperation[key] = `0x${BigInt( - // userOp[key] as BigNumberish - // ).toString(16)}` as `0x${string}` - // } - // } - // const keys2: (keyof UserOperationStruct)[] = [ - // "callGasLimit", - // "verificationGasLimit", - // "preVerificationGas" - // ] - // for (const key of keys2) { - // if (userOperation[key] && userOperation[key] !== "0x") { - // userOperation[key] = BigInt( - // userOp[key] as BigNumberish - // ).toString() as `0x${string}` - // } - // } - // } catch (error) { - // throw `Failed to transform user operation: ${error}` - // } - // userOperation.signature = userOp.signature || "0x" - // userOperation.paymasterAndData = userOp.paymasterAndData || "0x" - // return userOperation - // } - - /** - * @dev Builds a token approval transaction for the Biconomy token paymaster. - * @param tokenPaymasterRequest The token paymaster request data. This will include information about chosen feeQuote, spender address and optional flag to provide maxApproval - * @param provider Optional provider object. - * @returns A Promise that resolves to the built transaction object. - */ - async buildTokenApprovalTransaction( - tokenPaymasterRequest: BiconomyTokenPaymasterRequest - ): Promise { - const feeTokenAddress: string = tokenPaymasterRequest.feeQuote.tokenAddress - - const spender = tokenPaymasterRequest.spender - - // logging provider object isProvider - // Logger.log("provider object passed - is provider", provider?._isProvider); - - // TODO move below notes to separate method - // Note: should also check in caller if the approval is already given, if yes return object with address or data 0 - // Note: we would need userOp here to get the account/owner info to check allowance - - let requiredApproval = BigInt(0) - - if ( - tokenPaymasterRequest.maxApproval && - tokenPaymasterRequest.maxApproval === true - ) { - requiredApproval = BigInt(MAX_UINT256) - } else { - requiredApproval = BigInt( - Math.ceil( - tokenPaymasterRequest.feeQuote.maxGasFee * - 10 ** tokenPaymasterRequest.feeQuote.decimal - ) - ) - } - - try { - const parsedAbi = parseAbi(ERC20_ABI) - const data = encodeFunctionData({ - abi: parsedAbi, - functionName: "approve", - args: [spender, requiredApproval] - }) - - // TODO? - // Note: For some tokens we may need to set allowance to 0 first so that would return batch of transactions and changes the return type to Transaction[] - // In that case we would return two objects in an array, first of them being.. - /* - { - to: erc20.address, - value: ethers.BigNumber.from(0), - data: erc20.interface.encodeFunctionData('approve', [spender, BigNumber.from("0")]) - } - */ - - // const zeroValue: ethers.BigNumber = ethers.BigNumber.from(0); - // const value: BigNumberish | undefined = zeroValue as any; - return { - to: feeTokenAddress, - value: "0x00", - data: data - } - } catch (error) { - throw new Error("Failed to encode function data") - } - } - - /** - * @dev Retrieves paymaster fee quotes or data based on the provided user operation and paymaster service data. - * @param userOp The partial user operation. - * @param paymasterServiceData The paymaster service data containing token information and sponsorship details. Devs can send just the preferred token or array of token addresses in case of mode "ERC20" and sartAccountInfo in case of "sponsored" mode. - * @returns A Promise that resolves to the fee quotes or data response. - */ - async getPaymasterFeeQuotesOrData( - userOp: Partial, - paymasterServiceData: FeeQuotesOrDataDto - ): Promise { - // const userOp = await this.prepareUserOperation(_userOp) - - let mode: "SPONSORED" | "ERC20" | null = null - let expiryDuration: number | null = null - const calculateGasLimits = paymasterServiceData.calculateGasLimits ?? true - let preferredToken: string | null = null - let feeTokensArray: string[] = [] - // could make below null - let smartAccountInfo = { - name: "BICONOMY", - version: "2.0.0" - } - let webhookData: Record | null = null - - if (paymasterServiceData.mode) { - mode = paymasterServiceData.mode - // Validation on the mode passed / define allowed enums - } - - if (paymasterServiceData.expiryDuration) { - expiryDuration = paymasterServiceData.expiryDuration - } - - preferredToken = paymasterServiceData?.preferredToken - ? paymasterServiceData?.preferredToken - : preferredToken - - feeTokensArray = ( - paymasterServiceData?.tokenList?.length !== 0 - ? paymasterServiceData?.tokenList - : feeTokensArray - ) as string[] - - webhookData = paymasterServiceData?.webhookData ?? webhookData - - smartAccountInfo = - paymasterServiceData?.smartAccountInfo ?? smartAccountInfo - - try { - const response: JsonRpcResponse = await sendRequest( - { - url: `${this.paymasterConfig.paymasterUrl}`, - method: HttpMethod.Post, - body: { - method: "pm_getFeeQuoteOrData", - params: [ - userOp, - { - ...(mode !== null && { mode }), - calculateGasLimits: calculateGasLimits, - ...(expiryDuration !== null && { expiryDuration }), - tokenInfo: { - tokenList: feeTokensArray, - ...(preferredToken !== null && { preferredToken }) - }, - sponsorshipInfo: { - ...(webhookData !== null && { webhookData }), - smartAccountInfo: smartAccountInfo - } - } - ], // As per current API - id: getTimestampInSeconds(), - jsonrpc: "2.0" - } - }, - "Paymaster" - ) - - if (response?.result) { - if (response.result.mode === "ERC20") { - const feeQuotesResponse: Array = - response.result.feeQuotes - const paymasterAddress: Hex = response.result.paymasterAddress - // check all objects iterate and populate below calculation for all tokens - return { - feeQuotes: feeQuotesResponse, - tokenPaymasterAddress: paymasterAddress - } - } - if (response.result.mode === "SPONSORED") { - const paymasterAndData: Hex = response.result.paymasterAndData - const preVerificationGas = response.result.preVerificationGas - const verificationGasLimit = response.result.verificationGasLimit - const callGasLimit = response.result.callGasLimit - return { - paymasterAndData: paymasterAndData, - preVerificationGas: preVerificationGas, - verificationGasLimit: verificationGasLimit, - callGasLimit: callGasLimit - } - } - const errorObject = { - code: 417, - message: - "Expectation Failed: Invalid mode in Paymaster service response" - } - throw errorObject - } - } catch (error: any) { - Logger.error( - "Failed to fetch Fee Quotes or Paymaster data - reason: ", - JSON.stringify(error) - ) - // Note: we may not throw if we include strictMode off and return paymasterData '0x'. - if ( - !this.paymasterConfig.strictMode && - paymasterServiceData.mode === "SPONSORED" && - (error?.message.includes("Smart contract data not found") || - error?.message.includes("No policies were set")) - // can also check based on error.code being -32xxx - ) { - Logger.warn( - `Strict mode is ${this.paymasterConfig.strictMode}. sending paymasterAndData 0x` - ) - return { - paymasterAndData: "0x", - // send below values same as userOp gasLimits - preVerificationGas: userOp.preVerificationGas, - verificationGasLimit: userOp.verificationGasLimit, - callGasLimit: userOp.callGasLimit - } - } - throw error - } - throw new Error("Failed to fetch feeQuote or paymaster data") - } - - /** - * @dev Retrieves the paymaster and data based on the provided user operation and paymaster service data. - * @param userOp The partial user operation. - * @param paymasterServiceData Optional paymaster service data. - * @returns A Promise that resolves to the paymaster and data string. - */ - async getPaymasterAndData( - userOp: Partial, - paymasterServiceData?: SponsorUserOperationDto // mode is necessary. partial context of token paymaster or verifying - ): Promise { - // const userOp = await this.prepareUserOperation(_userOp) - - if (paymasterServiceData?.mode === undefined) { - throw new Error("mode is required in paymasterServiceData") - } - - const mode = paymasterServiceData.mode - - const calculateGasLimits = paymasterServiceData.calculateGasLimits ?? true - - let tokenInfo: Record | null = null - // could make below null - let smartAccountInfo = { - name: "BICONOMY", - version: "2.0.0" - } - let webhookData: Record | null = null - let expiryDuration: number | null = null - - if (mode === "ERC20") { - if ( - !paymasterServiceData?.feeTokenAddress && - paymasterServiceData?.feeTokenAddress === ADDRESS_ZERO - ) { - throw new Error("feeTokenAddress is required and should be non-zero") - } - tokenInfo = { - feeTokenAddress: paymasterServiceData.feeTokenAddress - } - } - - webhookData = paymasterServiceData?.webhookData ?? webhookData - smartAccountInfo = - paymasterServiceData?.smartAccountInfo ?? smartAccountInfo - expiryDuration = paymasterServiceData?.expiryDuration ?? expiryDuration - - // Note: The idea is before calling this below rpc, userOp values presense and types should be in accordance with how we call eth_estimateUseropGas on the bundler - - const hexlifiedUserOp = deepHexlify(userOp) - - try { - const response: JsonRpcResponse = await sendRequest( - { - url: `${this.paymasterConfig.paymasterUrl}`, - method: HttpMethod.Post, - body: { - method: "pm_sponsorUserOperation", - params: [ - hexlifiedUserOp, - { - mode: mode, - calculateGasLimits: calculateGasLimits, - ...(expiryDuration !== null && { expiryDuration }), - ...(tokenInfo !== null && { tokenInfo }), - sponsorshipInfo: { - ...(webhookData !== null && { webhookData }), - smartAccountInfo: smartAccountInfo - } - } - ], - id: getTimestampInSeconds(), - jsonrpc: "2.0" - } - }, - "Paymaster" - ) - - if (response?.result) { - const paymasterAndData = response.result.paymasterAndData - const preVerificationGas = - response.result.preVerificationGas ?? userOp.preVerificationGas - const verificationGasLimit = - response.result.verificationGasLimit ?? userOp.verificationGasLimit - const callGasLimit = response.result.callGasLimit ?? userOp.callGasLimit - return { - paymasterAndData: paymasterAndData, - preVerificationGas: preVerificationGas, - verificationGasLimit: verificationGasLimit, - callGasLimit: callGasLimit - } - } - // biome-ignore lint/suspicious/noExplicitAny: caught error is any - } catch (error: any) { - Logger.error( - "Error in generating paymasterAndData - reason: ", - JSON.stringify(error) - ) - throw error - } - throw new Error("Error in generating paymasterAndData") - } - - /** - * - * @param userOp user operation - * @param paymasterServiceData optional extra information to be passed to paymaster service - * @returns "0x" - */ - async getDummyPaymasterAndData( - _userOp: Partial, - _paymasterServiceData?: SponsorUserOperationDto // mode is necessary. partial context of token paymaster or verifying - ): Promise { - return "0x" - } - - public static async create(config: PaymasterConfig): Promise { - return new Paymaster(config) - } -} - -export const toPaymaster = Paymaster.create -export default toPaymaster diff --git a/src/paymaster/index.ts b/src/paymaster/index.ts deleted file mode 100644 index 70cbb99d8..000000000 --- a/src/paymaster/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Paymaster } from "./Paymaster.js" -export * from "./interfaces/IPaymaster.js" -export * from "./interfaces/IHybridPaymaster.js" -export * from "./utils/Types.js" -export * from "./Paymaster.js" - -export const createPaymaster = Paymaster.create diff --git a/src/paymaster/interfaces/IHybridPaymaster.ts b/src/paymaster/interfaces/IHybridPaymaster.ts deleted file mode 100644 index 104029e0a..000000000 --- a/src/paymaster/interfaces/IHybridPaymaster.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { UserOperationStruct } from "../../account" -import type { - BiconomyTokenPaymasterRequest, - Transaction -} from "../../account/utils/Types.js" -import type { - FeeQuotesOrDataDto, - FeeQuotesOrDataResponse, - PaymasterAndDataResponse -} from "../utils/Types.js" -import type { IPaymaster } from "./IPaymaster.js" - -export interface IHybridPaymaster extends IPaymaster { - getPaymasterAndData( - _userOp: Partial, - _paymasterServiceData?: T - ): Promise - getDummyPaymasterAndData( - _userOp: Partial, - _paymasterServiceData?: T - ): Promise - buildTokenApprovalTransaction( - _tokenPaymasterRequest: BiconomyTokenPaymasterRequest - ): Promise - getPaymasterFeeQuotesOrData( - _userOp: Partial, - _paymasterServiceData: FeeQuotesOrDataDto - ): Promise -} diff --git a/src/paymaster/interfaces/IPaymaster.ts b/src/paymaster/interfaces/IPaymaster.ts deleted file mode 100644 index 3aee820b2..000000000 --- a/src/paymaster/interfaces/IPaymaster.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { UserOperationStruct } from "../../account" -import type { PaymasterAndDataResponse } from "../utils/Types.js" - -export interface IPaymaster { - // Implementing class may add extra parameter (for example paymasterServiceData with it's own type) in below function signature - getPaymasterAndData( - _userOp: Partial - ): Promise - getDummyPaymasterAndData( - _userOp: Partial - ): Promise -} diff --git a/src/paymaster/utils/Constants.ts b/src/paymaster/utils/Constants.ts deleted file mode 100644 index a148d17ad..000000000 --- a/src/paymaster/utils/Constants.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const MAX_UINT256 = - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" -export const ADDRESS_ZERO = "0x0000000000000000000000000000000000000000" -// export const ERC20_PAYMASTER_ADDRESS = '0xE9f6Ffc87cac92bc94f704AE017e85cB83DBe4EC' // likely to be same address on all chains - -export const ERC20_ABI = [ - "function transfer(address to, uint256 value) external returns (bool)", - "function transferFrom(address from, address to, uint256 value) external returns (bool)", - "function approve(address spender, uint256 value) external returns (bool)", - "function allowance(address owner, address spender) external view returns (uint256)", - "function balanceOf(address owner) external view returns (uint256)" -] diff --git a/src/paymaster/utils/Helpers.ts b/src/paymaster/utils/Helpers.ts deleted file mode 100644 index d9aeceea9..000000000 --- a/src/paymaster/utils/Helpers.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @description this function will return current timestamp in seconds - * @returns Number - */ -export const getTimestampInSeconds = (): number => { - return Math.floor(Date.now() / 1000) -} diff --git a/src/paymaster/utils/Types.ts b/src/paymaster/utils/Types.ts deleted file mode 100644 index 25326220f..000000000 --- a/src/paymaster/utils/Types.ts +++ /dev/null @@ -1,123 +0,0 @@ -export type Hex = `0x${string}` -import type { BigNumberish } from "../../account" -import type { JsonRpcError } from "../../bundler/utils/Types" - -export type PaymasterServiceErrorResponse = { - jsonrpc: string - id: number - error: JsonRpcError -} - -export type JsonRpcResponse = { - jsonrpc: string - id: number - // biome-ignore lint/suspicious/noExplicitAny: - result?: any - error?: JsonRpcError -} - -export type PaymasterConfig = { - paymasterUrl: string - strictMode?: boolean -} - -export type SponsorUserOperationDto = { - /** mode: sponsored or erc20 */ - mode: "SPONSORED" | "ERC20" - /** Always recommended, especially when using token paymaster */ - calculateGasLimits?: boolean - /** Expiry duration in seconds */ - expiryDuration?: number - /** Webhooks to be fired after user op is sent */ - webhookData?: Record - /** Smart account meta data */ - smartAccountInfo?: SmartAccountData - /** the fee-paying token address */ - feeTokenAddress?: string -} - -export type FeeQuotesOrDataDto = { - /** mode: sponsored or erc20 */ - mode?: "SPONSORED" | "ERC20" - /** Expiry duration in seconds */ - expiryDuration?: number - /** Always recommended, especially when using token paymaster */ - calculateGasLimits?: boolean - /** List of tokens to be used for fee quotes, if ommitted fees for all supported will be returned */ - tokenList?: string[] - /** preferredToken: Can be ommitted to return all quotes */ - preferredToken?: string - /** Webhooks to be fired after user op is sent */ - // biome-ignore lint/suspicious/noExplicitAny: - webhookData?: Record - /** Smart account meta data */ - smartAccountInfo?: SmartAccountData -} - -export type FeeQuoteParams = { - tokenList?: string[] - preferredToken?: string -} - -export type FeeTokenInfo = { - feeTokenAddress: string -} - -export type SponsorpshipInfo = { - /** Webhooks to be fired after user op is sent */ - // biome-ignore lint/suspicious/noExplicitAny: - webhookData?: Record - /** Smart account meta data */ - smartAccountInfo: SmartAccountData -} - -export type SmartAccountData = { - /** name: Name of the smart account */ - name: string - /** version: Version of the smart account */ - version: string -} - -export type PaymasterFeeQuote = { - /** symbol: Token symbol */ - symbol: string - /** tokenAddress: Token address */ - tokenAddress: string - /** decimal: Token decimal */ - decimal: number - logoUrl?: string - /** maxGasFee: in wei */ - maxGasFee: number - /** maxGasFee: in dollars */ - maxGasFeeUSD?: number - usdPayment?: number - /** The premium paid on the token */ - premiumPercentage: number - /** validUntil: Unix timestamp */ - validUntil?: number -} - -export type FeeQuotesOrDataResponse = { - /** Array of results from the paymaster */ - feeQuotes?: PaymasterFeeQuote[] - /** Normally set to the spender in the proceeding call to send the tx */ - tokenPaymasterAddress?: Hex - /** Relevant Data returned from the paymaster */ - paymasterAndData?: Uint8Array | Hex - /* Gas overhead of this UserOperation */ - preVerificationGas?: BigNumberish - /* Actual gas used by the validation of this UserOperation */ - verificationGasLimit?: BigNumberish - /* Value used by inner account execution */ - callGasLimit?: BigNumberish -} - -export type PaymasterAndDataResponse = { - paymasterAndData: Hex - /* Gas overhead of this UserOperation */ - preVerificationGas: number - /* Actual gas used by the validation of this UserOperation */ - verificationGasLimit: number - /* Value used by inner account execution */ - callGasLimit: number -} diff --git a/src/__contracts/abi/EIP1271Abi.ts b/src/sdk/__contracts/abi/EIP1271Abi.ts similarity index 89% rename from src/__contracts/abi/EIP1271Abi.ts rename to src/sdk/__contracts/abi/EIP1271Abi.ts index 044ffe934..304c5485b 100644 --- a/src/__contracts/abi/EIP1271Abi.ts +++ b/src/sdk/__contracts/abi/EIP1271Abi.ts @@ -8,11 +8,7 @@ export const EIP1271Abi = [ { name: "name", type: "string", internalType: "string" }, { name: "version", type: "string", internalType: "string" }, { name: "chainId", type: "uint256", internalType: "uint256" }, - { - name: "verifyingContract", - type: "address", - internalType: "address" - }, + { name: "verifyingContract", type: "address", internalType: "address" }, { name: "salt", type: "bytes32", internalType: "bytes32" }, { name: "extensions", type: "uint256[]", internalType: "uint256[]" } ], diff --git a/src/__contracts/abi/EntryPointABI.ts b/src/sdk/__contracts/abi/EntryPointABI.ts similarity index 100% rename from src/__contracts/abi/EntryPointABI.ts rename to src/sdk/__contracts/abi/EntryPointABI.ts diff --git a/src/__contracts/abi/K1ValidatorAbi.ts b/src/sdk/__contracts/abi/K1ValidatorAbi.ts similarity index 100% rename from src/__contracts/abi/K1ValidatorAbi.ts rename to src/sdk/__contracts/abi/K1ValidatorAbi.ts diff --git a/src/__contracts/abi/K1ValidatorFactoryAbi.ts b/src/sdk/__contracts/abi/K1ValidatorFactoryAbi.ts similarity index 100% rename from src/__contracts/abi/K1ValidatorFactoryAbi.ts rename to src/sdk/__contracts/abi/K1ValidatorFactoryAbi.ts diff --git a/src/__contracts/abi/NexusAbi.ts b/src/sdk/__contracts/abi/NexusAbi.ts similarity index 100% rename from src/__contracts/abi/NexusAbi.ts rename to src/sdk/__contracts/abi/NexusAbi.ts diff --git a/src/__contracts/abi/UniActionPolicyAbi.ts b/src/sdk/__contracts/abi/UniActionPolicyAbi.ts similarity index 100% rename from src/__contracts/abi/UniActionPolicyAbi.ts rename to src/sdk/__contracts/abi/UniActionPolicyAbi.ts diff --git a/src/__contracts/abi/index.ts b/src/sdk/__contracts/abi/index.ts similarity index 85% rename from src/__contracts/abi/index.ts rename to src/sdk/__contracts/abi/index.ts index 78bca202f..27aecc585 100644 --- a/src/__contracts/abi/index.ts +++ b/src/sdk/__contracts/abi/index.ts @@ -1,5 +1,6 @@ export * from "./UniActionPolicyAbi" export * from "./EntryPointABI" +export * from "./EIP1271Abi" export * from "./NexusAbi" export * from "./K1ValidatorAbi" export * from "./K1ValidatorFactoryAbi" diff --git a/src/__contracts/addresses.ts b/src/sdk/__contracts/addresses.ts similarity index 83% rename from src/__contracts/addresses.ts rename to src/sdk/__contracts/addresses.ts index b4b39bfd3..ed316626d 100644 --- a/src/__contracts/addresses.ts +++ b/src/sdk/__contracts/addresses.ts @@ -1,7 +1,6 @@ // The contents of this folder is auto-generated. Please do not edit as your changes are likely to be overwritten -import type { Hex } from "viem" -export const addresses: Record = { +export const addresses = { Nexus: "0x21f4C007C9f091B93B7C1C6911E13ACcd3DAd403", K1Validator: "0x6854688d3D9A87a33Addd5f4deB5cea1B97fa5b7", K1ValidatorFactory: "0x976869CF9c5Dd5046b41963EF1bBcE62b5366869", diff --git a/src/__contracts/index.ts b/src/sdk/__contracts/index.ts similarity index 85% rename from src/__contracts/index.ts rename to src/sdk/__contracts/index.ts index eae2c5a65..38595002c 100644 --- a/src/__contracts/index.ts +++ b/src/sdk/__contracts/index.ts @@ -1,14 +1,13 @@ import type { Hex } from "viem" +import { entryPoint07Address } from "viem/account-abstraction" import { EntrypointAbi, K1ValidatorAbi, K1ValidatorFactoryAbi } from "./abi" import addresses from "./addresses" export const ENTRYPOINT_SIMULATIONS: Hex = "0x74Cb5e4eE81b86e70f9045036a1C5477de69eE87" -export const ENTRYPOINT_ADDRESS: Hex = - "0x0000000071727De22E5E9d8BAf0edAc6f37da032" const entryPoint = { - address: ENTRYPOINT_ADDRESS, + address: entryPoint07Address, abi: EntrypointAbi } as const diff --git a/src/sdk/account/index.ts b/src/sdk/account/index.ts new file mode 100644 index 000000000..57756f3eb --- /dev/null +++ b/src/sdk/account/index.ts @@ -0,0 +1,2 @@ +export * from "./utils/index.js" +export * from "./toNexusAccount.js" diff --git a/src/sdk/account/toNexusAccount.test.ts b/src/sdk/account/toNexusAccount.test.ts new file mode 100644 index 000000000..42d58a347 --- /dev/null +++ b/src/sdk/account/toNexusAccount.test.ts @@ -0,0 +1,376 @@ +import { + http, + type Account, + type Address, + type Chain, + type Hex, + type PublicClient, + type WalletClient, + concat, + concatHex, + createWalletClient, + domainSeparator, + encodeAbiParameters, + encodePacked, + hashMessage, + isAddress, + isHex, + keccak256, + parseAbi, + parseAbiParameters, + parseEther, + toBytes, + toHex +} from "viem" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { TokenWithPermitAbi } from "../../test/__contracts/abi/TokenWithPermitAbi" +import { mockAddresses } from "../../test/__contracts/mockAddresses" +import { toNetwork } from "../../test/testSetup" +import { + fundAndDeployClients, + getTestAccount, + killNetwork, + toTestClient +} from "../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../test/testUtils" +import { NexusAbi } from "../__contracts/abi/NexusAbi" +import { addresses } from "../__contracts/addresses" +import { + type NexusClient, + createNexusClient +} from "../clients/createNexusClient" +import { type NexusAccount, toNexusAccount } from "./toNexusAccount" +import { getAccountDomainStructFields } from "./utils" +import { + NEXUS_DOMAIN_NAME, + NEXUS_DOMAIN_TYPEHASH, + NEXUS_DOMAIN_VERSION, + PARENT_TYPEHASH, + eip1271MagicValue +} from "./utils/Constants" +import type { UserOperationStruct } from "./utils/Types" + +describe("nexus.account", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let account: Account + let nexusAccountAddress: Address + let nexusClient: NexusClient + let nexusAccount: NexusAccount + let walletClient: WalletClient + + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + account = getTestAccount(0) + testClient = toTestClient(chain, getTestAccount(5)) + + walletClient = createWalletClient({ + account, + chain, + transport: http() + }) + + nexusClient = await createNexusClient({ + holder: account, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + nexusAccount = nexusClient.account + nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() + await fundAndDeployClients(testClient, [nexusClient]) + }) + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test("should check isValidSignature PersonalSign is valid", async () => { + const data = hashMessage("0x1234") + + // Calculate the domain separator + const domainSeparator = keccak256( + encodeAbiParameters( + parseAbiParameters("bytes32, bytes32, bytes32, uint256, address"), + [ + keccak256(toBytes(NEXUS_DOMAIN_TYPEHASH)), + keccak256(toBytes(NEXUS_DOMAIN_NAME)), + keccak256(toBytes(NEXUS_DOMAIN_VERSION)), + BigInt(chain.id), + nexusAccountAddress + ] + ) + ) + + // Calculate the parent struct hash + const parentStructHash = keccak256( + encodeAbiParameters(parseAbiParameters("bytes32, bytes32"), [ + keccak256(toBytes("PersonalSign(bytes prefixed)")), + hashMessage(data) + ]) + ) + + // Calculate the final hash + const resultHash: Hex = keccak256( + concat(["0x1901", domainSeparator, parentStructHash]) + ) + + const signature = await nexusAccount.signMessage({ + message: { raw: toBytes(resultHash) } + }) + + const contractResponse = await testClient.readContract({ + address: nexusAccountAddress, + abi: NexusAbi, + functionName: "isValidSignature", + args: [hashMessage(data), signature] + }) + + const viemResponse = await testClient.verifyMessage({ + address: nexusAccountAddress, + message: data, + signature + }) + + expect(contractResponse).toBe(eip1271MagicValue) + expect(viemResponse).toBe(true) + }) + + test("should have 4337 account actions", async () => { + const [ + isDeployed, + counterfactualAddress, + userOpHash, + address, + factoryArgs, + stubSignature, + signedMessage, + nonce, + initCode, + encodedExecute, + encodedExecuteBatch + ] = await Promise.all([ + nexusAccount.isDeployed(), + nexusAccount.getCounterFactualAddress(), + nexusAccount.getUserOpHash({ + sender: account.address, + nonce: 0n, + data: "0x", + signature: "0x", + verificationGasLimit: 1n, + preVerificationGas: 1n, + callData: "0x", + callGasLimit: 1n, + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n + } as UserOperationStruct), + nexusAccount.getAddress(), + nexusAccount.getFactoryArgs(), + nexusAccount.getStubSignature(), + nexusAccount.signMessage({ message: "hello" }), + nexusAccount.getNonce(), + nexusAccount.getInitCode(), + nexusAccount.encodeExecute({ to: account.address, value: 100n }), + nexusAccount.encodeExecuteBatch([{ to: account.address, value: 100n }]) + ]) + + expect(isAddress(counterfactualAddress)).toBe(true) + expect(isHex(userOpHash)).toBe(true) + expect(isAddress(address)).toBe(true) + expect(address).toBe(nexusAccountAddress) + + if (isDeployed) { + expect(factoryArgs.factory).toBe(undefined) + expect(factoryArgs.factoryData).toBe(undefined) + } else { + // biome-ignore lint/style/noNonNullAssertion: + expect(isAddress(factoryArgs.factory!)).toBe(true) + // biome-ignore lint/style/noNonNullAssertion: + expect(isHex(factoryArgs.factoryData!)).toBe(true) + } + + expect(isHex(stubSignature)).toBe(true) + expect(isHex(signedMessage)).toBe(true) + expect(typeof nonce).toBe("bigint") + expect(initCode.indexOf(nexusAccount.factoryAddress) > -1).toBe(true) + expect(typeof isDeployed).toBe("boolean") + + expect(isHex(encodedExecute)).toBe(true) + expect(isHex(encodedExecuteBatch)).toBe(true) + }) + + test("should test isValidSignature EIP712Sign to be valid with viem", async () => { + const message = { + contents: keccak256(toBytes("test", { size: 32 })) + } + + const domainSeparator = await testClient.readContract({ + address: await nexusAccount.getAddress(), + abi: parseAbi([ + "function DOMAIN_SEPARATOR() external view returns (bytes32)" + ]), + functionName: "DOMAIN_SEPARATOR" + }) + + const typedHashHashed = keccak256( + concat(["0x1901", domainSeparator, message.contents]) + ) + + const accountDomainStructFields = await getAccountDomainStructFields( + testClient as unknown as PublicClient, + nexusAccountAddress + ) + + const parentStructHash = keccak256( + encodePacked( + ["bytes", "bytes"], + [ + encodeAbiParameters(parseAbiParameters(["bytes32, bytes32"]), [ + keccak256(toBytes(PARENT_TYPEHASH)), + message.contents + ]), + accountDomainStructFields + ] + ) + ) + + const dataToSign = keccak256( + concat(["0x1901", domainSeparator, parentStructHash]) + ) + + const signature = await walletClient.signMessage({ + account, + message: { raw: toBytes(dataToSign) } + }) + + const contentsType = toBytes("Contents(bytes32 stuff)") + + const signatureData = concatHex([ + signature, + domainSeparator, + message.contents, + toHex(contentsType), + toHex(contentsType.length, { size: 2 }) + ]) + + const finalSignature = encodePacked( + ["address", "bytes"], + [addresses.K1Validator, signatureData] + ) + + const contractResponse = await testClient.readContract({ + address: await nexusAccount.getAddress(), + abi: NexusAbi, + functionName: "isValidSignature", + args: [typedHashHashed, finalSignature] + }) + + expect(contractResponse).toBe(eip1271MagicValue) + }) + + test("should sign using signTypedData SDK method", async () => { + const appDomain = { + chainId: chain.id, + name: "TokenWithPermit", + verifyingContract: mockAddresses.TokenWithPermit, + version: "1" + } + + const primaryType = "Contents" + const types = { + Contents: [ + { + name: "stuff", + type: "bytes32" + } + ] + } + + const permitTypehash = keccak256( + toBytes( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ) + ) + const nonce = (await testClient.readContract({ + address: mockAddresses.TokenWithPermit, + abi: TokenWithPermitAbi, + functionName: "nonces", + args: [nexusAccountAddress] + })) as bigint + + const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600) // 1 hour from now + + const message = { + stuff: keccak256( + encodeAbiParameters( + parseAbiParameters( + "bytes32, address, address, uint256, uint256, uint256" + ), + [ + permitTypehash, + nexusAccountAddress, + nexusAccountAddress, + parseEther("2"), + nonce, + deadline + ] + ) + ) + } + + const appDomainSeparator = domainSeparator({ + domain: appDomain + }) + + const contentsHash = keccak256( + concat(["0x1901", appDomainSeparator, message.stuff]) + ) + + const finalSignature = await nexusClient.signTypedData({ + domain: appDomain, + primaryType, + types, + message + }) + + const nexusResponse = await testClient.readContract({ + address: nexusAccountAddress, + abi: NexusAbi, + functionName: "isValidSignature", + args: [contentsHash, finalSignature] + }) + + const permitTokenResponse = await nexusClient.writeContract({ + address: mockAddresses.TokenWithPermit, + abi: TokenWithPermitAbi, + functionName: "permitWith1271", + chain: network.chain, + args: [ + nexusAccountAddress, + nexusAccountAddress, + parseEther("2"), + deadline, + finalSignature + ] + }) + + await testClient.waitForTransactionReceipt({ hash: permitTokenResponse }) + + const allowance = await testClient.readContract({ + address: mockAddresses.TokenWithPermit, + abi: TokenWithPermitAbi, + functionName: "allowance", + args: [nexusAccountAddress, nexusAccountAddress] + }) + + expect(allowance).toEqual(parseEther("2")) + expect(nexusResponse).toEqual("0x1626ba7e") + }) +}) diff --git a/src/sdk/account/toNexusAccount.ts b/src/sdk/account/toNexusAccount.ts new file mode 100644 index 000000000..fab2f3f7b --- /dev/null +++ b/src/sdk/account/toNexusAccount.ts @@ -0,0 +1,517 @@ +import { + type AbiParameter, + type Account, + type Address, + type Chain, + type ClientConfig, + type Hex, + type Prettify, + type PublicClient, + type RpcSchema, + type SignableMessage, + type Transport, + type TypedData, + type TypedDataDefinition, + type UnionPartialBy, + concat, + concatHex, + createWalletClient, + domainSeparator, + encodeAbiParameters, + encodeFunctionData, + encodePacked, + getContract, + keccak256, + parseAbi, + parseAbiParameters, + publicActions, + toBytes, + toHex, + validateTypedData, + walletActions +} from "viem" +import { + type SmartAccount, + type SmartAccountImplementation, + type UserOperation, + entryPoint07Address, + getUserOperationHash, + toSmartAccount +} from "viem/account-abstraction" +import contracts from "../__contracts" +import { EntrypointAbi, K1ValidatorFactoryAbi } from "../__contracts/abi" +import type { Call, GetNonceArgs, UserOperationStruct } from "./utils/Types" + +import { + EXECUTE_BATCH, + EXECUTE_SINGLE, + MAGIC_BYTES, + MODE_VALIDATION, + PARENT_TYPEHASH +} from "./utils/Constants" + +import type { BaseValidationModule } from "../modules/base/BaseValidationModule" +import { K1ValidatorModule } from "../modules/validators/K1ValidatorModule" +import { + type TypedDataWith712, + eip712WrapHash, + getAccountDomainStructFields, + getTypesForEIP712Domain, + packUserOp, + typeToString +} from "./utils/Utils" +import { type UnknownHolder, toHolder } from "./utils/toHolder" + +/** + * Parameters for creating a Nexus Smart Account + */ +export type ToNexusSmartAccountParameters = { + /** The blockchain network */ + chain: Chain + /** The transport configuration */ + transport: ClientConfig["transport"] + /** The holder account or address */ + holder: UnknownHolder + /** Optional index for the account */ + index?: bigint | undefined + /** Optional active validation module */ + activeModule?: BaseValidationModule + /** Optional factory address */ + factoryAddress?: Address + /** Optional K1 validator address */ + k1ValidatorAddress?: Address +} & Prettify< + Pick< + ClientConfig, + | "account" + | "cacheTime" + | "chain" + | "key" + | "name" + | "pollingInterval" + | "rpcSchema" + > +> + +/** + * Nexus Smart Account type + */ +export type NexusAccount = Prettify< + SmartAccount +> + +/** + * Nexus Smart Account Implementation + */ +export type NexusSmartAccountImplementation = SmartAccountImplementation< + typeof EntrypointAbi, + "0.7", + { + getCounterFactualAddress: () => Promise
+ isDeployed: () => Promise + getInitCode: () => Hex + encodeExecute: (call: Call) => Promise + encodeExecuteBatch: (calls: readonly Call[]) => Promise + getUserOpHash: (userOp: Partial) => Promise + factoryData: Hex + factoryAddress: Address + } +> + +/** + * @description Create a Nexus Smart Account. + * + * @param parameters - {@link ToNexusSmartAccountParameters} + * @returns Nexus Smart Account. {@link NexusAccount} + * + * @example + * import { toNexusAccount } from '@biconomy/sdk' + * import { createWalletClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const account = await toNexusAccount({ + * chain: mainnet, + * transport: http(), + * holder: '0x...', + * }) + */ +export const toNexusAccount = async ( + parameters: ToNexusSmartAccountParameters +): Promise => { + const { + chain, + transport, + holder: holder_, + index = 0n, + activeModule, + factoryAddress = contracts.k1ValidatorFactory.address, + k1ValidatorAddress = contracts.k1Validator.address, + key = "nexus account", + name = "Nexus Account" + } = parameters + + const holder = await toHolder({ holder: holder_ }) + + const masterClient = createWalletClient({ + account: holder, + chain, + transport, + key, + name + }) + .extend(walletActions) + .extend(publicActions) + + const signerAddress = masterClient.account.address + const entryPointContract = getContract({ + address: contracts.entryPoint.address, + abi: EntrypointAbi, + client: { + public: masterClient, + wallet: masterClient + } + }) + + const factoryData = encodeFunctionData({ + abi: K1ValidatorFactoryAbi, + functionName: "createAccount", + args: [signerAddress, index, [], 0] + }) + + const defaultedActiveModule = + activeModule ?? + new K1ValidatorModule( + { + address: k1ValidatorAddress, + type: "validator", + context: signerAddress, + additionalContext: "0x" + }, + holder + ) + + let _accountAddress: Address + const getAddress = async () => { + if (_accountAddress) return _accountAddress + _accountAddress = (await masterClient.readContract({ + address: factoryAddress, + abi: K1ValidatorFactoryAbi, + functionName: "computeAccountAddress", + args: [signerAddress, index, [], 0] + })) as Address + return _accountAddress + } + + /** + * @description Gets the counterfactual address of the account + * @returns The counterfactual address + * @throws {Error} If unable to get the counterfactual address + */ + const getCounterFactualAddress = async (): Promise
=> { + try { + await entryPointContract.simulate.getSenderAddress([getInitCode()]) + } catch (e: any) { + if (e?.cause?.data?.errorName === "SenderAddressResult") { + _accountAddress = e?.cause.data.args[0] as Address + return _accountAddress + } + console.log("Im in here", e) + } + throw new Error("Failed to get counterfactual account address") + } + + /** + * @description Gets the init code for the account + * @returns The init code as a hexadecimal string + */ + const getInitCode = () => concatHex([factoryAddress, factoryData]) + + /** + * @description Checks if the account is deployed + * @returns True if the account is deployed, false otherwise + */ + const isDeployed = async (): Promise => { + const address = await getCounterFactualAddress() + const contractCode = await masterClient.getCode({ address }) + return (contractCode?.length ?? 0) > 2 + } + + /** + * @description Calculates the hash of a user operation + * @param userOp - The user operation + * @returns The hash of the user operation + */ + const getUserOpHash = async ( + userOp: Partial + ): Promise => { + const packedUserOp = packUserOp(userOp) + const userOpHash = keccak256(packedUserOp as Hex) + const enc = encodeAbiParameters( + parseAbiParameters("bytes32, address, uint256"), + [userOpHash, contracts.entryPoint.address, BigInt(chain.id)] + ) + return keccak256(enc) + } + + /** + * @description Encodes a batch of calls for execution + * @param calls - An array of calls to encode + * @param mode - The execution mode + * @returns The encoded calls + */ + const encodeExecuteBatch = async ( + calls: readonly Call[], + mode = EXECUTE_BATCH + ): Promise => { + const executionAbiParams: AbiParameter = { + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "value", type: "uint256" }, + { name: "callData", type: "bytes" } + ] + } + + const executions = calls.map((tx) => ({ + target: tx.to, + callData: tx.data ?? "0x", + value: BigInt(tx.value ?? 0n) + })) + + const executionCalldataPrep = encodeAbiParameters( + [executionAbiParams], + [executions] + ) + return encodeFunctionData({ + abi: parseAbi([ + "function execute(bytes32 mode, bytes calldata executionCalldata) external" + ]), + functionName: "execute", + args: [mode, executionCalldataPrep] + }) + } + + /** + * @description Encodes a single call for execution + * @param call - The call to encode + * @param mode - The execution mode + * @returns The encoded call + */ + const encodeExecute = async ( + call: Call, + mode = EXECUTE_SINGLE + ): Promise => { + const executionCalldata = encodePacked( + ["address", "uint256", "bytes"], + [call.to as Hex, BigInt(call.value ?? 0n), (call.data ?? "0x") as Hex] + ) + + return encodeFunctionData({ + abi: parseAbi([ + "function execute(bytes32 mode, bytes calldata executionCalldata) external" + ]), + functionName: "execute", + args: [mode, executionCalldata] + }) + } + + /** + * @description Gets the nonce for the account + * @param args - Optional arguments for getting the nonce + * @returns The nonce + */ + const getNonce = async ({ + validationMode: _validationMode = MODE_VALIDATION, + nonceOptions + }: GetNonceArgs = {}): Promise => { + if (nonceOptions) { + if (nonceOptions?.nonceOverride) return BigInt(nonceOptions.nonceOverride) + if (nonceOptions?.validationMode) + _validationMode = nonceOptions.validationMode + } + try { + const key: string = concat([ + "0x000000", + _validationMode, + defaultedActiveModule.address + ]) + const accountAddress = await getAddress() + return await entryPointContract.read.getNonce([ + accountAddress, + BigInt(key) + ]) + } catch (e) { + return BigInt(0) + } + } + + /** + * @description Signs a message + * @param params - The parameters for signing + * @param params.message - The message to sign + * @returns The signature + */ + const signMessage = async ({ + message + }: { message: SignableMessage }): Promise => { + const tempSignature = await defaultedActiveModule + .getHolder() + .signMessage({ message }) + + const signature = encodePacked( + ["address", "bytes"], + [defaultedActiveModule.getAddress(), tempSignature] + ) + + const erc6492Signature = concat([ + encodeAbiParameters( + [ + { + type: "address", + name: "create2Factory" + }, + { + type: "bytes", + name: "factoryCalldata" + }, + { + type: "bytes", + name: "originalERC1271Signature" + } + ], + [factoryAddress, factoryData, signature] + ), + MAGIC_BYTES + ]) + + const accountIsDeployed = await isDeployed() + return accountIsDeployed ? signature : erc6492Signature + } + + /** + * @description Signs typed data + * @param parameters - The typed data parameters + * @returns The signature + */ + async function signTypedData< + const typedData extends TypedData | Record, + primaryType extends keyof typedData | "EIP712Domain" = keyof typedData + >(parameters: TypedDataDefinition): Promise { + const { message, primaryType, types: _types, domain } = parameters + + if (!domain) throw new Error("Missing domain") + if (!message) throw new Error("Missing message") + + const types = { + EIP712Domain: getTypesForEIP712Domain({ domain }), + ..._types + } + + // @ts-ignore: Comes from nexus parent typehash + const messageStuff: Hex = message.stuff + + // @ts-ignore + validateTypedData({ + domain, + message, + primaryType, + types + }) + + const appDomainSeparator = domainSeparator({ domain }) + const accountDomainStructFields = await getAccountDomainStructFields( + masterClient as unknown as PublicClient, + await getAddress() + ) + + const parentStructHash = keccak256( + encodePacked( + ["bytes", "bytes"], + [ + encodeAbiParameters(parseAbiParameters(["bytes32, bytes32"]), [ + keccak256(toBytes(PARENT_TYPEHASH)), + messageStuff + ]), + accountDomainStructFields + ] + ) + ) + + const wrappedTypedHash = eip712WrapHash( + parentStructHash, + appDomainSeparator + ) + + let signature = await defaultedActiveModule.signMessage( + toBytes(wrappedTypedHash) + ) + + const contentsType = toBytes(typeToString(types as TypedDataWith712)[1]) + + const signatureData = concatHex([ + signature, + appDomainSeparator, + messageStuff, + toHex(contentsType), + toHex(contentsType.length, { size: 2 }) + ]) + + signature = encodePacked( + ["address", "bytes"], + [defaultedActiveModule.getAddress(), signatureData] + ) + + return signature + } + + return toSmartAccount({ + client: masterClient, + entryPoint: { + abi: EntrypointAbi, + address: contracts.entryPoint.address, + version: "0.7" + }, + getAddress, + encodeCalls: (calls: readonly Call[]): Promise => { + return calls.length === 1 + ? encodeExecute(calls[0]) + : encodeExecuteBatch(calls) + }, + getFactoryArgs: async () => { + return { factory: factoryAddress, factoryData } + }, + getStubSignature: async (): Promise => { + return defaultedActiveModule.getDummySignature() + }, + signMessage, + signTypedData, + signUserOperation: async ( + parameters: UnionPartialBy & { + chainId?: number | undefined + } + ): Promise => { + const { chainId = masterClient.chain.id, ...userOpWithoutSender } = + parameters + const address = await getCounterFactualAddress() + const userOperation = { ...userOpWithoutSender, sender: address } + const hash = getUserOperationHash({ + chainId, + entryPointAddress: entryPoint07Address, + entryPointVersion: "0.7", + userOperation + }) + return await defaultedActiveModule.signUserOpHash(hash) + }, + getNonce, + extend: { + getCounterFactualAddress, + isDeployed, + getInitCode, + encodeExecute, + encodeExecuteBatch, + getUserOpHash, + factoryData, + factoryAddress + } + }) +} diff --git a/src/sdk/account/utils/AccountNotFound.ts b/src/sdk/account/utils/AccountNotFound.ts new file mode 100644 index 000000000..e373fcac3 --- /dev/null +++ b/src/sdk/account/utils/AccountNotFound.ts @@ -0,0 +1,20 @@ +import { BaseError } from "viem" + +/** + * @ignore + */ +export class AccountNotFoundError extends BaseError { + constructor({ docsPath }: { docsPath?: string | undefined } = {}) { + super( + [ + "Could not find an Account to execute with this Action.", + "Please provide an Account with the `account` argument on the Action, or by supplying an `account` to the Client." + ].join("\n"), + { + docsPath, + docsSlug: "account", + name: "AccountNotFoundError" + } + ) + } +} diff --git a/src/account/utils/Constants.ts b/src/sdk/account/utils/Constants.ts similarity index 69% rename from src/account/utils/Constants.ts rename to src/sdk/account/utils/Constants.ts index d42eda820..19b692682 100644 --- a/src/account/utils/Constants.ts +++ b/src/sdk/account/utils/Constants.ts @@ -1,4 +1,4 @@ -import { type Hex, keccak256, toHex } from "viem" +import { type Hex, concat, keccak256, pad, toHex } from "viem" export const ADDRESS_ZERO = "0x0000000000000000000000000000000000000000" export const MAGIC_BYTES = "0x6492649264926492649264926492649264926492649264926492649264926492" @@ -26,7 +26,7 @@ export const ERROR_MESSAGES = { ACCOUNT_NOT_DEPLOYED: "Account has not yet been deployed", ACCOUNT_ALREADY_DEPLOYED: "Account already deployed", NO_NATIVE_TOKEN_BALANCE_DURING_DEPLOY: - "Native token balance is not available during deploy", + "Smart Account does not have sufficient funds to execute the User Operation.", SPENDER_REQUIRED: "spender is required for ERC20 mode", NO_FEE_QUOTE: "FeeQuote was not provided, please call smartAccount.getTokenFees() to get feeQuote", @@ -70,8 +70,8 @@ export const GENERIC_FALLBACK_SELECTOR = "0xcb5baf0f" export const SENTINEL_ADDRESS: Hex = "0x0000000000000000000000000000000000000001" -export const MODE_VALIDATION = "0x00" as Hex -export const MODE_MODULE_ENABLE = "0x01" as Hex +export const MODE_VALIDATION = "0x00" +export const MODE_MODULE_ENABLE = "0x01" export const MODULE_ENABLE_MODE_TYPE_HASH = keccak256( toHex("ModuleEnableMode(address module, bytes32 initDataHash)") @@ -82,7 +82,63 @@ export const MODULE_TYPE_MULTI = 0 export const NEXUS_DOMAIN_NAME = "Nexus" export const NEXUS_DOMAIN_VERSION = "1.0.0-beta" +export const NEXUS_DOMAIN_TYPEHASH = + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" export const PARENT_TYPEHASH = "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)" export const eip1271MagicValue: Hex = "0x1626ba7e" + +export const EXECUTE_SINGLE = concat([ + CALLTYPE_SINGLE, + EXECTYPE_DEFAULT, + MODE_DEFAULT, + UNUSED, + MODE_PAYLOAD +]) + +export const EXECUTE_BATCH = concat([ + CALLTYPE_BATCH, + EXECTYPE_DEFAULT, + MODE_DEFAULT, + UNUSED, + MODE_PAYLOAD +]) + +export const ACCOUNT_MODES = { + DEFAULT_SINGLE: concat([ + pad(EXECTYPE_DEFAULT, { size: 1 }), + pad(CALLTYPE_SINGLE, { size: 1 }), + pad(UNUSED, { size: 4 }), + pad(MODE_DEFAULT, { size: 4 }), + pad(MODE_PAYLOAD, { size: 22 }) + ]), + DEFAULT_BATCH: concat([ + pad(EXECTYPE_DEFAULT, { size: 1 }), + pad(CALLTYPE_BATCH, { size: 1 }), + pad(UNUSED, { size: 4 }), + pad(MODE_DEFAULT, { size: 4 }), + pad(MODE_PAYLOAD, { size: 22 }) + ]), + TRY_BATCH: concat([ + pad(EXECTYPE_TRY, { size: 1 }), + pad(CALLTYPE_BATCH, { size: 1 }), + pad(UNUSED, { size: 4 }), + pad(MODE_DEFAULT, { size: 4 }), + pad(MODE_PAYLOAD, { size: 22 }) + ]), + TRY_SINGLE: concat([ + pad(EXECTYPE_TRY, { size: 1 }), + pad(CALLTYPE_SINGLE, { size: 1 }), + pad(UNUSED, { size: 4 }), + pad(MODE_DEFAULT, { size: 4 }), + pad(MODE_PAYLOAD, { size: 22 }) + ]), + DELEGATE_SINGLE: concat([ + pad(EXECTYPE_DELEGATE, { size: 1 }), + pad(CALLTYPE_SINGLE, { size: 1 }), + pad(UNUSED, { size: 4 }), + pad(MODE_DEFAULT, { size: 4 }), + pad(MODE_PAYLOAD, { size: 22 }) + ]) +} diff --git a/src/sdk/account/utils/Helpers.ts b/src/sdk/account/utils/Helpers.ts new file mode 100644 index 000000000..85d5c3051 --- /dev/null +++ b/src/sdk/account/utils/Helpers.ts @@ -0,0 +1,4 @@ +export const isDebugging = () => + process.env.BICONOMY_SDK_DEBUG === "true" || + process.env.REACT_APP_BICONOMY_SDK_DEBUG === "true" || + process.env.NEXT_PUBLIC_BICONOMY_SDK_DEBUG === "true" diff --git a/src/account/utils/Logger.ts b/src/sdk/account/utils/Logger.ts similarity index 100% rename from src/account/utils/Logger.ts rename to src/sdk/account/utils/Logger.ts diff --git a/src/sdk/account/utils/Types.ts b/src/sdk/account/utils/Types.ts new file mode 100644 index 000000000..f1cd3a424 --- /dev/null +++ b/src/sdk/account/utils/Types.ts @@ -0,0 +1,106 @@ +import type { Address, Hash, Hex, Log } from "viem" +import type { MODE_MODULE_ENABLE, MODE_VALIDATION } from "./Constants" + +export type TStatus = "success" | "reverted" + +export type UserOpReceiptTransaction = { + transactionHash: Hex + transactionIndex: bigint + blockHash: Hash + blockNumber: bigint + from: Address + to: Address | null + cumulativeGasUsed: bigint + status: TStatus + gasUsed: bigint + contractAddress: Address | null + logsBloom: Hex + effectiveGasPrice: bigint +} + +export type UserOpReceipt = { + userOpHash: Hash + entryPoint: Address + sender: Address + nonce: bigint + paymaster?: Address + actualGasUsed: bigint + actualGasCost: bigint + success: boolean + reason?: string + receipt: UserOpReceiptTransaction + logs: Log[] +} + +export type NonceOptions = { + /** nonceKey: The key to use for nonce */ + nonceKey?: bigint + /** validationMode: Mode of the validation module */ + validationMode?: typeof MODE_VALIDATION | typeof MODE_MODULE_ENABLE + /** nonceOverride: The nonce to use for the transaction */ + nonceOverride?: bigint +} + +export type Service = "Bundler" | "Paymaster" +export type BigNumberish = Hex | number | bigint +export type BytesLike = Uint8Array | Hex | string + +//#region UserOperationStruct +// based on @account-abstraction/common +// this is used for building requests +export type UserOperationStruct = { + sender: Address + nonce: bigint + factory?: Address + factoryData?: Hex + callData: Hex + callGasLimit: bigint + verificationGasLimit: bigint + preVerificationGas: bigint + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + paymaster?: Address + paymasterVerificationGasLimit?: bigint + paymasterPostOpGasLimit?: bigint + paymasterData?: Hex + signature: Hex + // initCode?: never + paymasterAndData?: never +} +//#endregion UserOperationStruct + +export type EIP712DomainReturn = [ + Hex, + string, + string, + bigint, + Address, + Hex, + bigint[] +] + +export type AccountMetadata = { + name: string + version: string + chainId: bigint +} + +export type TypeField = { + name: string + type: string +} + +export type TypeDefinition = { + [key: string]: TypeField[] +} + +export type GetNonceArgs = { + key?: bigint | undefined + validationMode?: "0x00" | "0x01" + nonceOptions?: NonceOptions +} +export type Call = { + to: Hex + data?: Hex | undefined + value?: bigint | undefined +} diff --git a/src/account/utils/Utils.ts b/src/sdk/account/utils/Utils.ts similarity index 88% rename from src/account/utils/Utils.ts rename to src/sdk/account/utils/Utils.ts index 09511380c..a0c024c7b 100644 --- a/src/account/utils/Utils.ts +++ b/src/sdk/account/utils/Utils.ts @@ -4,6 +4,7 @@ import { type Hash, type Hex, type PublicClient, + type TypedData, type TypedDataDomain, type TypedDataParameter, concat, @@ -21,20 +22,20 @@ import { toBytes, toHex } from "viem" -import { EIP1271Abi } from "../../__contracts/abi/EIP1271Abi" -import type { - AccountMetadata, - EIP712DomainReturn, - TypeDefinition, - UserOperationStruct -} from "../../account" +import { EIP1271Abi } from "../../__contracts/abi" import { MOCK_MULTI_MODULE_ADDRESS, MODULE_ENABLE_MODE_TYPE_HASH, NEXUS_DOMAIN_NAME, + NEXUS_DOMAIN_TYPEHASH, NEXUS_DOMAIN_VERSION -} from "../../account" +} from "../../account/utils/Constants" import { type ModuleType, moduleTypeIds } from "../../modules/utils/Types" +import type { + AccountMetadata, + EIP712DomainReturn, + UserOperationStruct +} from "./Types" /** * pack the userOperation @@ -162,15 +163,11 @@ export function convertToFactor(percentage: number | undefined): number { export function makeInstallDataAndHash( accountOwner: Address, - modules: { moduleType: ModuleType; config: Hex }[] + modules: { type: ModuleType; config: Hex }[] ): [string, string] { - const types = modules.map((module) => - BigInt(moduleTypeIds[module.moduleType]) - ) + const types = modules.map((module) => BigInt(moduleTypeIds[module.type])) const initDatas = modules.map((module) => - toHex( - concat([toBytes(BigInt(moduleTypeIds[module.moduleType])), module.config]) - ) + toHex(concat([toBytes(BigInt(moduleTypeIds[module.type])), module.config])) ) const multiInstallData = encodeAbiParameters( @@ -214,11 +211,7 @@ export function _hashTypedData( { type: "address" } ], [ - keccak256( - stringToBytes( - "EIP712Domain(string name,string version,address verifyingContract)" - ) - ), + keccak256(stringToBytes(NEXUS_DOMAIN_TYPEHASH)), keccak256(stringToBytes(name)), keccak256(stringToBytes(version)), verifyingContract @@ -278,9 +271,9 @@ export const accountMetadata = async ( data: domain }) return { - name: decoded[1], - version: decoded[2], - chainId: decoded[3] + name: decoded?.[1], + version: decoded?.[2], + chainId: decoded?.[3] } } } catch (error) {} @@ -293,24 +286,27 @@ export const accountMetadata = async ( } } -export const eip712WrapHash = async ( - typedHash: Hex, - appDomainSeparator: Hex -): Promise => { - const digest = keccak256(concat(["0x1901", appDomainSeparator, typedHash])) +export const eip712WrapHash = (typedHash: Hex, appDomainSeparator: Hex): Hex => + keccak256(concat(["0x1901", appDomainSeparator, typedHash])) - return digest -} +export type TypedDataWith712 = { + EIP712Domain: TypedDataParameter[] +} & TypedData -export function typeToString(typeDef: TypeDefinition): string[] { +export function typeToString(typeDef: TypedDataWith712): string[] { return Object.entries(typeDef).map(([key, fields]) => { - const fieldStrings = fields + const fieldStrings = (fields ?? []) .map((field) => `${field.type} ${field.name}`) .join(",") return `${key}(${fieldStrings})` }) } +/** @ignore */ +export function bigIntReplacer(_key: string, value: any): any { + return typeof value === "bigint" ? value.toString() : value +} + export const getAccountDomainStructFields = async ( publicClient: PublicClient, accountAddress: Address diff --git a/src/bundler/utils/getAAError.ts b/src/sdk/account/utils/getAAError.ts similarity index 93% rename from src/bundler/utils/getAAError.ts rename to src/sdk/account/utils/getAAError.ts index 2f0425b3c..8be4806c0 100644 --- a/src/bundler/utils/getAAError.ts +++ b/src/sdk/account/utils/getAAError.ts @@ -1,6 +1,5 @@ import { BaseError } from "viem" -import type { Service } from "../../account" -import { SDK_VERSION } from "./Constants" +import type { Service } from ".." export type KnownError = { name: string regex: string @@ -47,7 +46,7 @@ type AccountAbstractionErrorParams = { class AccountAbstractionError extends BaseError { override name = "AccountAbstractionError" - override version = `@biconomy/account@${SDK_VERSION}` + override version = "@biconomy/sdk" constructor(title: string, params: AccountAbstractionErrorParams = {}) { super(title, params) diff --git a/src/account/utils/getChain.ts b/src/sdk/account/utils/getChain.ts similarity index 95% rename from src/account/utils/getChain.ts rename to src/sdk/account/utils/getChain.ts index ede2ea4db..e3d83bf99 100644 --- a/src/account/utils/getChain.ts +++ b/src/sdk/account/utils/getChain.ts @@ -64,7 +64,7 @@ type StringOrStrings = string | string[] * * @example * - * import { getCustomChain, createSmartAccountClient } from "@biconomy/account" + * import { getCustomChain, createNexusClient } from "@biconomy/account" * * const customChain = getCustomChain( * "My Custom Chain", @@ -80,7 +80,7 @@ type StringOrStrings = string | string[] * transport: http() * }) * - * const smartAccountCustomChain = await createSmartAccountClient({ + * const smartAccountCustomChain = await createNexusClient({ * signer: walletClientWithCustomChain, * bundlerUrl, * customChain diff --git a/src/account/utils/index.ts b/src/sdk/account/utils/index.ts similarity index 58% rename from src/account/utils/index.ts rename to src/sdk/account/utils/index.ts index b1f3d8378..36c7543f6 100644 --- a/src/account/utils/index.ts +++ b/src/sdk/account/utils/index.ts @@ -1,8 +1,6 @@ export * from "./Types.js" export * from "./Utils.js" export * from "./Constants.js" -export * from "./convertSigner.js" export * from "./getChain.js" export * from "./Logger.js" -export * from "./HttpRequests.js" -export * from "./EthersSigner.js" +export * from "./toHolder.js" diff --git a/src/sdk/account/utils/toHolder.ts b/src/sdk/account/utils/toHolder.ts new file mode 100644 index 000000000..94d2948fa --- /dev/null +++ b/src/sdk/account/utils/toHolder.ts @@ -0,0 +1,85 @@ +import { + type Account, + type Address, + type Chain, + type EIP1193Provider, + type EIP1193RequestFn, + type EIP1474Methods, + type LocalAccount, + type OneOf, + type Transport, + type WalletClient, + createWalletClient, + custom +} from "viem" +import { toAccount } from "viem/accounts" + +import { signTypedData } from "viem/actions" +import { getAction } from "viem/utils" + +export type Holder = LocalAccount +export type UnknownHolder = OneOf< + | EIP1193Provider + | WalletClient + | LocalAccount +> +export async function toHolder({ + holder, + address +}: { + holder: UnknownHolder + address?: Address +}): Promise { + if ("type" in holder && holder.type === "local") { + return holder as LocalAccount + } + + let walletClient: + | WalletClient + | undefined = undefined + + if ("request" in holder) { + if (!address) { + try { + ;[address] = await (holder.request as EIP1193RequestFn)( + { + method: "eth_requestAccounts" + } + ) + } catch { + ;[address] = await (holder.request as EIP1193RequestFn)( + { + method: "eth_accounts" + } + ) + } + } + if (!address) throw new Error("address required") + + walletClient = createWalletClient({ + account: address, + transport: custom(holder as EIP1193Provider) + }) + } + + if (!walletClient) { + walletClient = holder as WalletClient + } + + return toAccount({ + address: walletClient.account.address, + async signMessage({ message }) { + return walletClient.signMessage({ message }) + }, + async signTypedData(typedData) { + return getAction( + walletClient, + signTypedData, + "signTypedData" + )(typedData as any) + }, + async signTransaction(_) { + throw new Error("Not supported") + } + }) +} diff --git a/src/sdk/account/utils/utils.test.ts b/src/sdk/account/utils/utils.test.ts new file mode 100644 index 000000000..3ae0f3882 --- /dev/null +++ b/src/sdk/account/utils/utils.test.ts @@ -0,0 +1,58 @@ +import { ParamType, ethers } from "ethers" +import { type AbiParameter, encodeAbiParameters } from "viem" +import { describe, expect, test } from "vitest" + +describe("utils", async () => { + test.concurrent( + "should have consistent behaviour between ethers.AbiCoder.defaultAbiCoder() and viem.encodeAbiParameters()", + async () => { + const expectedResult = + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b90600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b906000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + + const Executions = ParamType.from({ + type: "tuple(address,uint256,bytes)[]", + baseType: "tuple", + name: "executions", + arrayLength: null, + components: [ + { name: "target", type: "address" }, + { name: "value", type: "uint256" }, + { name: "callData", type: "bytes" } + ] + }) + + const viemExecutions: AbiParameter = { + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "value", type: "uint256" }, + { name: "callData", type: "bytes" } + ] + } + + const txs = [ + { + target: "0x90F79bf6EB2c4f870365E785982E1f101E93b906", + callData: "0x", + value: 1n + }, + { + target: "0x90F79bf6EB2c4f870365E785982E1f101E93b906", + callData: "0x", + value: 1n + } + ] + + const executionCalldataPrepWithEthers = + ethers.AbiCoder.defaultAbiCoder().encode([Executions], [txs]) + + const executionCalldataPrepWithViem = encodeAbiParameters( + [viemExecutions], + [txs] + ) + + expect(executionCalldataPrepWithEthers).toBe(expectedResult) + expect(executionCalldataPrepWithViem).toBe(expectedResult) + } + ) +}) diff --git a/src/sdk/clients/createBicoBundlerClient.test.ts b/src/sdk/clients/createBicoBundlerClient.test.ts new file mode 100644 index 000000000..2592ff0d0 --- /dev/null +++ b/src/sdk/clients/createBicoBundlerClient.test.ts @@ -0,0 +1,85 @@ +import { http, type Account, type Address, type Chain, isHex } from "viem" +import type { BundlerClient } from "viem/account-abstraction" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../test/testSetup" +import { + getTestAccount, + killNetwork, + toTestClient, + topUp +} from "../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../test/testUtils" +import contracts from "../__contracts" +import { type NexusAccount, toNexusAccount } from "../account/toNexusAccount" +import { createBicoBundlerClient } from "./createBicoBundlerClient" + +describe("bico.bundler", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let account: Account + let nexusAccountAddress: Address + let bicoBundler: BundlerClient + let nexusAccount: NexusAccount + + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + account = getTestAccount(0) + testClient = toTestClient(chain, getTestAccount(5)) + + nexusAccount = await toNexusAccount({ + holder: account, + chain, + transport: http() + }) + + bicoBundler = createBicoBundlerClient({ bundlerUrl, account: nexusAccount }) + nexusAccountAddress = await nexusAccount.getCounterFactualAddress() + await topUp(testClient, nexusAccountAddress) + }) + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test.concurrent("should have 4337 bundler actions", async () => { + const [chainId, supportedEntrypoints, preparedUserOp] = await Promise.all([ + bicoBundler.getChainId(), + bicoBundler.getSupportedEntryPoints(), + bicoBundler.prepareUserOperation({ + sender: account.address, + nonce: 0n, + data: "0x", + signature: "0x", + verificationGasLimit: 1n, + preVerificationGas: 1n, + callData: "0x", + callGasLimit: 1n, + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + account: nexusAccount + }) + ]) + expect(chainId).toEqual(chain.id) + expect(supportedEntrypoints).to.include(contracts.entryPoint.address) + expect(preparedUserOp).toHaveProperty("signature") + }) + + test("should send a user operation and get the receipt", async () => { + const calls = [{ to: account.address, value: 1n }] + // Must find gas fees before sending the user operation + const gas = await testClient.estimateFeesPerGas() + const hash = await bicoBundler.sendUserOperation({ + ...gas, + calls, + account: nexusAccount + }) + const receipt = await bicoBundler.waitForUserOperationReceipt({ hash }) + expect(receipt.success).toBeTruthy() + }) +}) diff --git a/src/sdk/clients/createBicoBundlerClient.ts b/src/sdk/clients/createBicoBundlerClient.ts new file mode 100644 index 000000000..9a5d38846 --- /dev/null +++ b/src/sdk/clients/createBicoBundlerClient.ts @@ -0,0 +1,61 @@ +import { http, type OneOf, type Transport } from "viem" +import { + type BundlerClient, + type BundlerClientConfig, + createBundlerClient +} from "viem/account-abstraction" + +type BicoBundlerClientConfig = Omit & + OneOf< + | { + transport?: Transport + } + | { + bundlerUrl: string + } + | { + apiKey?: string + } + > + +/** + * Creates a Bico Bundler Client with a given Transport configured for a Chain. + * + * @param parameters - Configuration for the Bico Bundler Client + * @returns A Bico Bundler Client + * + * @example + * import { createBicoBundlerClient, http } from '@biconomy/sdk' + * import { mainnet } from 'viem/chains' + * + * const bundlerClient = createBicoBundlerClient({ chain: mainnet }); + */ +export const createBicoBundlerClient = ( + parameters: BicoBundlerClientConfig +): BundlerClient => { + if ( + !parameters.apiKey && + !parameters.bundlerUrl && + !parameters.transport && + !parameters?.chain + ) { + throw new Error( + "Cannot set determine a bundler url, please provide a chain." + ) + } + + return createBundlerClient({ + ...parameters, + transport: parameters.transport + ? parameters.transport + : parameters.bundlerUrl + ? http(parameters.bundlerUrl) + : http( + // @ts-ignore: Type saftey provided by the if statement above + `https://bundler.biconomy.io/api/v3/${parameters.chain.id}/${ + parameters.apiKey ?? + "nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f14" + }` + ) + }) +} diff --git a/src/sdk/clients/createBicoPaymasterClient.ts b/src/sdk/clients/createBicoPaymasterClient.ts new file mode 100644 index 000000000..01abfcc51 --- /dev/null +++ b/src/sdk/clients/createBicoPaymasterClient.ts @@ -0,0 +1,62 @@ +import { http, type OneOf, type Transport } from "viem" +import { + type PaymasterClient, + type PaymasterClientConfig, + createPaymasterClient +} from "viem/account-abstraction" + +/** + * Configuration options for creating a Bico Paymaster Client. + * @typedef {Object} BicoPaymasterClientConfig + * @property {Transport} [transport] - Optional custom transport. + * @property {string} [paymasterUrl] - URL of the paymaster service. + * @property {number} [chainId] - Chain ID for the network. + * @property {string} [apiKey] - API key for authentication. + */ +type BicoPaymasterClientConfig = Omit & + OneOf< + | { + transport?: Transport + } + | { + paymasterUrl: string + } + | { + chainId: number + apiKey: string + } + > + +/** + * Creates a Bico Paymaster Client. + * + * This function sets up a client for interacting with Biconomy's paymaster service. + * It can be configured with a custom transport, a specific paymaster URL, or with a chain ID and API key. + * + * @param {BicoPaymasterClientConfig} parameters - Configuration options for the client. + * @returns {PaymasterClient} A configured Paymaster Client instance. + * + * @example + * // Create a client with a custom transport + * const client1 = createBicoPaymasterClient({ transport: customTransport }) + * + * @example + * // Create a client with a specific paymaster URL + * const client2 = createBicoPaymasterClient({ paymasterUrl: 'https://example.com/paymaster' }) + * + * @example + * // Create a client with chain ID and API key + * const client3 = createBicoPaymasterClient({ chainId: 1, apiKey: 'your-api-key' }) + */ +export const createBicoPaymasterClient = ( + parameters: BicoPaymasterClientConfig +): PaymasterClient => + createPaymasterClient({ + ...parameters, + transport: + parameters.transport ?? parameters.paymasterUrl + ? http(parameters.paymasterUrl) + : http( + `https://paymaster.biconomy.io/api/v3/${parameters.chainId}/${parameters.apiKey}` + ) + }) diff --git a/src/sdk/clients/createNexusClient.test.ts b/src/sdk/clients/createNexusClient.test.ts new file mode 100644 index 000000000..156819159 --- /dev/null +++ b/src/sdk/clients/createNexusClient.test.ts @@ -0,0 +1,275 @@ +import { AbiCoder, ParamType } from "ethers/abi" +import { JsonRpcProvider } from "ethers/providers" +import { Wallet } from "ethers/wallet" +import { + http, + type AbiParameter, + type Account, + type Address, + type Chain, + type Hex, + encodeAbiParameters, + encodeFunctionData, + parseEther +} from "viem" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { CounterAbi } from "../../test/__contracts/abi" +import mockAddresses from "../../test/__contracts/mockAddresses" +import { toNetwork } from "../../test/testSetup" +import { + getBalance, + getTestAccount, + killNetwork, + toTestClient, + topUp +} from "../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../test/testUtils" +import { pKey } from "../../test/testUtils" +import { addresses } from "../__contracts/addresses" +import { ERROR_MESSAGES } from "../account/utils/Constants" +import { makeInstallDataAndHash } from "../account/utils/Utils" +import { getChain } from "../account/utils/getChain" +import { type NexusClient, createNexusClient } from "./createNexusClient" + +describe("nexus.client", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let account: Account + let recipientAccount: Account + let recipientAddress: Address + let nexusClient: NexusClient + let nexusAccountAddress: Address + + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + account = getTestAccount(0) + recipientAccount = getTestAccount(1) + recipientAddress = recipientAccount.address + + testClient = toTestClient(chain, getTestAccount(5)) + + nexusClient = await createNexusClient({ + holder: account, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() + }) + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test("should deploy smart account if not deployed", async () => { + const isDeployed = await nexusClient.account.isDeployed() + + if (!isDeployed) { + console.log("Smart account not deployed. Deploying...") + + // Fund the account first + await topUp(testClient, nexusAccountAddress, parseEther("0.01")) + + const hash = await nexusClient.sendTransaction({ + calls: [ + { + to: nexusAccountAddress, + value: 0n, + data: "0x" + } + ] + }) + const { status } = await testClient.waitForTransactionReceipt({ + hash + }) + expect(status).toBe("success") + + const isNowDeployed = await nexusClient.account.isDeployed() + expect(isNowDeployed).toBe(true) + + console.log("Smart account deployed successfully") + } else { + console.log("Smart account already deployed") + } + + // Verify the account is now deployed + const finalDeploymentStatus = await nexusClient.account.isDeployed() + expect(finalDeploymentStatus).toBe(true) + }) + + test("should fund the smart account", async () => { + await topUp(testClient, nexusAccountAddress, parseEther("0.01")) + + const balance = await getBalance(testClient, nexusAccountAddress) + expect(balance > 0) + }) + + // @note @todo this test is only valid for anvil + test("should have account addresses", async () => { + const addresses = await Promise.all([ + account.address, + nexusClient.account.getAddress() + ]) + expect(addresses.every(Boolean)).to.be.true + expect(addresses).toStrictEqual([ + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account + ]) + }) + + test("should estimate gas for writing to a contract", async () => { + const encodedCall = encodeFunctionData({ + abi: CounterAbi, + functionName: "incrementNumber" + }) + const call = { + to: mockAddresses.Counter, + data: encodedCall + } + const results = await Promise.all([ + nexusClient.estimateUserOperationGas({ calls: [call] }), + nexusClient.estimateUserOperationGas({ calls: [call, call] }) + ]) + + const increasingGasExpenditure = results.every( + ({ preVerificationGas }, i) => + preVerificationGas > (results[i - 1]?.preVerificationGas ?? 0) + ) + + expect(increasingGasExpenditure).toBeTruthy() + }, 60000) + + test("should check enable mode", async () => { + const result = makeInstallDataAndHash(account.address, [ + { + type: "validator", + config: account.address + } + ]) + + expect(result).toBeTruthy() + }, 30000) + + test.skip("should create a nexusAccount from an ethers signer", async () => { + const ethersProvider = new JsonRpcProvider(chain.rpcUrls.default.http[0]) + const ethersSigner = new Wallet(pKey, ethersProvider) + + const ethOwnerNexusClient = await createNexusClient({ + chain, + owner: (await ethersSigner.getAddress()) as Hex, + bundlerTransport: http(bundlerUrl), + transport: http(chain.rpcUrls.default.http[0]) + }) + + expect(await ethOwnerNexusClient.account.getAddress()).toBeTruthy() + }) + + test("should read estimated user op gas values", async () => { + const userOp = await nexusClient.prepareUserOperation({ + calls: [ + { + to: recipientAccount.address, + data: "0x" + } + ] + }) + + const estimatedGas = await nexusClient.estimateUserOperationGas(userOp) + expect(estimatedGas.verificationGasLimit).toBeTruthy() + expect(estimatedGas.callGasLimit).toBeTruthy() + expect(estimatedGas.preVerificationGas).toBeTruthy() + }, 30000) + + test.skip("should create a smart account with paymaster with an api key", async () => { + const paymaster = nexusClient.paymaster + expect(paymaster).not.toBeNull() + expect(paymaster).not.toBeUndefined() + }) + + test("should return chain object for chain id 1", async () => { + const chainId = 1 + const chain = getChain(chainId) + expect(chain.id).toBe(chainId) + }) + + test("should have correct fields", async () => { + const chainId = 1 + const chain = getChain(chainId) + ;[ + "blockExplorers", + "contracts", + "fees", + "formatters", + "id", + "name", + "nativeCurrency", + "rpcUrls", + "serializers" + ].every((field) => { + expect(chain).toHaveProperty(field) + }) + }) + + test("should throw an error, chain id not found", async () => { + const chainId = 0 + expect(() => getChain(chainId)).toThrow(ERROR_MESSAGES.CHAIN_NOT_FOUND) + }) + + test("should have attached erc757 actions", async () => { + const [ + accountId, + isModuleInstalled, + supportsExecutionMode, + supportsModule + ] = await Promise.all([ + nexusClient.accountId(), + nexusClient.isModuleInstalled({ + module: { + type: "validator", + address: addresses.K1Validator, + context: "0x" + } + }), + nexusClient.supportsExecutionMode({ + type: "delegatecall" + }), + nexusClient.supportsModule({ + type: "validator" + }) + ]) + expect(accountId).toBe("biconomy.nexus.1.0.0-beta") + expect(isModuleInstalled).toBe(true) + expect(supportsExecutionMode).toBe(true) + expect(supportsModule).toBe(true) + }) + + test("should send eth twice", async () => { + const balanceBefore = await getBalance(testClient, recipientAddress) + const tx = { to: recipientAddress, value: 1n } + const hash = await nexusClient.sendTransaction({ calls: [tx, tx] }) + const { status } = await testClient.waitForTransactionReceipt({ hash }) + const balanceAfter = await getBalance(testClient, recipientAddress) + expect(status).toBe("success") + expect(balanceAfter - balanceBefore).toBe(2n) + }) + + // Not working + test.skip("should uninstall modules", async () => { + const result = await nexusClient.uninstallModules({ + modules: [ + { + type: "validator", + address: addresses.K1Validator, + context: "0x" + } + ] + }) + }) +}) diff --git a/src/sdk/clients/createNexusClient.ts b/src/sdk/clients/createNexusClient.ts new file mode 100644 index 000000000..c6637f1f2 --- /dev/null +++ b/src/sdk/clients/createNexusClient.ts @@ -0,0 +1,226 @@ +import type { + Address, + Chain, + Client, + ClientConfig, + EstimateFeesPerGasReturnType, + Prettify, + PublicClient, + RpcSchema, + Transport +} from "viem" +import type { + BundlerActions, + BundlerClientConfig, + PaymasterActions, + SmartAccount, + UserOperationRequest +} from "viem/account-abstraction" +import contracts from "../__contracts" +import type { Call } from "../account/utils/Types" + +import { type NexusAccount, toNexusAccount } from "../account/toNexusAccount" +import type { UnknownHolder } from "../account/utils/toHolder" +import type { BaseExecutionModule } from "../modules/base/BaseExecutionModule" +import type { BaseValidationModule } from "../modules/base/BaseValidationModule" +import { createBicoBundlerClient } from "./createBicoBundlerClient" +import { type Erc7579Actions, erc7579Actions } from "./decorators/erc7579" +import { + type SmartAccountActions, + smartAccountActions +} from "./decorators/smartAccount" + +/** + * Parameters for sending a transaction + */ +export type SendTransactionParameters = { + calls: Call | Call[] +} + +/** + * Nexus Client type + */ +export type NexusClient< + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined, + account extends NexusAccount | undefined = NexusAccount | undefined, + client extends Client | undefined = Client | undefined, + rpcSchema extends RpcSchema | undefined = undefined +> = Prettify< + Pick< + ClientConfig, + "cacheTime" | "chain" | "key" | "name" | "pollingInterval" | "rpcSchema" + > & + BundlerActions & + Erc7579Actions & + SmartAccountActions & { + /** + * The Nexus account associated with this client + */ + account: NexusAccount + /** + * Optional client for additional functionality + */ + client?: client | Client | undefined + /** + * Transport configuration for the bundler + */ + bundlerTransport?: BundlerClientConfig["transport"] + /** + * Optional paymaster configuration + */ + paymaster?: BundlerClientConfig["paymaster"] | undefined + /** + * Optional paymaster context + */ + paymasterContext?: BundlerClientConfig["paymasterContext"] | undefined + /** + * Optional user operation configuration + */ + userOperation?: BundlerClientConfig["userOperation"] | undefined + } +> + +/** + * Configuration for creating a Nexus Client + */ +export type NexusClientConfig< + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined, + account extends SmartAccount | undefined = SmartAccount | undefined, + client extends Client | undefined = Client | undefined, + rpcSchema extends RpcSchema | undefined = undefined +> = Prettify< + Pick< + ClientConfig, + | "account" + | "cacheTime" + | "chain" + | "key" + | "name" + | "pollingInterval" + | "rpcSchema" + > & { + /** RPC URL. */ + transport: transport + /** Bundler URL. */ + bundlerTransport: transport + /** Client that points to an Execution RPC URL. */ + client?: client | Client | undefined + /** Paymaster configuration. */ + paymaster?: + | true + | { + /** Retrieves paymaster-related User Operation properties to be used for sending the User Operation. */ + getPaymasterData?: PaymasterActions["getPaymasterData"] | undefined + /** Retrieves paymaster-related User Operation properties to be used for gas estimation. */ + getPaymasterStubData?: + | PaymasterActions["getPaymasterStubData"] + | undefined + } + | undefined + /** Paymaster context to pass to `getPaymasterData` and `getPaymasterStubData` calls. */ + paymasterContext?: unknown + /** User Operation configuration. */ + userOperation?: + | { + /** Prepares fee properties for the User Operation request. */ + estimateFeesPerGas?: + | ((parameters: { + account: account | SmartAccount + bundlerClient: Client + userOperation: UserOperationRequest + }) => Promise>) + | undefined + } + | undefined + /** Owner of the account. */ + holder: UnknownHolder + /** Index of the account. */ + index?: bigint + /** Active module of the account. */ + activeModule?: BaseValidationModule + /** Executor module of the account. */ + executorModule?: BaseExecutionModule + /** Factory address of the account. */ + factoryAddress?: Address + /** Owner module */ + k1ValidatorAddress?: Address + } +> + +/** + * Creates a Nexus Client for interacting with the Nexus smart account system. + * + * @param parameters - {@link NexusClientConfig} + * @returns Nexus Client. {@link NexusClient} + * + * @example + * import { createNexusClient } from '@biconomy/sdk' + * import { http } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const nexusClient = await createNexusClient({ + * chain: mainnet, + * transport: http('https://mainnet.infura.io/v3/YOUR-PROJECT-ID'), + * bundlerTransport: http('https://api.biconomy.io'), + * holder: '0x...', + * }) + */ +export async function createNexusClient( + parameters: NexusClientConfig +): Promise { + const { + client: client_, + chain = parameters.chain ?? client_?.chain, + holder, + index = 0n, + key = "nexus client", + name = "Nexus Client", + activeModule, + factoryAddress = contracts.k1ValidatorFactory.address, + k1ValidatorAddress = contracts.k1Validator.address, + bundlerTransport, + paymaster, + transport, + paymasterContext, + userOperation = { + estimateFeesPerGas: async (parameters) => { + const feeData = await ( + parameters?.account?.client as PublicClient + )?.estimateFeesPerGas?.() + return { + maxFeePerGas: feeData.maxFeePerGas * 2n, + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas * 2n + } + } + } + } = parameters + + if (!chain) throw new Error("Missing chain") + + const nexusAccount = await toNexusAccount({ + transport, + chain, + holder, + index, + activeModule, + factoryAddress, + k1ValidatorAddress + }) + + const bundler = createBicoBundlerClient({ + ...parameters, + key, + name, + account: nexusAccount, + paymaster, + paymasterContext, + transport: bundlerTransport, + userOperation + }) + .extend(erc7579Actions()) + .extend(smartAccountActions()) + + return bundler as unknown as NexusClient +} diff --git a/src/sdk/clients/decorators/erc7579/accountId.ts b/src/sdk/clients/decorators/erc7579/accountId.ts new file mode 100644 index 000000000..7068e9600 --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/accountId.ts @@ -0,0 +1,108 @@ +import { + type Chain, + type Client, + ContractFunctionExecutionError, + type Transport, + decodeFunctionResult, + encodeFunctionData +} from "viem" +import type { + GetSmartAccountParameter, + SmartAccount +} from "viem/account-abstraction" +import { call, readContract } from "viem/actions" +import { getAction } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" + +/** + * Retrieves the account ID for a given smart account. + * + * @param client - The client instance. + * @param args - Optional parameters for getting the smart account. + * @returns The account ID as a string. + * @throws {AccountNotFoundError} If the account is not found. + * @throws {Error} If the accountId result is empty. + * + * @example + * import { accountId } from '@biconomy/sdk' + * + * const id = await accountId(nexusClient) + * console.log(id) // 'example_account_id' + */ +export async function accountId( + client: Client, + args?: GetSmartAccountParameter +): Promise { + let account_ = client.account + + if (args) { + account_ = args.account as TSmartAccount + } + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = account_ as SmartAccount + + const publicClient = account.client + + const abi = [ + { + name: "accountId", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [ + { + type: "string", + name: "accountImplementationId" + } + ] + } + ] as const + + try { + return await getAction( + publicClient, + readContract, + "readContract" + )({ + abi, + functionName: "accountId", + address: await account.getAddress() + }) + } catch (error) { + if (error instanceof ContractFunctionExecutionError) { + const { factory, factoryData } = await account.getFactoryArgs() + + const result = await getAction( + publicClient, + call, + "call" + )({ + factory: factory, + factoryData: factoryData, + to: account.address, + data: encodeFunctionData({ + abi, + functionName: "accountId" + }) + }) + + if (!result || !result.data) { + throw new Error("accountId result is empty") + } + + return decodeFunctionResult({ + abi, + functionName: "accountId", + data: result.data + }) + } + + throw error + } +} diff --git a/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts b/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts new file mode 100644 index 000000000..e8d5c7cc5 --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts @@ -0,0 +1,116 @@ +import { textSpanOverlapsWith } from "typescript" +import { http, type Account, type Address, type Chain, isHex } from "viem" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../../test/testSetup" +import { + type MasterClient, + type NetworkConfig, + fundAndDeployClients, + getTestAccount, + killNetwork, + toTestClient +} from "../../../../test/testUtils" +import contracts from "../../../__contracts" +import { type NexusClient, createNexusClient } from "../../createNexusClient" + +describe("erc7579.decorators", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let account: Account + let nexusClient: NexusClient + let nexusAccountAddress: Address + let recipient: Account + let recipientAddress: Address + + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + account = getTestAccount(0) + recipient = getTestAccount(1) + recipientAddress = recipient.address + testClient = toTestClient(chain, getTestAccount(5)) + + nexusClient = await createNexusClient({ + holder: account, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() + await fundAndDeployClients(testClient, [nexusClient]) + }) + + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test.concurrent("should test read methods", async () => { + const [ + installedValidators, + installedExecutors, + activeHook, + fallbackSelector, + supportsValidator, + supportsDelegateCall, + isK1ValidatorInstalled + ] = await Promise.all([ + nexusClient.getInstalledValidators({}), + nexusClient.getInstalledExecutors({}), + nexusClient.getActiveHook({}), + nexusClient.getFallbackBySelector({ selector: "0xcb5baf0f" }), + nexusClient.supportsModule({ type: "validator" }), + nexusClient.supportsExecutionMode({ type: "delegatecall" }), + nexusClient.isModuleInstalled({ + module: { + type: "validator", + address: contracts.k1Validator.address, + context: "0x" + } + }) + ]) + + expect(installedExecutors[0].length).toBeTypeOf("number") + expect(installedValidators[0]).toEqual([contracts.k1Validator.address]) + expect(isHex(activeHook)).toBe(true) + expect(fallbackSelector.length).toBeTypeOf("number") + expect(supportsValidator).toBe(true) + expect(supportsDelegateCall).toBe(true) + expect(isK1ValidatorInstalled).toBe(true) + }) + + test.skip("should uninstall a module", async () => { + const gas = await testClient.estimateFeesPerGas() + + const hash = await nexusClient.uninstallModule({ + ...gas, + module: { + type: "validator", + address: contracts.k1Validator.address, + context: "0x" + } + }) + + const { success } = await nexusClient.waitForUserOperationReceipt({ hash }) + expect(success).toBe(true) + }) + + test.skip("should install a module", async () => { + const hash = await nexusClient.installModule({ + module: { + type: "validator", + address: contracts.k1Validator.address, + context: "0x" + } + }) + + const { success } = await nexusClient.waitForUserOperationReceipt({ hash }) + expect(success).toBe(true) + }) +}) diff --git a/src/sdk/clients/decorators/erc7579/getActiveHook.ts b/src/sdk/clients/decorators/erc7579/getActiveHook.ts new file mode 100644 index 000000000..d13a27871 --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/getActiveHook.ts @@ -0,0 +1,69 @@ +import type { Chain, Client, Hex, Transport } from "viem" +import type { + GetSmartAccountParameter, + SmartAccount +} from "viem/account-abstraction" +import { readContract } from "viem/actions" +import { getAction, parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" + +export type GetActiveHookParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter + +/** + * Retrieves the active hook for a given smart account. + * + * @param client - The client instance. + * @param parameters - Parameters for getting the smart account. + * @returns The address of the active hook as a hexadecimal string. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { getActiveHook } from '@biconomy/sdk' + * + * const activeHook = await getActiveHook(nexusClient) + * console.log(activeHook) // '0x...' + */ +export async function getActiveHook< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + parameters: GetActiveHookParameters +): Promise { + const { account: account_ = client.account } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const publicClient = account.client + + return getAction( + publicClient, + readContract, + "readContract" + )({ + address: account.address, + abi: [ + { + inputs: [], + name: "getActiveHook", + outputs: [ + { + internalType: "address", + name: "hook", + type: "address" + } + ], + stateMutability: "view", + type: "function" + } + ], + functionName: "getActiveHook" + }) as Promise +} diff --git a/src/sdk/clients/decorators/erc7579/getFallbackBySelector.ts b/src/sdk/clients/decorators/erc7579/getFallbackBySelector.ts new file mode 100644 index 000000000..22b925aef --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/getFallbackBySelector.ts @@ -0,0 +1,90 @@ +import type { Chain, Client, Hex, Transport } from "viem" +import type { + GetSmartAccountParameter, + SmartAccount +} from "viem/account-abstraction" +import { readContract } from "viem/actions" +import { getAction, parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import { GENERIC_FALLBACK_SELECTOR } from "../../../account/utils/Constants" + +export type GetFallbackBySelectorParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter & + Partial<{ + selector?: Hex + }> + +/** + * Retrieves the fallback handler for a given selector in a smart account. + * + * @param client - The client instance. + * @param parameters - Parameters including the smart account and optional selector. + * @returns A tuple containing the call type and address of the fallback handler. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { getFallbackBySelector } from '@biconomy/sdk' + * + * const [callType, handlerAddress] = await getFallbackBySelector(nexusClient, { + * selector: '0x12345678' + * }) + * console.log(callType, handlerAddress) // '0x1' '0x...' + */ +export async function getFallbackBySelector< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + parameters: GetFallbackBySelectorParameters +): Promise<[Hex, Hex]> { + const { + account: account_ = client.account, + selector = GENERIC_FALLBACK_SELECTOR + } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const publicClient = account.client + + return getAction( + publicClient, + readContract, + "readContract" + )({ + address: account.address, + abi: [ + { + inputs: [ + { + internalType: "bytes4", + name: "selector", + type: "bytes4" + } + ], + name: "getFallbackHandlerBySelector", + outputs: [ + { + internalType: "CallType", + name: "", + type: "bytes1" + }, + { + internalType: "address", + name: "", + type: "address" + } + ], + stateMutability: "view", + type: "function" + } + ], + functionName: "getFallbackHandlerBySelector", + args: [selector] + }) as Promise<[Hex, Hex]> +} diff --git a/src/sdk/clients/decorators/erc7579/getInstalledExecutors.ts b/src/sdk/clients/decorators/erc7579/getInstalledExecutors.ts new file mode 100644 index 000000000..9c4adaca3 --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/getInstalledExecutors.ts @@ -0,0 +1,96 @@ +import type { Chain, Client, Hex, Transport } from "viem" +import type { + GetSmartAccountParameter, + SmartAccount +} from "viem/account-abstraction" +import { readContract } from "viem/actions" +import { getAction, parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import { SENTINEL_ADDRESS } from "../../../account/utils/Constants" + +export type GetInstalledExecutorsParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter & { + pageSize?: bigint + cursor?: Hex +} + +/** + * Retrieves the installed executors for a given smart account. + * + * @param client - The client instance. + * @param parameters - Parameters including the smart account, page size, and cursor. + * @returns A tuple containing an array of executor addresses and the next cursor. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { getInstalledExecutors } from '@biconomy/sdk' + * + * const [executors, nextCursor] = await getInstalledExecutors(nexusClient, { + * pageSize: 10n + * }) + * console.log(executors, nextCursor) // ['0x...', '0x...'], '0x...' + */ +export async function getInstalledExecutors< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + parameters: GetInstalledExecutorsParameters +): Promise { + const { + account: account_ = client.account, + pageSize = 100n, + cursor = SENTINEL_ADDRESS + } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const publicClient = account.client + + return getAction( + publicClient, + readContract, + "readContract" + )({ + address: account.address, + abi: [ + { + inputs: [ + { + internalType: "address", + name: "cursor", + type: "address" + }, + { + internalType: "uint256", + name: "size", + type: "uint256" + } + ], + name: "getExecutorsPaginated", + outputs: [ + { + internalType: "address[]", + name: "array", + type: "address[]" + }, + { + internalType: "address", + name: "next", + type: "address" + } + ], + stateMutability: "view", + type: "function" + } + ], + functionName: "getExecutorsPaginated", + args: [cursor, pageSize] + }) as Promise +} diff --git a/src/sdk/clients/decorators/erc7579/getInstalledValidators.ts b/src/sdk/clients/decorators/erc7579/getInstalledValidators.ts new file mode 100644 index 000000000..960165782 --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/getInstalledValidators.ts @@ -0,0 +1,96 @@ +import type { Chain, Client, Hex, Transport } from "viem" +import type { + GetSmartAccountParameter, + SmartAccount +} from "viem/account-abstraction" +import { readContract } from "viem/actions" +import { getAction, parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import { SENTINEL_ADDRESS } from "../../../account/utils/Constants" + +export type GetInstalledValidatorsParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter & { + pageSize?: bigint + cursor?: Hex +} + +/** + * Retrieves the installed validators for a given smart account. + * + * @param client - The client instance. + * @param parameters - Parameters including the smart account, page size, and cursor. + * @returns A tuple containing an array of validator addresses and the next cursor. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { getInstalledValidators } from '@biconomy/sdk' + * + * const [validators, nextCursor] = await getInstalledValidators(nexusClient, { + * pageSize: 10n + * }) + * console.log(validators, nextCursor) // ['0x...', '0x...'], '0x...' + */ +export async function getInstalledValidators< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + parameters: GetInstalledValidatorsParameters +): Promise { + const { + account: account_ = client.account, + pageSize = 100n, + cursor = SENTINEL_ADDRESS + } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const publicClient = account.client + + return getAction( + publicClient, + readContract, + "readContract" + )({ + address: account.address, + abi: [ + { + inputs: [ + { + internalType: "address", + name: "cursor", + type: "address" + }, + { + internalType: "uint256", + name: "size", + type: "uint256" + } + ], + name: "getValidatorsPaginated", + outputs: [ + { + internalType: "address[]", + name: "array", + type: "address[]" + }, + { + internalType: "address", + name: "next", + type: "address" + } + ], + stateMutability: "view", + type: "function" + } + ], + functionName: "getValidatorsPaginated", + args: [cursor, pageSize] + }) as Promise +} diff --git a/src/sdk/clients/decorators/erc7579/index.ts b/src/sdk/clients/decorators/erc7579/index.ts new file mode 100644 index 000000000..a0e79cd96 --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/index.ts @@ -0,0 +1,148 @@ +import type { Address, Chain, Client, Hash, Hex, Transport } from "viem" +import type { + GetSmartAccountParameter, + SmartAccount +} from "viem/account-abstraction" +import type { ModuleType, SafeHookType } from "../../../modules/utils/Types.js" +import { accountId } from "./accountId.js" +import { type GetActiveHookParameters, getActiveHook } from "./getActiveHook.js" +import { + type GetFallbackBySelectorParameters, + getFallbackBySelector +} from "./getFallbackBySelector.js" +import { + type GetInstalledExecutorsParameters, + getInstalledExecutors +} from "./getInstalledExecutors.js" +import { + type GetInstalledValidatorsParameters, + getInstalledValidators +} from "./getInstalledValidators.js" +import { type InstallModuleParameters, installModule } from "./installModule.js" +import { + type InstallModulesParameters, + installModules +} from "./installModules.js" +import { + type IsModuleInstalledParameters, + isModuleInstalled +} from "./isModuleInstalled.js" +import { + type SupportsExecutionModeParameters, + supportsExecutionMode +} from "./supportsExecutionMode.js" +import type { CallType, ExecutionMode } from "./supportsExecutionMode.js" +import { + type SupportsModuleParameters, + supportsModule +} from "./supportsModule.js" +import { + type UninstallModuleParameters, + uninstallModule +} from "./uninstallModule.js" +import { + type UninstallModulesParameters, + uninstallModules +} from "./uninstallModules.js" + +export type Erc7579Actions = { + accountId: (args?: GetSmartAccountParameter) => Promise + installModule: (args: InstallModuleParameters) => Promise + installModules: ( + args: InstallModulesParameters + ) => Promise + isModuleInstalled: ( + args: IsModuleInstalledParameters + ) => Promise + supportsExecutionMode: ( + args: SupportsExecutionModeParameters + ) => Promise + supportsModule: ( + args: SupportsModuleParameters + ) => Promise + uninstallModule: ( + args: UninstallModuleParameters + ) => Promise + uninstallModules: ( + args: UninstallModulesParameters + ) => Promise + getInstalledValidators: ( + args: GetInstalledValidatorsParameters + ) => Promise + getInstalledExecutors: ( + args: GetInstalledExecutorsParameters + ) => Promise + getActiveHook: (args: GetActiveHookParameters) => Promise + getFallbackBySelector: ( + args: GetFallbackBySelectorParameters + ) => Promise<[Hex, Hex]> +} + +export type { + InstallModuleParameters, + IsModuleInstalledParameters, + CallType, + ExecutionMode, + SupportsExecutionModeParameters, + ModuleType, + SupportsModuleParameters, + UninstallModuleParameters, + GetInstalledValidatorsParameters, + GetInstalledExecutorsParameters, + GetActiveHookParameters +} + +export { + accountId, + installModule, + installModules, + isModuleInstalled, + supportsExecutionMode, + supportsModule, + uninstallModule, + uninstallModules, + getInstalledValidators, + getInstalledExecutors, + getActiveHook, + getFallbackBySelector +} + +export function erc7579Actions() { + return ( + client: Client + ): Erc7579Actions => ({ + accountId: (args) => accountId(client, args), + installModule: (args) => installModule(client, args), + installModules: (args) => installModules(client, args), + isModuleInstalled: (args) => isModuleInstalled(client, args), + supportsExecutionMode: (args) => supportsExecutionMode(client, args), + supportsModule: (args) => supportsModule(client, args), + uninstallModule: (args) => uninstallModule(client, args), + uninstallModules: (args) => uninstallModules(client, args), + getInstalledValidators: (args) => getInstalledValidators(client, args), + getInstalledExecutors: (args) => getInstalledExecutors(client, args), + getActiveHook: (args) => getActiveHook(client, args), + getFallbackBySelector: (args) => getFallbackBySelector(client, args) + }) +} + +export type Module = { + address: Address + context: Hex + additionalContext?: Hex + type: ModuleType + + /* ---- kernel module params ---- */ + // these param needed for installing validator, executor, fallback handler + hook?: Address + /* ---- end kernel module params ---- */ + + /* ---- safe module params ---- */ + // these two params needed for installing hooks + hookType?: SafeHookType + selector?: Hex + + // these two params needed for installing fallback handlers + functionSig?: Hex + callType?: CallType +} diff --git a/src/sdk/clients/decorators/erc7579/installModule.ts b/src/sdk/clients/decorators/erc7579/installModule.ts new file mode 100644 index 000000000..f5eb21548 --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/installModule.ts @@ -0,0 +1,105 @@ +import { type Client, type Hex, encodeFunctionData, getAddress } from "viem" +import { + type GetSmartAccountParameter, + type SmartAccount, + sendUserOperation +} from "viem/account-abstraction" +import { getAction, parseAccount } from "viem/utils" +import type { Module } from "." +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import { parseModuleTypeId } from "./supportsModule" + +export type InstallModuleParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter & { + module: Module + maxFeePerGas?: bigint + maxPriorityFeePerGas?: bigint + nonce?: bigint +} + +/** + * Installs a module on a given smart account. + * + * @param client - The client instance. + * @param parameters - Parameters including the smart account, module to install, and optional gas settings. + * @returns The hash of the user operation as a hexadecimal string. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { installModule } from '@biconomy/sdk' + * + * const userOpHash = await installModule(nexusClient, { + * module: { + * type: 'executor', + * address: '0x...', + * context: '0x' + * } + * }) + * console.log(userOpHash) // '0x...' + */ +export async function installModule< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + parameters: InstallModuleParameters +): Promise { + const { + account: account_ = client.account, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + module: { type, address, context } + } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + return getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ + calls: [ + { + to: account.address, + value: BigInt(0), + data: encodeFunctionData({ + abi: [ + { + name: "installModule", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { + type: "uint256", + name: "moduleTypeId" + }, + { + type: "address", + name: "module" + }, + { + type: "bytes", + name: "initData" + } + ], + outputs: [] + } + ], + functionName: "installModule", + args: [parseModuleTypeId(type), getAddress(address), context] + }) + } + ], + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + account + }) +} diff --git a/src/sdk/clients/decorators/erc7579/installModules.ts b/src/sdk/clients/decorators/erc7579/installModules.ts new file mode 100644 index 000000000..ac7366b8d --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/installModules.ts @@ -0,0 +1,113 @@ +import { + type Address, + type Chain, + type Client, + type Hex, + type Transport, + encodeFunctionData, + getAddress +} from "viem" +import { + type GetSmartAccountParameter, + type SmartAccount, + sendUserOperation +} from "viem/account-abstraction" +import { getAction, parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import type { ModuleType } from "../../../modules/utils/Types" +import { parseModuleTypeId } from "./supportsModule" + +export type InstallModulesParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter & { + modules: { + type: ModuleType + address: Address + context: Hex + }[] + maxFeePerGas?: bigint + maxPriorityFeePerGas?: bigint + nonce?: bigint +} + +/** + * Installs multiple modules on a given smart account. + * + * @param client - The client instance. + * @param parameters - Parameters including the smart account, modules to install, and optional gas settings. + * @returns The hash of the user operation as a hexadecimal string. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { installModules } from '@biconomy/sdk' + * + * const userOpHash = await installModules(nexusClient, { + * modules: [ + * { type: 'executor', address: '0x...', context: '0x' }, + * { type: 'validator', address: '0x...', context: '0x' } + * ] + * }) + * console.log(userOpHash) // '0x...' + */ +export async function installModules< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + parameters: InstallModulesParameters +): Promise { + const { + account: account_ = client.account, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + modules + } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + return getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ + calls: modules.map(({ type, address, context }) => ({ + to: account.address, + value: BigInt(0), + data: encodeFunctionData({ + abi: [ + { + name: "installModule", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { + type: "uint256", + name: "moduleTypeId" + }, + { + type: "address", + name: "module" + }, + { + type: "bytes", + name: "initData" + } + ], + outputs: [] + } + ], + functionName: "installModule", + args: [parseModuleTypeId(type), getAddress(address), context] + }) + })), + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + account: account + }) +} diff --git a/src/sdk/clients/decorators/erc7579/isModuleInstalled.ts b/src/sdk/clients/decorators/erc7579/isModuleInstalled.ts new file mode 100644 index 000000000..a135bb9bb --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/isModuleInstalled.ts @@ -0,0 +1,138 @@ +import { + type Chain, + type Client, + ContractFunctionExecutionError, + type Transport, + decodeFunctionResult, + encodeFunctionData, + getAddress +} from "viem" +import type { + GetSmartAccountParameter, + SmartAccount +} from "viem/account-abstraction" +import { call, readContract } from "viem/actions" +import { getAction, parseAccount } from "viem/utils" +import type { Module } from "." +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import { parseModuleTypeId } from "./supportsModule" + +export type IsModuleInstalledParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter & { + module: Module +} + +/** + * Checks if a specific module is installed on a given smart account. + * + * @param client - The client instance. + * @param parameters - Parameters including the smart account and the module to check. + * @returns A boolean indicating whether the module is installed. + * @throws {AccountNotFoundError} If the account is not found. + * @throws {Error} If the accountId result is empty. + * + * @example + * import { isModuleInstalled } from '@biconomy/sdk' + * + * const isInstalled = await isModuleInstalled(nexusClient, { + * module: { + * type: 'executor', + * address: '0x...', + * context: '0x' + * } + * }) + * console.log(isInstalled) // true or false + */ +export async function isModuleInstalled< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + parameters: IsModuleInstalledParameters +): Promise { + const { + account: account_ = client.account, + module: { address, context, type } + } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const publicClient = account.client + + const abi = [ + { + name: "isModuleInstalled", + type: "function", + stateMutability: "view", + inputs: [ + { + type: "uint256", + name: "moduleTypeId" + }, + { + type: "address", + name: "module" + }, + { + type: "bytes", + name: "additionalContext" + } + ], + outputs: [ + { + type: "bool" + } + ] + } + ] as const + + try { + return (await getAction( + publicClient, + readContract, + "readContract" + )({ + abi, + functionName: "isModuleInstalled", + args: [parseModuleTypeId(type), getAddress(address), context], + address: account.address + })) as unknown as Promise + } catch (error) { + if (error instanceof ContractFunctionExecutionError) { + const { factory, factoryData } = await account.getFactoryArgs() + + const result = await getAction( + publicClient, + call, + "call" + )({ + factory: factory, + factoryData: factoryData, + to: account.address, + data: encodeFunctionData({ + abi, + functionName: "isModuleInstalled", + args: [parseModuleTypeId(type), getAddress(address), context] + }) + }) + + if (!result || !result.data) { + throw new Error("accountId result is empty") + } + + return decodeFunctionResult({ + abi, + functionName: "isModuleInstalled", + data: result.data + }) as unknown as Promise + } + + throw error + } +} diff --git a/src/sdk/clients/decorators/erc7579/supportsExecutionMode.ts b/src/sdk/clients/decorators/erc7579/supportsExecutionMode.ts new file mode 100644 index 000000000..23c788b85 --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/supportsExecutionMode.ts @@ -0,0 +1,183 @@ +import { + type Chain, + type Client, + ContractFunctionExecutionError, + type Hex, + type Transport, + decodeFunctionResult, + encodeFunctionData, + encodePacked, + toBytes, + toHex +} from "viem" +import type { + GetSmartAccountParameter, + SmartAccount +} from "viem/account-abstraction" +import { call, readContract } from "viem/actions" +import { getAction } from "viem/utils" +import { parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" + +export type CallType = "call" | "delegatecall" | "batchcall" + +export type ExecutionMode = { + type: callType + revertOnError?: boolean + selector?: Hex + context?: Hex +} + +export type SupportsExecutionModeParameters< + TSmartAccount extends SmartAccount | undefined, + callType extends CallType = CallType +> = GetSmartAccountParameter & ExecutionMode + +function parseCallType(callType: CallType) { + switch (callType) { + case "call": + return "0x00" + case "batchcall": + return "0x01" + case "delegatecall": + return "0xff" + } +} + +/** + * Encodes the execution mode for a smart account operation. + * + * @param mode - The execution mode parameters. + * @returns The encoded execution mode as a hexadecimal string. + */ +export function encodeExecutionMode({ + type, + revertOnError, + selector, + context +}: ExecutionMode): Hex { + return encodePacked( + ["bytes1", "bytes1", "bytes4", "bytes4", "bytes22"], + [ + toHex(toBytes(parseCallType(type), { size: 1 })), + toHex(toBytes(revertOnError ? "0x01" : "0x00", { size: 1 })), + toHex(toBytes("0x0", { size: 4 })), + toHex(toBytes(selector ?? "0x", { size: 4 })), + toHex(toBytes(context ?? "0x", { size: 22 })) + ] + ) +} + +/** + * Checks if a smart account supports a specific execution mode. + * + * @param client - The client instance. + * @param args - Parameters including the smart account and execution mode details. + * @returns A boolean indicating whether the execution mode is supported. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { supportsExecutionMode } from '@biconomy/sdk' + * + * const isSupported = await supportsExecutionMode(nexusClient, { + * type: 'call', + * revertOnError: true, + * selector: '0x12345678' + * }) + * console.log(isSupported) // true or false + */ +export async function supportsExecutionMode< + TSmartAccount extends SmartAccount | undefined, + callType extends CallType = CallType +>( + client: Client, + args: SupportsExecutionModeParameters +): Promise { + const { + account: account_ = client.account, + type, + revertOnError, + selector, + context + } = args + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const publicClient = account.client + + const encodedMode = encodeExecutionMode({ + type, + revertOnError, + selector, + context + }) + + const abi = [ + { + name: "supportsExecutionMode", + type: "function", + stateMutability: "view", + inputs: [ + { + type: "bytes32", + name: "encodedMode" + } + ], + outputs: [ + { + type: "bool" + } + ] + } + ] as const + + try { + return await getAction( + publicClient, + readContract, + "readContract" + )({ + abi, + functionName: "supportsExecutionMode", + args: [encodedMode], + address: account.address + }) + } catch (error) { + if (error instanceof ContractFunctionExecutionError) { + const { factory, factoryData } = await account.getFactoryArgs() + + const result = await getAction( + publicClient, + call, + "call" + )({ + factory: factory, + factoryData: factoryData, + to: account.address, + data: encodeFunctionData({ + abi, + functionName: "supportsExecutionMode", + args: [encodedMode] + }) + }) + + if (!result || !result.data) { + throw new Error("accountId result is empty") + } + + return decodeFunctionResult({ + abi, + functionName: "supportsExecutionMode", + data: result.data + }) + } + + throw error + } +} diff --git a/src/sdk/clients/decorators/erc7579/supportsModule.ts b/src/sdk/clients/decorators/erc7579/supportsModule.ts new file mode 100644 index 000000000..cf7da1cfa --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/supportsModule.ts @@ -0,0 +1,143 @@ +import { + type Chain, + type Client, + ContractFunctionExecutionError, + type Transport, + decodeFunctionResult, + encodeFunctionData +} from "viem" +import type { + GetSmartAccountParameter, + SmartAccount +} from "viem/account-abstraction" +import { call, readContract } from "viem/actions" +import { getAction } from "viem/utils" +import { parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import type { ModuleType } from "../../../modules/utils/Types" + +export type SupportsModuleParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter & { + type: ModuleType +} + +/** + * Parses a module type to its corresponding ID. + * + * @param type - The module type to parse. + * @returns The corresponding bigint ID for the module type. + * @throws {Error} If an invalid module type is provided. + */ +export function parseModuleTypeId(type: ModuleType): bigint { + switch (type) { + case "validator": + return BigInt(1) + case "executor": + return BigInt(2) + case "fallback": + return BigInt(3) + case "hook": + return BigInt(4) + default: + throw new Error("Invalid module type") + } +} + +/** + * Checks if a smart account supports a specific module type. + * + * @param client - The client instance. + * @param args - Parameters including the smart account and module type to check. + * @returns A boolean indicating whether the module type is supported. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { supportsModule } from '@biconomy/sdk' + * + * const isSupported = await supportsModule(nexusClient, { + * type: 'executor' + * }) + * console.log(isSupported) // true or false + */ +export async function supportsModule< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + args: SupportsModuleParameters +): Promise { + const { account: account_ = client.account } = args + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const publicClient = account.client + + const abi = [ + { + name: "supportsModule", + type: "function", + stateMutability: "view", + inputs: [ + { + type: "uint256", + name: "moduleTypeId" + } + ], + outputs: [ + { + type: "bool" + } + ] + } + ] as const + + try { + return await getAction( + publicClient, + readContract, + "readContract" + )({ + abi, + functionName: "supportsModule", + args: [parseModuleTypeId(args.type)], + address: account.address + }) + } catch (error) { + if (error instanceof ContractFunctionExecutionError) { + const { factory, factoryData } = await account.getFactoryArgs() + + const result = await getAction( + publicClient, + call, + "call" + )({ + factory: factory, + factoryData: factoryData, + to: account.address, + data: encodeFunctionData({ + abi, + functionName: "supportsModule", + args: [parseModuleTypeId(args.type)] + }) + }) + + if (!result || !result.data) { + throw new Error("accountId result is empty") + } + + return decodeFunctionResult({ + abi, + functionName: "supportsModule", + data: result.data + }) + } + + throw error + } +} diff --git a/src/sdk/clients/decorators/erc7579/uninstallFallback.ts b/src/sdk/clients/decorators/erc7579/uninstallFallback.ts new file mode 100644 index 000000000..7a4b6dc37 --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/uninstallFallback.ts @@ -0,0 +1,113 @@ +import { + type Chain, + type Client, + type Hex, + type Transport, + encodeFunctionData, + getAddress +} from "viem" +import { + type GetSmartAccountParameter, + type SmartAccount, + sendUserOperation +} from "viem/account-abstraction" +import { getAction } from "viem/utils" +import { parseAccount } from "viem/utils" +import type { Module } from "." +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import { parseModuleTypeId } from "./supportsModule" + +export type UninstallFallbackParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter & { + module: Module + maxFeePerGas?: bigint + maxPriorityFeePerGas?: bigint + nonce?: bigint +} + +/** + * Uninstalls a fallback module from a smart account. + * + * @param client - The client instance. + * @param parameters - Parameters including the smart account, module to uninstall, and optional gas settings. + * @returns The hash of the user operation as a hexadecimal string. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { uninstallFallback } from '@biconomy/sdk' + * + * const userOpHash = await uninstallFallback(nexusClient, { + * module: { + * type: 'fallback', + * address: '0x...', + * context: '0x' + * } + * }) + * console.log(userOpHash) // '0x...' + */ +export async function uninstallFallback< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + parameters: UninstallFallbackParameters +): Promise { + const { + account: account_ = client.account, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + module: { address, context, type } + } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + return getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ + calls: [ + { + to: account.address, + value: BigInt(0), + data: encodeFunctionData({ + abi: [ + { + name: "uninstallFallback", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { + type: "uint256", + name: "moduleTypeId" + }, + { + type: "address", + name: "module" + }, + { + type: "bytes", + name: "deInitData" + } + ], + outputs: [] + } + ], + functionName: "uninstallFallback", + args: [parseModuleTypeId(type), getAddress(address), context] + }) + } + ], + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + account: account + }) +} diff --git a/src/sdk/clients/decorators/erc7579/uninstallModule.ts b/src/sdk/clients/decorators/erc7579/uninstallModule.ts new file mode 100644 index 000000000..03f29878f --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/uninstallModule.ts @@ -0,0 +1,113 @@ +import { + type Chain, + type Client, + type Hex, + type Transport, + encodeFunctionData, + getAddress +} from "viem" +import { + type GetSmartAccountParameter, + type SmartAccount, + sendUserOperation +} from "viem/account-abstraction" +import { getAction } from "viem/utils" +import { parseAccount } from "viem/utils" +import type { Module } from "." +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import { parseModuleTypeId } from "./supportsModule" + +export type UninstallModuleParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter & { + module: Module + maxFeePerGas?: bigint + maxPriorityFeePerGas?: bigint + nonce?: bigint +} + +/** + * Uninstalls a module from a smart account. + * + * @param client - The client instance. + * @param parameters - Parameters including the smart account, module to uninstall, and optional gas settings. + * @returns The hash of the user operation as a hexadecimal string. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { uninstallModule } from '@biconomy/sdk' + * + * const userOpHash = await uninstallModule(nexusClient, { + * module: { + * type: 'executor', + * address: '0x...', + * context: '0x' + * } + * }) + * console.log(userOpHash) // '0x...' + */ +export async function uninstallModule< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + parameters: UninstallModuleParameters +): Promise { + const { + account: account_ = client.account, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + module: { address, context, type } + } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + return getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ + calls: [ + { + to: account.address, + value: BigInt(0), + data: encodeFunctionData({ + abi: [ + { + name: "uninstallModule", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { + type: "uint256", + name: "moduleTypeId" + }, + { + type: "address", + name: "module" + }, + { + type: "bytes", + name: "deInitData" + } + ], + outputs: [] + } + ], + functionName: "uninstallModule", + args: [parseModuleTypeId(type), getAddress(address), context] + }) + } + ], + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + account: account + }) +} diff --git a/src/sdk/clients/decorators/erc7579/uninstallModules.ts b/src/sdk/clients/decorators/erc7579/uninstallModules.ts new file mode 100644 index 000000000..0c28dd4de --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/uninstallModules.ts @@ -0,0 +1,110 @@ +import { + type Chain, + type Client, + type Hex, + type Transport, + encodeFunctionData, + getAddress +} from "viem" +import { + type GetSmartAccountParameter, + type SmartAccount, + sendUserOperation +} from "viem/account-abstraction" +import { getAction } from "viem/utils" +import { parseAccount } from "viem/utils" +import type { Module } from "." +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import { parseModuleTypeId } from "./supportsModule" + +export type UninstallModulesParameters< + TSmartAccount extends SmartAccount | undefined +> = GetSmartAccountParameter & { + modules: Module[] + maxFeePerGas?: bigint + maxPriorityFeePerGas?: bigint + nonce?: bigint +} + +/** + * Uninstalls multiple modules from a smart account. + * + * @param client - The client instance. + * @param parameters - Parameters including the smart account, modules to uninstall, and optional gas settings. + * @returns The hash of the user operation as a hexadecimal string. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { uninstallModules } from '@biconomy/sdk' + * + * const userOpHash = await uninstallModules(nexusClient, { + * modules: [ + * { type: 'executor', address: '0x...', context: '0x' }, + * { type: 'validator', address: '0x...', context: '0x' } + * ] + * }) + * console.log(userOpHash) // '0x...' + */ +export async function uninstallModules< + TSmartAccount extends SmartAccount | undefined +>( + client: Client, + parameters: UninstallModulesParameters +): Promise { + const { + account: account_ = client.account, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + modules + } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + return getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ + calls: modules.map(({ type, address, context }) => ({ + to: account.address, + value: BigInt(0), + data: encodeFunctionData({ + abi: [ + { + name: "uninstallModule", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { + type: "uint256", + name: "moduleTypeId" + }, + { + type: "address", + name: "module" + }, + { + type: "bytes", + name: "deInitData" + } + ], + outputs: [] + } + ], + functionName: "uninstallModule", + args: [parseModuleTypeId(type), getAddress(address), context] + }) + })), + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + account + }) +} diff --git a/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts b/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts new file mode 100644 index 000000000..7dfa9c8d8 --- /dev/null +++ b/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts @@ -0,0 +1,139 @@ +import { http, type Account, type Address, type Chain, isHex } from "viem" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { CounterAbi } from "../../../../test/__contracts/abi" +import { mockAddresses } from "../../../../test/__contracts/mockAddresses" +import { toNetwork } from "../../../../test/testSetup" +import { + type MasterClient, + type NetworkConfig, + fundAndDeployClients, + getBalance, + getTestAccount, + killNetwork, + toTestClient +} from "../../../../test/testUtils" +import { type NexusClient, createNexusClient } from "../../createNexusClient" + +describe("account.decorators", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let account: Account + let nexusClient: NexusClient + let nexusAccountAddress: Address + let recipient: Account + let recipientAddress: Address + + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + account = getTestAccount(0) + recipient = getTestAccount(1) + recipientAddress = recipient.address + testClient = toTestClient(chain, getTestAccount(5)) + + nexusClient = await createNexusClient({ + holder: account, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() + await fundAndDeployClients(testClient, [nexusClient]) + }) + + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test.concurrent("should sign a message", async () => { + const signedMessage = await nexusClient.signMessage({ message: "hello" }) + + expect(signedMessage).toEqual( + "0x6854688d3d9a87a33addd5f4deb5cea1b97fa5b7f16ea9a3478698f695fd1401bfe27e9e4a7e8e3da94aa72b021125e31fa899cc573c48ea3fe1d4ab61a9db10c19032026e3ed2dbccba5a178235ac27f94504311c" + ) + }) + + test.concurrent("should currently fail to sign with typed data", async () => { + expect( + nexusClient.signTypedData({ + domain: { + name: "Ether Mail", + version: "1", + chainId: 1, + verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + types: { + Person: [ + { name: "name", type: "string" }, + { name: "wallet", type: "address" } + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person" }, + { name: "contents", type: "string" } + ] + }, + primaryType: "Mail", + message: { + from: { + name: "Cow", + wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + to: { + name: "Bob", + wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + contents: "Hello, Bob!" + } + }) + ).rejects.toThrow() + }) + + test("should send a user operation using sendTransaction", async () => { + const balanceBefore = await getBalance(testClient, recipientAddress) + const hash = await nexusClient.sendTransaction({ + calls: [ + { + to: recipientAddress, + value: 1n + } + ] + }) + const { status } = await testClient.waitForTransactionReceipt({ hash }) + const balanceAfter = await getBalance(testClient, recipientAddress) + expect(status).toBe("success") + expect(balanceAfter - balanceBefore).toBe(1n) + }) + + test("should write to a contract", async () => { + const counterValueBefore = await testClient.readContract({ + abi: CounterAbi, + functionName: "getNumber", + address: mockAddresses.Counter + }) + + expect(counterValueBefore).toBe(0n) + const hash = await nexusClient.writeContract({ + abi: CounterAbi, + functionName: "incrementNumber", + address: mockAddresses.Counter, + chain + }) + const { status } = await testClient.waitForTransactionReceipt({ hash }) + const counterValueAfter = await testClient.readContract({ + abi: CounterAbi, + functionName: "getNumber", + address: mockAddresses.Counter + }) + + expect(status).toBe("success") + expect(counterValueAfter).toBe(1n) + }) +}) diff --git a/src/sdk/clients/decorators/smartAccount/index.ts b/src/sdk/clients/decorators/smartAccount/index.ts new file mode 100644 index 000000000..d0bbb611d --- /dev/null +++ b/src/sdk/clients/decorators/smartAccount/index.ts @@ -0,0 +1,324 @@ +import type { + Abi, + Chain, + Client, + ContractFunctionArgs, + ContractFunctionName, + Hash, + SendTransactionParameters, + Transport, + TypedData, + WriteContractParameters +} from "viem" +import type { SmartAccount } from "viem/account-abstraction" +import { sendTransaction } from "./sendTransaction" +import { signMessage } from "./signMessage" +import { signTypedData } from "./signTypedData" +import { writeContract } from "./writeContract" + +export type SmartAccountActions< + TChain extends Chain | undefined = Chain | undefined, + TSmartAccount extends SmartAccount | undefined = SmartAccount | undefined +> = { + /** + * Creates, signs, and sends a new transaction to the network. + * This function also allows you to sponsor this transaction if sender is a smartAccount + * + * - Docs: https://viem.sh/docs/actions/wallet/sendTransaction.html + * - Examples: https://stackblitz.com/github/wagmi-dev/viem/tree/main/examples/transactions/sending-transactions + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`eth_sendTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction) + * - Local Accounts: [`eth_sendRawTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendrawtransaction) + * + * @param args - {@link SendTransactionParameters} + * @returns The [Transaction](https://viem.sh/docs/glossary/terms.html#transaction) hash. {@link SendTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const hash = await client.sendTransaction({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: http(), + * }) + * const hash = await client.sendTransaction({ + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }) + */ + sendTransaction: < + TChainOverride extends Chain | undefined = undefined, + accountOverride extends SmartAccount | undefined = undefined, + calls extends readonly unknown[] = readonly unknown[] + >( + args: Parameters< + typeof sendTransaction< + TSmartAccount, + TChain, + accountOverride, + TChainOverride, + calls + > + >[1] + ) => Promise + /** + * Calculates an Ethereum-specific signature in [EIP-191 format](https://eips.ethereum.org/EIPS/eip-191): `keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))`. + * + * - Docs: https://viem.sh/docs/actions/wallet/signMessage.html + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`personal_sign`](https://docs.metamask.io/guide/signing-data.html#personal-sign) + * - Local Accounts: Signs locally. No JSON-RPC request. + * + * With the calculated signature, you can: + * - use [`verifyMessage`](https://viem.sh/docs/utilities/verifyMessage.html) to verify the signature, + * - use [`recoverMessageAddress`](https://viem.sh/docs/utilities/recoverMessageAddress.html) to recover the signing address from a signature. + * + * @param args - {@link SignMessageParameters} + * @returns The signed message. {@link SignMessageReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const signature = await client.signMessage({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * message: 'hello world', + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: http(), + * }) + * const signature = await client.signMessage({ + * message: 'hello world', + * }) + */ + signMessage: ( + args: Parameters>[1] + ) => ReturnType> + /** + * Signs typed data and calculates an Ethereum-specific signature in [EIP-191 format](https://eips.ethereum.org/EIPS/eip-191): `keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))`. + * + * - Docs: https://viem.sh/docs/actions/wallet/signTypedData.html + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`eth_signTypedData_v4`](https://docs.metamask.io/guide/signing-data.html#signtypeddata-v4) + * - Local Accounts: Signs locally. No JSON-RPC request. + * + * @param client - Client to use + * @param args - {@link SignTypedDataParameters} + * @returns The signed data. {@link SignTypedDataReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const signature = await client.signTypedData({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * domain: { + * name: 'Ether Mail', + * version: '1', + * chainId: 1, + * verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + * }, + * types: { + * Person: [ + * { name: 'name', type: 'string' }, + * { name: 'wallet', type: 'address' }, + * ], + * Mail: [ + * { name: 'from', type: 'Person' }, + * { name: 'to', type: 'Person' }, + * { name: 'contents', type: 'string' }, + * ], + * }, + * primaryType: 'Mail', + * message: { + * from: { + * name: 'Cow', + * wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + * }, + * to: { + * name: 'Bob', + * wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + * }, + * contents: 'Hello, Bob!', + * }, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: http(), + * }) + * const signature = await client.signTypedData({ + * domain: { + * name: 'Ether Mail', + * version: '1', + * chainId: 1, + * verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + * }, + * types: { + * Person: [ + * { name: 'name', type: 'string' }, + * { name: 'wallet', type: 'address' }, + * ], + * Mail: [ + * { name: 'from', type: 'Person' }, + * { name: 'to', type: 'Person' }, + * { name: 'contents', type: 'string' }, + * ], + * }, + * primaryType: 'Mail', + * message: { + * from: { + * name: 'Cow', + * wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + * }, + * to: { + * name: 'Bob', + * wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + * }, + * contents: 'Hello, Bob!', + * }, + * }) + */ + signTypedData: < + const TTypedData extends TypedData | { [key: string]: unknown }, + TPrimaryType extends string + >( + args: Parameters< + typeof signTypedData + >[1] + ) => ReturnType> + /** + * Executes a write function on a contract. + * This function also allows you to sponsor this transaction if sender is a smartAccount + * + * - Docs: https://viem.sh/docs/contract/writeContract.html + * - Examples: https://stackblitz.com/github/wagmi-dev/viem/tree/main/examples/contracts/writing-to-contracts + * + * A "write" function on a Solidity contract modifies the state of the blockchain. These types of functions require gas to be executed, and hence a [Transaction](https://viem.sh/docs/glossary/terms.html) is needed to be broadcast in order to change the state. + * + * Internally, uses a [Wallet Client](https://viem.sh/docs/clients/wallet.html) to call the [`sendTransaction` action](https://viem.sh/docs/actions/wallet/sendTransaction.html) with [ABI-encoded `data`](https://viem.sh/docs/contract/encodeFunctionData.html). + * + * __Warning: The `write` internally sends a transaction – it does not validate if the contract write will succeed (the contract may throw an error). It is highly recommended to [simulate the contract write with `contract.simulate`](https://viem.sh/docs/contract/writeContract.html#usage) before you execute it.__ + * + * @param args - {@link WriteContractParameters} + * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} + * + * @example + * import { createWalletClient, custom, parseAbi } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const hash = await client.writeContract({ + * address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + * abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), + * functionName: 'mint', + * args: [69420], + * }) + * + * @example + * // With Validation + * import { createWalletClient, custom, parseAbi } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const { request } = await client.simulateContract({ + * address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + * abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), + * functionName: 'mint', + * args: [69420], + * } + * const hash = await client.writeContract(request) + */ + writeContract: < + const TAbi extends Abi | readonly unknown[], + TFunctionName extends ContractFunctionName< + TAbi, + "nonpayable" | "payable" + > = ContractFunctionName, + TArgs extends ContractFunctionArgs< + TAbi, + "nonpayable" | "payable", + TFunctionName + > = ContractFunctionArgs, + TChainOverride extends Chain | undefined = undefined + >( + args: WriteContractParameters< + TAbi, + TFunctionName, + TArgs, + TChain, + TSmartAccount, + TChainOverride + > + ) => ReturnType< + typeof writeContract< + TChain, + TSmartAccount, + TAbi, + TFunctionName, + TArgs, + TChainOverride + > + > +} + +export function smartAccountActions() { + return < + TChain extends Chain | undefined = Chain | undefined, + TSmartAccount extends SmartAccount | undefined = SmartAccount | undefined + >( + client: Client + ): SmartAccountActions => ({ + sendTransaction: (args) => sendTransaction(client, args as any), + signMessage: (args) => signMessage(client, args), + signTypedData: (args) => signTypedData(client, args), + writeContract: (args) => writeContract(client, args) + }) +} diff --git a/src/sdk/clients/decorators/smartAccount/sendTransaction.ts b/src/sdk/clients/decorators/smartAccount/sendTransaction.ts new file mode 100644 index 000000000..c282896dc --- /dev/null +++ b/src/sdk/clients/decorators/smartAccount/sendTransaction.ts @@ -0,0 +1,105 @@ +import type { + Chain, + Client, + Hash, + SendTransactionParameters, + Transport +} from "viem" +import { + type SendUserOperationParameters, + type SmartAccount, + sendUserOperation, + waitForUserOperationReceipt +} from "viem/account-abstraction" +import { getAction, parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" + +/** + * Creates, signs, and sends a new transaction to the network using a smart account. + * This function also allows you to sponsor this transaction if the sender is a smart account. + * + * @param client - The client instance. + * @param args - Parameters for sending the transaction or user operation. + * @returns The transaction hash as a hexadecimal string. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { sendTransaction } from '@biconomy/sdk' + * + * const hash = await sendTransaction(nexusClient, { + * to: '0x...', + * value: parseEther('0.1'), + * data: '0x...' + * }) + * console.log(hash) // '0x...' + */ +export async function sendTransaction< + account extends SmartAccount | undefined, + chain extends Chain | undefined, + accountOverride extends SmartAccount | undefined = undefined, + chainOverride extends Chain | undefined = Chain | undefined, + calls extends readonly unknown[] = readonly unknown[] +>( + client: Client, + args: + | SendTransactionParameters + | SendUserOperationParameters +): Promise { + let userOpHash: Hash + + if ("to" in args) { + const { + account: account_ = client.account, + data, + maxFeePerGas, + maxPriorityFeePerGas, + to, + value, + nonce + } = args + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + if (!to) throw new Error("Missing to address") + + userOpHash = await getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ + calls: [ + { + to, + value: value || BigInt(0), + data: data || "0x" + } + ], + account, + maxFeePerGas, + maxPriorityFeePerGas, + nonce: nonce ? BigInt(nonce) : undefined + }) + } else { + userOpHash = await getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ ...args } as SendUserOperationParameters) + } + + const userOperationReceipt = await getAction( + client, + waitForUserOperationReceipt, + "waitForUserOperationReceipt" + )({ + hash: userOpHash + }) + + return userOperationReceipt?.receipt.transactionHash +} diff --git a/src/sdk/clients/decorators/smartAccount/signMessage.ts b/src/sdk/clients/decorators/smartAccount/signMessage.ts new file mode 100644 index 000000000..bf5ebb556 --- /dev/null +++ b/src/sdk/clients/decorators/smartAccount/signMessage.ts @@ -0,0 +1,46 @@ +import type { + Chain, + Client, + SignMessageParameters, + SignMessageReturnType, + Transport +} from "viem" +import type { SmartAccount } from "viem/account-abstraction" +import { parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" + +/** + * Signs a message using the smart account. + * + * This function calculates an Ethereum-specific signature in [EIP-191 format](https://eips.ethereum.org/EIPS/eip-191): + * `keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))`. + * + * @param client - The client instance. + * @param parameters - Parameters for signing the message. + * @returns The signature as a hexadecimal string. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { signMessage } from '@biconomy/sdk' + * + * const signature = await signMessage(nexusClient, { + * message: 'Hello, Biconomy!' + * }) + * console.log(signature) // '0x...' + */ +export async function signMessage( + client: Client, + { + account: account_ = client.account, + message + }: SignMessageParameters +): Promise { + if (!account_) + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/signMessage" + }) + + const account = parseAccount(account_) as SmartAccount + + return account.signMessage({ message }) +} diff --git a/src/sdk/clients/decorators/smartAccount/signTypedData.ts b/src/sdk/clients/decorators/smartAccount/signTypedData.ts new file mode 100644 index 000000000..b4db03271 --- /dev/null +++ b/src/sdk/clients/decorators/smartAccount/signTypedData.ts @@ -0,0 +1,113 @@ +import { + type Chain, + type Client, + type SignTypedDataParameters, + type SignTypedDataReturnType, + type Transport, + type TypedData, + type TypedDataDefinition, + type TypedDataDomain, + getTypesForEIP712Domain, + validateTypedData +} from "viem" +import type { SmartAccount } from "viem/account-abstraction" +import { parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" + +/** + * Signs typed data using the smart account. + * + * This function calculates an Ethereum-specific signature in [EIP-712 format](https://eips.ethereum.org/EIPS/eip-712): + * `sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message)))` + * + * @param client - The client instance. + * @param parameters - Parameters for signing the typed data. + * @returns The signature as a hexadecimal string. + * @throws {AccountNotFoundError} If the account is not found. + * + * @example + * import { signTypedData } from '@biconomy/sdk' + * import { keccak256, encodeAbiParameters, parseAbiParameters } from 'viem' + * + * const domain = { + * name: 'Ether Mail', + * version: '1', + * chainId: 1, + * verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' + * } + * + * const types = { + * Person: [ + * { name: 'name', type: 'string' }, + * { name: 'wallet', type: 'address' } + * ], + * Mail: [ + * { name: 'from', type: 'Person' }, + * { name: 'to', type: 'Person' }, + * { name: 'contents', type: 'string' } + * ] + * } + * + * const message = { + * from: { + * name: 'Cow', + * wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' + * }, + * to: { + * name: 'Bob', + * wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' + * }, + * contents: 'Hello, Bob!' + * } + * + * const signature = await signTypedData(nexusClient, { + * domain, + * types, + * primaryType: 'Mail', + * message + * }) + * console.log(signature) // '0x...' + */ +export async function signTypedData< + const TTypedData extends TypedData | { [key: string]: unknown }, + TPrimaryType extends string, + TAccount extends SmartAccount | undefined = SmartAccount | undefined +>( + client: Client, + { + account: account_ = client.account, + domain, + message, + primaryType, + types: types_ + }: SignTypedDataParameters +): Promise { + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/docs/actions/wallet/signMessage" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const types = { + EIP712Domain: getTypesForEIP712Domain({ domain } as { + domain: TypedDataDomain + }), + ...(types_ as TTypedData) + } + + validateTypedData({ + domain, + message, + primaryType, + types + } as TypedDataDefinition) + + return account.signTypedData({ + domain, + primaryType, + types, + message + } as TypedDataDefinition) +} diff --git a/src/sdk/clients/decorators/smartAccount/writeContract.ts b/src/sdk/clients/decorators/smartAccount/writeContract.ts new file mode 100644 index 000000000..10470875b --- /dev/null +++ b/src/sdk/clients/decorators/smartAccount/writeContract.ts @@ -0,0 +1,93 @@ +import { + type Abi, + type Chain, + type Client, + type ContractFunctionArgs, + type ContractFunctionName, + type EncodeFunctionDataParameters, + type Hash, + type SendTransactionParameters, + type Transport, + type WriteContractParameters, + encodeFunctionData +} from "viem" +import type { SmartAccount } from "viem/account-abstraction" +import { getAction } from "viem/utils" +import { sendTransaction } from "./sendTransaction" + +/** + * Executes a write operation on a smart contract using a smart account. + * + * @param client - The client instance. + * @param parameters - Parameters for the contract write operation. + * @returns The transaction hash as a hexadecimal string. + * @throws {Error} If the 'to' address is missing in the request. + * + * @example + * import { writeContract } from '@biconomy/sdk' + * import { encodeFunctionData } from 'viem' + * + * const encodedCall = encodeFunctionData({ + * abi: CounterAbi, + * functionName: "incrementNumber" + * }) + * const call = { + * to: '0x61f70428b61864B38D9B45b7B032c700B960acCD', + * data: encodedCall + * } + * const hash = await writeContract(nexusClient, call) + * console.log(hash) // '0x...' + */ +export async function writeContract< + TChain extends Chain | undefined, + TAccount extends SmartAccount | undefined, + const TAbi extends Abi | readonly unknown[], + TFunctionName extends ContractFunctionName< + TAbi, + "nonpayable" | "payable" + > = ContractFunctionName, + TArgs extends ContractFunctionArgs< + TAbi, + "nonpayable" | "payable", + TFunctionName + > = ContractFunctionArgs, + TChainOverride extends Chain | undefined = undefined +>( + client: Client, + { + abi, + address, + args, + dataSuffix, + functionName, + ...request + }: WriteContractParameters< + TAbi, + TFunctionName, + TArgs, + TChain, + TAccount, + TChainOverride + > +): Promise { + const data = encodeFunctionData({ + abi, + args, + functionName + } as EncodeFunctionDataParameters) + + const hash = await getAction( + client, + sendTransaction, + "sendTransaction" + )({ + data: `${data}${dataSuffix ? dataSuffix.replace("0x", "") : ""}`, + to: address, + ...request + } as unknown as SendTransactionParameters< + Chain | undefined, + TAccount, + undefined + >) + return hash +} diff --git a/src/sdk/clients/index.ts b/src/sdk/clients/index.ts new file mode 100644 index 000000000..46973e1a4 --- /dev/null +++ b/src/sdk/clients/index.ts @@ -0,0 +1,5 @@ +export * from "./createBicoBundlerClient" +export * from "./createBicoPaymasterClient" +export * from "./createNexusClient" +export * from "./decorators/erc7579" +export * from "./decorators/smartAccount" diff --git a/src/index.ts b/src/sdk/index.ts similarity index 65% rename from src/index.ts rename to src/sdk/index.ts index 903f1f5b4..e5e14f2cb 100644 --- a/src/index.ts +++ b/src/sdk/index.ts @@ -1,3 +1,3 @@ export * from "./account" -export * from "./paymaster" export * from "./modules" +export * from "./clients" diff --git a/src/modules/base/BaseExecutionModule.ts b/src/sdk/modules/base/BaseExecutionModule.ts similarity index 54% rename from src/modules/base/BaseExecutionModule.ts rename to src/sdk/modules/base/BaseExecutionModule.ts index f63140753..678a7aa01 100644 --- a/src/modules/base/BaseExecutionModule.ts +++ b/src/sdk/modules/base/BaseExecutionModule.ts @@ -1,11 +1,10 @@ -import type { Address } from "viem" -import type { UserOpReceipt } from "../../bundler/index.js" -import { BaseModule } from "../base/BaseModule.js" +import type { Address, Hash } from "viem" import type { Execution } from "../utils/Types.js" +import { BaseModule } from "./BaseModule.js" export abstract class BaseExecutionModule extends BaseModule { abstract execute( execution: Execution | Execution[], ownedAccountAddress?: Address - ): Promise + ): Promise } diff --git a/src/modules/base/BaseModule.ts b/src/sdk/modules/base/BaseModule.ts similarity index 86% rename from src/modules/base/BaseModule.ts rename to src/sdk/modules/base/BaseModule.ts index 20eb272fc..7b6e9d216 100644 --- a/src/modules/base/BaseModule.ts +++ b/src/sdk/modules/base/BaseModule.ts @@ -1,30 +1,30 @@ import { type Address, type Hex, encodeFunctionData, parseAbi } from "viem" import contracts from "../../__contracts/index.js" -import type { SmartAccountSigner } from "../../account/index.js" +import type { Holder } from "../../account/utils/toHolder.js" +import type { Module } from "../../clients/decorators/erc7579/index.js" import { - type Module, type ModuleType, type ModuleVersion, moduleTypeIds } from "../utils/Types.js" export abstract class BaseModule { - moduleAddress: Address - data: Hex + address: Address + context: Hex additionalContext: Hex type: ModuleType hook?: Address version: ModuleVersion = "1.0.0-beta" entryPoint: Address = contracts.entryPoint.address - signer: SmartAccountSigner + holder: Holder - constructor(module: Module, signer: SmartAccountSigner) { - this.moduleAddress = module.moduleAddress - this.data = module.data ?? "0x" + constructor(module: Module, holder: Holder) { + this.address = module.address + this.context = module.context ?? "0x" this.additionalContext = module.additionalContext ?? "0x" this.hook = module.hook this.type = module.type - this.signer = signer + this.holder = holder } public installModule(): Hex { @@ -35,8 +35,8 @@ export abstract class BaseModule { functionName: "installModule", args: [ BigInt(moduleTypeIds[this.type]), - this.moduleAddress, - this.data ?? "0x" + this.address, + this.context ?? "0x" ] }) @@ -51,7 +51,7 @@ export abstract class BaseModule { functionName: "uninstallModule", args: [ BigInt(moduleTypeIds[this.type]), - this.moduleAddress, + this.address, uninstallData ?? "0x" ] }) @@ -93,7 +93,7 @@ export abstract class BaseModule { } public getAddress(): Hex { - return this.moduleAddress + return this.address } public getVersion(): string { diff --git a/src/modules/base/BaseValidationModule.ts b/src/sdk/modules/base/BaseValidationModule.ts similarity index 78% rename from src/modules/base/BaseValidationModule.ts rename to src/sdk/modules/base/BaseValidationModule.ts index 67f5af00d..6bc74508d 100644 --- a/src/modules/base/BaseValidationModule.ts +++ b/src/sdk/modules/base/BaseValidationModule.ts @@ -1,10 +1,10 @@ import { type Hex, getAddress } from "viem" -import type { SmartAccountSigner } from "../../account/index.js" -import { BaseModule } from "../base/BaseModule.js" +import type { Holder } from "../../account/utils/toHolder.js" +import { BaseModule } from "./BaseModule.js" export abstract class BaseValidationModule extends BaseModule { - public getSigner(): SmartAccountSigner { - return this.signer + public getHolder(): Holder { + return this.holder } getDummySignature(): Hex { @@ -19,16 +19,18 @@ export abstract class BaseValidationModule extends BaseModule { } async signUserOpHash(userOpHash: string): Promise { - const signature = await this.signer.signMessage({ raw: userOpHash as Hex }) + const signature = await this.holder.signMessage({ + message: { raw: userOpHash as Hex } + }) return signature as Hex } - async signMessageSmartAccountSigner( + async signMessageHolder( _message: string | Uint8Array, - signer: SmartAccountSigner + holder: Holder ): Promise { const message = typeof _message === "string" ? _message : { raw: _message } - let signature: `0x${string}` = await signer.signMessage(message) + let signature: `0x${string}` = await holder.signMessage({ message }) const potentiallyIncorrectV = Number.parseInt(signature.slice(-2), 16) if (![27, 28].includes(potentiallyIncorrectV)) { @@ -40,15 +42,15 @@ export abstract class BaseValidationModule extends BaseModule { } /** - * Signs a message using the appropriate method based on the type of signer. + * Signs a message using the appropriate method based on the type of holder. * * @param {Uint8Array | string} message - The message to be signed. * @returns {Promise} A promise resolving to the signature or error message. - * @throws {Error} If the signer type is invalid or unsupported. + * @throws {Error} If the holder type is invalid or unsupported. */ async signMessage(_message: Uint8Array | string): Promise { const message = typeof _message === "string" ? _message : { raw: _message } - let signature = await this.signer.signMessage(message) + let signature = await this.holder.signMessage({ message }) const potentiallyIncorrectV = Number.parseInt(signature.slice(-2), 16) if (![27, 28].includes(potentiallyIncorrectV)) { diff --git a/src/modules/executors/OwnableExecutor.ts b/src/sdk/modules/executors/OwnableExecutor.ts similarity index 64% rename from src/modules/executors/OwnableExecutor.ts rename to src/sdk/modules/executors/OwnableExecutor.ts index 76be22bb8..fd9b94d9e 100644 --- a/src/modules/executors/OwnableExecutor.ts +++ b/src/sdk/modules/executors/OwnableExecutor.ts @@ -1,67 +1,80 @@ import { + type Account, type Address, + type Chain, + type Hash, type Hex, + type PublicClient, + type Transport, + type WalletClient, encodeAbiParameters, encodeFunctionData, encodePacked, getAddress, parseAbi } from "viem" -import { SENTINEL_ADDRESS } from "../../account" -import type { NexusSmartAccount } from "../../account/NexusSmartAccount" -import type { UserOpReceipt } from "../../bundler" +import { SENTINEL_ADDRESS } from "../../account/utils/Constants" +import { type Holder, toHolder } from "../../account/utils/toHolder" +import type { NexusClient } from "../../clients/createNexusClient" +import type { Module } from "../../clients/decorators/erc7579" import { BaseExecutionModule } from "../base/BaseExecutionModule" -import type { Execution, Module } from "../utils/Types" - +import type { Execution } from "../utils/Types" export class OwnableExecutorModule extends BaseExecutionModule { - smartAccount!: NexusSmartAccount + public nexusClient: NexusClient public owners: Address[] - private address: Address + public override address: Address public constructor( module: Module, - smartAccount: NexusSmartAccount, + nexusClient: NexusClient, owners: Address[], - address: Address + address: Address, + holder: Holder ) { - super(module, smartAccount.getSigner()) - this.smartAccount = smartAccount + super(module, holder) + this.nexusClient = nexusClient this.owners = owners - this.data = module.data ?? "0x" + this.context = module.context ?? "0x" this.address = address } public static async create( - smartAccount: NexusSmartAccount, + nexusClient: NexusClient, address: Address, - data?: Hex + context?: Hex ): Promise { const module: Module = { - moduleAddress: address, + address: address, type: "executor", - data: data ?? "0x", + context: context ?? "0x", additionalContext: "0x" } - const owners = await smartAccount.publicClient.readContract({ + const owners = await ( + nexusClient.account.client as PublicClient + ).readContract({ address, abi: parseAbi([ "function getOwners(address account) external view returns (address[])" ]), functionName: "getOwners", - args: [await smartAccount.getAddress()] + args: [await nexusClient.account.getAddress()] + }) + const holder = await toHolder({ holder: nexusClient.account.client } as { + holder: WalletClient }) return new OwnableExecutorModule( module, - smartAccount, + nexusClient, owners as Address[], - address + address, + holder ) } public async execute( execution: Execution | Execution[], accountAddress?: Address - ): Promise { + ): Promise { let calldata: Hex if (Array.isArray(execution)) { calldata = encodeFunctionData({ @@ -70,7 +83,7 @@ export class OwnableExecutorModule extends BaseExecutionModule { "function executeBatchOnOwnedAccount(address ownedAccount, bytes callData)" ]), args: [ - accountAddress ?? (await this.smartAccount.getAddress()), + accountAddress ?? (await this.nexusClient.account.getAddress()), encodeAbiParameters( [ { @@ -103,7 +116,7 @@ export class OwnableExecutorModule extends BaseExecutionModule { "function executeOnOwnedAccount(address ownedAccount, bytes callData)" ]), args: [ - accountAddress ?? (await this.smartAccount.getAddress()), + accountAddress ?? (await this.nexusClient.account.getAddress()), encodePacked( ["address", "uint256", "bytes"], [ @@ -115,34 +128,23 @@ export class OwnableExecutorModule extends BaseExecutionModule { ] }) } - const response = await this.smartAccount.sendTransaction({ - to: this.moduleAddress, - data: calldata, - value: 0n + return this.nexusClient.sendTransaction({ + calls: [{ to: this.address, data: calldata, value: 0n }] }) - const receipt = await response.wait() - return receipt } - public async addOwner(newOwner: Address): Promise { + public async addOwner(newOwner: Address) { const callData = encodeFunctionData({ functionName: "addOwner", abi: parseAbi(["function addOwner(address owner)"]), args: [newOwner] }) - const response = await this.smartAccount.sendTransaction({ - to: this.moduleAddress, - data: callData, - value: 0n + return this.nexusClient.sendTransaction({ + calls: [{ to: this.address, data: callData, value: 0n }] }) - const receipt = await response.wait() - if (receipt.success) { - this.owners.push(newOwner) - } - return receipt } - public async removeOwner(ownerToRemove: Address): Promise { + public async removeOwner(ownerToRemove: Address) { const owners = await this.getOwners(this.address) let prevOwner: Address @@ -165,30 +167,30 @@ export class OwnableExecutorModule extends BaseExecutionModule { args: [prevOwner, ownerToRemove] }) - const response = await this.smartAccount.sendTransaction({ - to: this.moduleAddress, - data: calldata, - value: 0n + return this.nexusClient.sendTransaction({ + calls: [ + { + to: this.address, + data: calldata, + value: 0n + } + ] }) - - const receipt = await response.wait() - if (receipt.success) { - this.owners = this.owners.filter((o: Address) => o !== ownerToRemove) - } - return receipt } public async getOwners( moduleAddress: Address, accountAddress?: Address ): Promise { - const owners = await this.smartAccount.publicClient.readContract({ + const owners = await ( + this.nexusClient.account.client as PublicClient + ).readContract({ address: moduleAddress, abi: parseAbi([ "function getOwners(address account) external view returns (address[])" ]), functionName: "getOwners", - args: [accountAddress ?? (await this.smartAccount.getAddress())] + args: [accountAddress ?? (await this.nexusClient.account.getAddress())] }) return owners as Address[] diff --git a/src/modules/index.ts b/src/sdk/modules/index.ts similarity index 100% rename from src/modules/index.ts rename to src/sdk/modules/index.ts diff --git a/src/modules/interfaces/IExecutorModule.ts b/src/sdk/modules/interfaces/IExecutorModule.ts similarity index 100% rename from src/modules/interfaces/IExecutorModule.ts rename to src/sdk/modules/interfaces/IExecutorModule.ts diff --git a/src/modules/interfaces/IValidationModule.ts b/src/sdk/modules/interfaces/IValidationModule.ts similarity index 73% rename from src/modules/interfaces/IValidationModule.ts rename to src/sdk/modules/interfaces/IValidationModule.ts index a6bedc379..1ab6b5c67 100644 --- a/src/modules/interfaces/IValidationModule.ts +++ b/src/sdk/modules/interfaces/IValidationModule.ts @@ -1,10 +1,10 @@ import type { Hex } from "viem" -import type { SmartAccountSigner } from "../../account" +import type { Holder } from "../../account/utils/toHolder" export interface IValidationModule { getAddress(): Hex getInitData(): Promise - getSigner(): Promise + getHolder(): Promise signUserOpHash(_userOpHash: string): Promise signMessage(_message: string | Uint8Array): Promise getDummySignature(): Promise diff --git a/tests/smart.sessions.test.ts b/src/sdk/modules/smart.sessions.test.ts similarity index 62% rename from tests/smart.sessions.test.ts rename to src/sdk/modules/smart.sessions.test.ts index 5c7308c3c..de07b3101 100644 --- a/tests/smart.sessions.test.ts +++ b/src/sdk/modules/smart.sessions.test.ts @@ -1,100 +1,63 @@ -import { - http, - type Account, - type Chain, - type Hex, - type WalletClient, - createWalletClient, - pad, - toBytes, - toHex -} from "viem" +import { http, type Account, type Address, type Chain, pad, toHex } from "viem" import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { parseReferenceValue } from "../src" -import { - type NexusSmartAccount, - createSmartAccountClient -} from "../src/account" -import policies, { - ParamCondition, - type ActionConfig -} from "../src/modules/smartSessions" -import { TEST_CONTRACTS } from "./src/callDatas" -import { type TestFileNetworkType, toNetwork } from "./src/testSetup" +import { parseReferenceValue } from ".." +import { TEST_CONTRACTS } from "../../test/callDatas" +import { toNetwork } from "../../test/testSetup" import { + fundAndDeployClients, getTestAccount, killNetwork, - toTestClient, - topUp -} from "./src/testUtils" -import type { MasterClient, NetworkConfig } from "./src/testUtils" -const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" + toTestClient +} from "../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../test/testUtils" +import { + type NexusClient, + createNexusClient +} from "../clients/createNexusClient" +import policies, { ParamCondition } from "./smartSessions" -describe("smart.sessions", () => { +describe("smart.sessions", async () => { let network: NetworkConfig - // Nexus Config let chain: Chain let bundlerUrl: string - let walletClient: WalletClient // Test utils let testClient: MasterClient let account: Account - let recipientAccount: Account - let smartAccount: NexusSmartAccount - let smartAccountAddress: Hex + let nexusClient: NexusClient + let nexusAccountAddress: Address + let recipient: Account + let recipientAddress: Address beforeAll(async () => { - network = (await toNetwork(NETWORK_TYPE)) as NetworkConfig + network = await toNetwork() chain = network.chain bundlerUrl = network.bundlerUrl - account = getTestAccount(0) - recipientAccount = getTestAccount(3) + recipient = getTestAccount(1) + recipientAddress = recipient.address + testClient = toTestClient(chain, getTestAccount(5)) - walletClient = createWalletClient({ - account, + nexusClient = await createNexusClient({ + holder: account, chain, - transport: http() - }) - - testClient = toTestClient(chain, getTestAccount(0)) - - smartAccount = await createSmartAccountClient({ - signer: walletClient, - bundlerUrl, - chain + transport: http(), + bundlerTransport: http(bundlerUrl) }) - smartAccountAddress = await smartAccount.getAddress() + nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() + await fundAndDeployClients(testClient, [nexusClient]) }) + afterAll(async () => { await killNetwork([network?.rpcPort, network?.bundlerPort]) }) - test("should fund the smart account", async () => { - await topUp(testClient, smartAccountAddress) - const [balance] = await smartAccount.getBalances() - expect(balance.amount > 0) - }) - - test("should have account addresses", async () => { - const addresses = await Promise.all([ - account.address, - smartAccount.getAddress() - ]) - expect(addresses.every(Boolean)).toBeTruthy() - expect(addresses).toStrictEqual([ - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account - ]) - }) - test("should have smart account bytecode", async () => { const bytecodes = await Promise.all( [TEST_CONTRACTS.SmartSession, TEST_CONTRACTS.UniActionPolicy].map( - (address) => testClient.getBytecode(address) + (address) => testClient.getCode(address) ) ) expect(bytecodes.every((bytecode) => !!bytecode?.length)).toBeTruthy() diff --git a/src/modules/smartSessions.ts b/src/sdk/modules/smartSessions.ts similarity index 100% rename from src/modules/smartSessions.ts rename to src/sdk/modules/smartSessions.ts diff --git a/src/modules/utils/Constants.ts b/src/sdk/modules/utils/Constants.ts similarity index 100% rename from src/modules/utils/Constants.ts rename to src/sdk/modules/utils/Constants.ts diff --git a/src/modules/utils/Helper.ts b/src/sdk/modules/utils/Helper.ts similarity index 99% rename from src/modules/utils/Helper.ts rename to src/sdk/modules/utils/Helper.ts index a6250108c..8975e154f 100644 --- a/src/modules/utils/Helper.ts +++ b/src/sdk/modules/utils/Helper.ts @@ -14,7 +14,7 @@ import { ERROR_MESSAGES, type UserOperationStruct, getChain -} from "../../account" +} from "../../account/index.js" import type { ChainInfo, SignerData diff --git a/src/modules/utils/Types.ts b/src/sdk/modules/utils/Types.ts similarity index 63% rename from src/modules/utils/Types.ts rename to src/sdk/modules/utils/Types.ts index ada55804c..ba723048c 100644 --- a/src/modules/utils/Types.ts +++ b/src/sdk/modules/utils/Types.ts @@ -1,11 +1,6 @@ import type { Address, Chain, Hex } from "viem" -import type { - CallType, - SimulationType, - SmartAccountSigner, - SupportedSigner, - UserOperationStruct -} from "../../account" +import type { Holder, UnknownHolder } from "../../account/utils/toHolder" + export type ModuleVersion = "1.0.0-beta" // | 'V1_0_1' export interface BaseValidationModuleConfig { @@ -19,7 +14,7 @@ export interface K1ValidationModuleConfig extends BaseValidationModuleConfig { /** Version of the module */ version?: ModuleVersion /** Signer: viemWallet or ethers signer. Ingested when passed into smartAccount */ - signer: SupportedSigner + signer: UnknownHolder } export interface K1ValidatorModuleConfigConstructorProps @@ -28,41 +23,8 @@ export interface K1ValidatorModuleConfigConstructorProps moduleAddress?: Hex /** Version of the module */ version?: ModuleVersion - /** Signer: Converted from viemWallet or ethers signer to SmartAccountSigner */ - signer: SmartAccountSigner -} - -// export interface SessionKeyManagerModuleConfig -// extends BaseValidationModuleConfig { -// /** Address of the module */ -// moduleAddress?: Hex -// /** Version of the module */ -// version?: ModuleVersion -// /** SmartAccount address */ -// smartAccountAddress: Hex -// storageType?: StorageType -// sessionStorageClient?: ISessionStorage -// } - -// export interface BatchedSessionRouterModuleConfig -// extends BaseValidationModuleConfig { -// /** Address of the module */ -// moduleAddress?: Hex -// /** Version of the module */ -// version?: ModuleVersion -// /** Session Key Manager module: Could be BaseValidationModule */ -// sessionKeyManagerModule?: SessionKeyManagerModule -// /** Session Key Manager module address */ -// sessionManagerModuleAddress?: Hex -// /** Address of the associated smart account */ -// smartAccountAddress: Hex -// /** Storage type, e.g. local storage */ -// storageType?: StorageType -// } - -export enum StorageType { - LOCAL_STORAGE = 0, - MEMORY_STORAGE = 1 + /** Signer: Converted from viemWallet or ethers signer to Holder */ + holder: Holder } export type SessionDataTuple = [ @@ -78,7 +40,7 @@ export type SessionParams = { /** ID of the session */ sessionID?: string /** Session Signer: viemWallet or ethers signer. Ingested when passed into smartAccount */ - sessionSigner: SupportedSigner + sessionSigner: UnknownHolder /** The session validation module is a sub-module smart-contract which works with session key manager validation module. It validates the userop calldata against the defined session permissions (session key data) within the contract. */ sessionValidationModule?: Hex /** Additional info if needed to be appended in signature */ @@ -87,7 +49,7 @@ export type SessionParams = { export type StrictSessionParams = { sessionID: string - sessionSigner: SupportedSigner + sessionSigner: UnknownHolder } export type ModuleInfo = { @@ -95,7 +57,7 @@ export type ModuleInfo = { // sessionParams?: SessionParams[] // where SessionParams is below four sessionID?: string /** Session Signer: viemWallet or ethers signer. Ingested when passed into smartAccount */ - sessionSigner?: SupportedSigner + sessionHolder?: UnknownHolder /** The session validation module is a sub-module smart-contract which works with session key manager validation module. It validates the userop calldata against the defined session permissions (session key data) within the contract. */ sessionValidationModule?: Hex /** Additional info if needed to be appended in signature */ @@ -104,10 +66,7 @@ export type ModuleInfo = { batchSessionParams?: SessionParams[] } -export interface SendUserOpParams extends ModuleInfo { - /** "validation_and_execution" is recommended during development for improved debugging & devEx, but will add some additional latency to calls. "validation" can be used in production ro remove this latency once flows have been tested. */ - simulationType?: SimulationType -} +export interface SendUserOpParams extends ModuleInfo {} export type SignerData = { /** This is not the public as provided by viem, key but address for the given pvKey */ @@ -145,7 +104,7 @@ export interface MultiChainValidationModuleConfig /** Version of the module */ version?: ModuleVersion /** Signer: viemWallet or ethers signer. Ingested when passed into smartAccount */ - signer: SupportedSigner + signer: UnknownHolder } export interface MultiChainValidationModuleConfigConstructorProps extends BaseValidationModuleConfig { @@ -154,16 +113,7 @@ export interface MultiChainValidationModuleConfigConstructorProps /** Version of the module */ version?: ModuleVersion /** Signer: viemWallet or ethers signer. Ingested when passed into smartAccount */ - signer: SmartAccountSigner -} - -export type MultiChainUserOpDto = { - /** window end timestamp */ - validUntil?: number - /** window start timestamp */ - validAfter?: number - chainId: number - userOp: Partial + holder: Holder } export interface BaseSessionKeyData { @@ -203,30 +153,6 @@ export enum SafeHookType { SIG = 1 } -export type Module = { - moduleAddress: Address - data?: Hex - additionalContext?: Hex - type: ModuleType - - /* ---- kernel module params ---- */ - // these param needed for installing validator, executor, fallback handler - hook?: Address - /* ---- end kernel module params ---- */ - - /* ---- safe module params ---- */ - - // these two params needed for installing hooks - hookType?: SafeHookType - selector?: Hex - - // these two params needed for installing fallback handlers - functionSig?: Hex - callType?: CallType - - /* ---- end safe module params ---- */ -} - export type ModuleType = "validator" | "executor" | "fallback" | "hook" type ModuleTypeIds = { diff --git a/src/modules/utils/Uid.ts b/src/sdk/modules/utils/Uid.ts similarity index 100% rename from src/modules/utils/Uid.ts rename to src/sdk/modules/utils/Uid.ts diff --git a/src/sdk/modules/validators/K1ValidatorModule.ts b/src/sdk/modules/validators/K1ValidatorModule.ts new file mode 100644 index 000000000..eb9f7415f --- /dev/null +++ b/src/sdk/modules/validators/K1ValidatorModule.ts @@ -0,0 +1,25 @@ +import addresses from "../../__contracts/addresses.js" +import type { Holder } from "../../account/utils/toHolder.js" +import type { Module } from "../../clients/decorators/erc7579/index.js" +import { BaseValidationModule } from "../base/BaseValidationModule.js" + +export class K1ValidatorModule extends BaseValidationModule { + // biome-ignore lint/complexity/noUselessConstructor: + public constructor(moduleConfig: Module, holder: Holder) { + super(moduleConfig, holder) + } + + public static async create( + holder: Holder, + k1ValidatorAddress = addresses.K1Validator + ): Promise { + const module: Module = { + address: k1ValidatorAddress, + type: "validator", + context: holder.address, + additionalContext: "0x" + } + const instance = new K1ValidatorModule(module, holder) + return instance + } +} diff --git a/src/modules/validators/OwnableValidator.ts b/src/sdk/modules/validators/OwnableValidator.ts similarity index 100% rename from src/modules/validators/OwnableValidator.ts rename to src/sdk/modules/validators/OwnableValidator.ts diff --git a/src/sdk/modules/validators/ValidationModule.ts b/src/sdk/modules/validators/ValidationModule.ts new file mode 100644 index 000000000..4a57b9ce1 --- /dev/null +++ b/src/sdk/modules/validators/ValidationModule.ts @@ -0,0 +1,25 @@ +import type { Address, Hex } from "viem" +import type { Holder } from "../../account/utils/toHolder.js" +import type { Module } from "../../clients/decorators/erc7579/index.js" +import { BaseValidationModule } from "../base/BaseValidationModule.js" + +export class ValidationModule extends BaseValidationModule { + private constructor(moduleConfig: Module, holder: Holder) { + super(moduleConfig, holder) + } + + public static async create( + holder: Holder, + address: Address, + context: Hex + ): Promise { + const module: Module = { + address, + type: "validator", + context, + additionalContext: "0x" + } + const instance = new ValidationModule(module, holder) + return instance + } +} diff --git a/src/sdk/modules/validators/k1Validator.test.ts b/src/sdk/modules/validators/k1Validator.test.ts new file mode 100644 index 000000000..e50d06928 --- /dev/null +++ b/src/sdk/modules/validators/k1Validator.test.ts @@ -0,0 +1,127 @@ +import { http, type Account, type Address, type Chain } from "viem" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import { + fundAndDeployClients, + getBalance, + getTestAccount, + killNetwork, + toTestClient +} from "../../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../../test/testUtils" +import addresses from "../../__contracts/addresses" +import { + type NexusClient, + createNexusClient +} from "../../clients/createNexusClient" + +describe("modules.k1Validator.write", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let account: Account + let nexusClient: NexusClient + let nexusAccountAddress: Address + let recipient: Account + let recipientAddress: Address + + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + account = getTestAccount(0) + recipient = getTestAccount(1) + recipientAddress = recipient.address + + testClient = toTestClient(chain, getTestAccount(5)) + + nexusClient = await createNexusClient({ + holder: account, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() + await fundAndDeployClients(testClient, [nexusClient]) + }) + + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test.skip("should send eth", async () => { + const balanceBefore = await getBalance(testClient, recipientAddress) + const hash = await nexusClient.sendTransaction({ + calls: [ + { + to: recipientAddress, + value: 1n + } + ] + }) + const { success } = await nexusClient.waitForUserOperationReceipt({ hash }) + const balanceAfter = await getBalance(testClient, recipientAddress) + expect(success).toBe(true) + expect(balanceAfter - balanceBefore).toBe(1n) + }) + + test.skip("should install k1 validator with 1 owner", async () => { + const isInstalledBefore = await nexusClient.isModuleInstalled({ + module: { + type: "validator", + address: addresses.K1Validator, + context: "0x" + } + }) + + if (!isInstalledBefore) { + const hash = await nexusClient.installModule({ + module: { + address: addresses.K1Validator, + type: "validator", + context: "0x" + } + }) + + const { success: installSuccess } = + await nexusClient.waitForUserOperationReceipt({ hash }) + expect(installSuccess).toBe(true) + + const hashUninstall = await nexusClient.uninstallModule({ + module: { + address: addresses.K1Validator, + type: "validator", + context: "0x" + } + }) + + const { success: uninstallSuccess } = + await nexusClient.waitForUserOperationReceipt({ hash: hashUninstall }) + expect(uninstallSuccess).toBe(true) + } else { + // Uninstall + + const byteCode = await testClient.getCode({ + address: addresses.K1Validator + }) + + const hash = await nexusClient.uninstallModule({ + module: { + address: addresses.K1Validator, + type: "validator", + context: "0x" + } + }) + const { success } = await nexusClient.waitForUserOperationReceipt({ + hash + }) + expect(success).toBe(true) + } + // Get installed modules + }) +}) diff --git a/tests/src/README.md b/src/test/README.md similarity index 100% rename from tests/src/README.md rename to src/test/README.md diff --git a/tests/src/__contracts/abi/BiconomyMetaFactoryAbi.ts b/src/test/__contracts/abi/BiconomyMetaFactoryAbi.ts similarity index 100% rename from tests/src/__contracts/abi/BiconomyMetaFactoryAbi.ts rename to src/test/__contracts/abi/BiconomyMetaFactoryAbi.ts diff --git a/tests/src/__contracts/abi/BootstrapAbi.ts b/src/test/__contracts/abi/BootstrapAbi.ts similarity index 100% rename from tests/src/__contracts/abi/BootstrapAbi.ts rename to src/test/__contracts/abi/BootstrapAbi.ts diff --git a/tests/src/__contracts/abi/BootstrapLibAbi.ts b/src/test/__contracts/abi/BootstrapLibAbi.ts similarity index 100% rename from tests/src/__contracts/abi/BootstrapLibAbi.ts rename to src/test/__contracts/abi/BootstrapLibAbi.ts diff --git a/tests/src/__contracts/abi/CounterAbi.ts b/src/test/__contracts/abi/CounterAbi.ts similarity index 100% rename from tests/src/__contracts/abi/CounterAbi.ts rename to src/test/__contracts/abi/CounterAbi.ts diff --git a/tests/src/__contracts/abi/MockExecutorAbi.ts b/src/test/__contracts/abi/MockExecutorAbi.ts similarity index 100% rename from tests/src/__contracts/abi/MockExecutorAbi.ts rename to src/test/__contracts/abi/MockExecutorAbi.ts diff --git a/tests/src/__contracts/abi/MockHandlerAbi.ts b/src/test/__contracts/abi/MockHandlerAbi.ts similarity index 100% rename from tests/src/__contracts/abi/MockHandlerAbi.ts rename to src/test/__contracts/abi/MockHandlerAbi.ts diff --git a/tests/src/__contracts/abi/MockHookAbi.ts b/src/test/__contracts/abi/MockHookAbi.ts similarity index 100% rename from tests/src/__contracts/abi/MockHookAbi.ts rename to src/test/__contracts/abi/MockHookAbi.ts diff --git a/tests/src/__contracts/abi/MockRegistryAbi.ts b/src/test/__contracts/abi/MockRegistryAbi.ts similarity index 100% rename from tests/src/__contracts/abi/MockRegistryAbi.ts rename to src/test/__contracts/abi/MockRegistryAbi.ts diff --git a/tests/src/__contracts/abi/MockTokenAbi.ts b/src/test/__contracts/abi/MockTokenAbi.ts similarity index 100% rename from tests/src/__contracts/abi/MockTokenAbi.ts rename to src/test/__contracts/abi/MockTokenAbi.ts diff --git a/tests/src/__contracts/abi/MockValidatorAbi.ts b/src/test/__contracts/abi/MockValidatorAbi.ts similarity index 100% rename from tests/src/__contracts/abi/MockValidatorAbi.ts rename to src/test/__contracts/abi/MockValidatorAbi.ts diff --git a/tests/src/__contracts/abi/NexusAccountFactoryAbi.ts b/src/test/__contracts/abi/NexusAccountFactoryAbi.ts similarity index 100% rename from tests/src/__contracts/abi/NexusAccountFactoryAbi.ts rename to src/test/__contracts/abi/NexusAccountFactoryAbi.ts diff --git a/tests/src/__contracts/abi/StakeableAbi.ts b/src/test/__contracts/abi/StakeableAbi.ts similarity index 100% rename from tests/src/__contracts/abi/StakeableAbi.ts rename to src/test/__contracts/abi/StakeableAbi.ts diff --git a/tests/src/__contracts/abi/TokenWithPermitAbi.ts b/src/test/__contracts/abi/TokenWithPermitAbi.ts similarity index 100% rename from tests/src/__contracts/abi/TokenWithPermitAbi.ts rename to src/test/__contracts/abi/TokenWithPermitAbi.ts diff --git a/tests/src/__contracts/abi/index.ts b/src/test/__contracts/abi/index.ts similarity index 100% rename from tests/src/__contracts/abi/index.ts rename to src/test/__contracts/abi/index.ts index 50290d37d..d4c21de13 100644 --- a/tests/src/__contracts/abi/index.ts +++ b/src/test/__contracts/abi/index.ts @@ -8,6 +8,6 @@ export * from "./MockTokenAbi" export * from "./BootstrapLibAbi" export * from "./MockRegistryAbi" export * from "./MockHandlerAbi" -export * from "./TokenWithPermitAbi" export * from "./BootstrapAbi" export * from "./MockExecutorAbi" +export * from "./TokenWithPermitAbi" diff --git a/tests/src/__contracts/mockAddresses.ts b/src/test/__contracts/mockAddresses.ts similarity index 100% rename from tests/src/__contracts/mockAddresses.ts rename to src/test/__contracts/mockAddresses.ts diff --git a/tests/src/callDatas.ts b/src/test/callDatas.ts similarity index 100% rename from tests/src/callDatas.ts rename to src/test/callDatas.ts diff --git a/tests/src/executables.ts b/src/test/executables.ts similarity index 100% rename from tests/src/executables.ts rename to src/test/executables.ts diff --git a/tests/src/globalSetup.ts b/src/test/globalSetup.ts similarity index 97% rename from tests/src/globalSetup.ts rename to src/test/globalSetup.ts index b3e12288c..ac4620817 100644 --- a/tests/src/globalSetup.ts +++ b/src/test/globalSetup.ts @@ -5,6 +5,7 @@ import { } from "./testUtils" let globalConfig: NetworkConfigWithBundler +// @ts-ignore export const setup = async ({ provide }) => { globalConfig = await initLocalhostNetwork() const { bundlerInstance, instance, ...serializeableConfig } = globalConfig diff --git a/tests/playground.test.ts b/src/test/playground.test.ts similarity index 57% rename from tests/playground.test.ts rename to src/test/playground.test.ts index 1a689c397..176280202 100644 --- a/tests/playground.test.ts +++ b/src/test/playground.test.ts @@ -1,33 +1,28 @@ import { http, + type Address, type Chain, - type Hex, type PrivateKeyAccount, type PublicClient, type WalletClient, createPublicClient, createWalletClient } from "viem" -import { beforeAll, expect, test } from "vitest" +import { beforeAll, describe, expect, test } from "vitest" +import { createBicoPaymasterClient } from "../sdk/clients/createBicoPaymasterClient" import { - type NexusSmartAccount, - createSmartAccountClient -} from "../src/account" -import { - type TestFileNetworkType, - describeWithPlaygroundGuard, - toNetwork -} from "./src/testSetup" -import type { NetworkConfig } from "./src/testUtils" - -const NETWORK_TYPE: TestFileNetworkType = "PUBLIC_TESTNET" + type NexusClient, + createNexusClient +} from "../sdk/clients/createNexusClient" +import { playgroundTrue, toNetwork } from "./testSetup" +import type { NetworkConfig } from "./testUtils" // Remove the following lines to use the default factory and validator addresses // These are relevant only for now on base sopelia chain and are likely to change const k1ValidatorAddress = "0x663E709f60477f07885230E213b8149a7027239B" const factoryAddress = "0x887Ca6FaFD62737D0E79A2b8Da41f0B15A864778" -describeWithPlaygroundGuard("playground", () => { +describe.skipIf(!playgroundTrue)("playground", () => { let network: NetworkConfig // Nexus Config let chain: Chain @@ -38,17 +33,20 @@ describeWithPlaygroundGuard("playground", () => { // Test utils let publicClient: PublicClient // testClient not available on public testnets let account: PrivateKeyAccount - let smartAccount: NexusSmartAccount - let smartAccountAddress: Hex + let recipientAddress: Address + let nexusClient: NexusClient + let nexusAccountAddress: Address beforeAll(async () => { - network = await toNetwork(NETWORK_TYPE) + network = await toNetwork("PUBLIC_TESTNET") chain = network.chain bundlerUrl = network.bundlerUrl paymasterUrl = network.paymasterUrl account = network.account as PrivateKeyAccount + recipientAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" // vitalik.eth + walletClient = createWalletClient({ account, chain, @@ -59,24 +57,14 @@ describeWithPlaygroundGuard("playground", () => { chain, transport: http() }) - - smartAccount = await createSmartAccountClient({ - signer: walletClient, - bundlerUrl, - chain, - k1ValidatorAddress, - factoryAddress - }) - - smartAccountAddress = await smartAccount.getAddress() }) test("should have factory and k1Validator deployed", async () => { const byteCodes = await Promise.all([ - publicClient.getBytecode({ + publicClient.getCode({ address: k1ValidatorAddress }), - publicClient.getBytecode({ + publicClient.getCode({ address: factoryAddress }) ]) @@ -85,20 +73,19 @@ describeWithPlaygroundGuard("playground", () => { }) test("should init the smart account", async () => { - smartAccount = await createSmartAccountClient({ - signer: walletClient, + nexusClient = await createNexusClient({ + holder: account, chain, - bundlerUrl, - // Remove the following lines to use the default factory and validator addresses - // These are relevant only for now on sopelia chain and are likely to change + transport: http(), + bundlerTransport: http(bundlerUrl), k1ValidatorAddress, factoryAddress }) }) test("should log relevant addresses", async () => { - smartAccountAddress = await smartAccount.getAddress() - console.log({ smartAccountAddress }) + nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() + console.log({ nexusAccountAddress }) }) test("should check balances and top up relevant addresses", async () => { @@ -107,10 +94,9 @@ describeWithPlaygroundGuard("playground", () => { address: account.address }), publicClient.getBalance({ - address: smartAccountAddress + address: nexusAccountAddress }) ]) - console.log({ ownerBalance, smartAccountBalance }) const balancesAreOfCorrectType = [ownerBalance, smartAccountBalance].every( (balance) => typeof balance === "bigint" @@ -119,7 +105,7 @@ describeWithPlaygroundGuard("playground", () => { const hash = await walletClient.sendTransaction({ chain, account, - to: smartAccountAddress, + to: nexusAccountAddress, value: 1000000000000000000n }) const receipt = await publicClient.waitForTransactionReceipt({ hash }) @@ -130,27 +116,21 @@ describeWithPlaygroundGuard("playground", () => { test("should send some native token", async () => { const balanceBefore = await publicClient.getBalance({ - address: account.address + address: recipientAddress }) - - const { wait } = await smartAccount.sendTransaction({ - to: account.address, - data: "0x", - value: 1n + const hash = await nexusClient.sendTransaction({ + calls: [ + { + to: recipientAddress, + value: 1n + } + ] }) - - const { - success, - receipt: { transactionHash } - } = await wait() - expect(success).toBeTruthy() - - console.log({ transactionHash }) - + const { status } = await publicClient.waitForTransactionReceipt({ hash }) const balanceAfter = await publicClient.getBalance({ - address: account.address + address: recipientAddress }) - + expect(status).toBe("success") expect(balanceAfter - balanceBefore).toBe(1n) }) @@ -160,30 +140,26 @@ describeWithPlaygroundGuard("playground", () => { return } - const smartAccount = await createSmartAccountClient({ - signer: walletClient, + nexusClient = await createNexusClient({ + holder: account, chain, - paymasterUrl, - bundlerUrl, - // Remove the following lines to use the default factory and validator addresses - // These are relevant only for now on sopelia chain and are likely to change + transport: http(), + bundlerTransport: http(bundlerUrl), k1ValidatorAddress, - factoryAddress + factoryAddress, + paymaster: createBicoPaymasterClient({ + paymasterUrl + }) }) - expect(async () => - smartAccount.sendTransaction( - { - to: account.address, - data: "0x", - value: 1n - }, - { - paymasterServiceData: { - mode: "SPONSORED" + nexusClient.sendTransaction({ + calls: [ + { + to: account.address, + value: 1n } - } - ) + ] + }) ).rejects.toThrow("Error in generating paymasterAndData") }) }) diff --git a/tests/src/testSetup.ts b/src/test/testSetup.ts similarity index 83% rename from tests/src/testSetup.ts rename to src/test/testSetup.ts index 53505827e..dd9813f80 100644 --- a/tests/src/testSetup.ts +++ b/src/test/testSetup.ts @@ -1,4 +1,4 @@ -import { describe, inject, test } from "vitest" +import { inject, test } from "vitest" import { type FundedTestClients, type NetworkConfig, @@ -46,7 +46,7 @@ export type TestFileNetworkType = | "PUBLIC_TESTNET" export const toNetwork = async ( - networkType: TestFileNetworkType + networkType: TestFileNetworkType = "FILE_LOCALHOST" ): Promise => await (networkType === "COMMON_LOCALHOST" ? // @ts-ignore @@ -55,9 +55,5 @@ export const toNetwork = async ( ? initLocalhostNetwork() : initTestnetNetwork()) -export const describeWithPlaygroundGuard = - process.env.RUN_PLAYGROUND === "true" ? describe : describe.skip - -export const describeWithPaymasterGuard = process.env.PAYMASTER_URL - ? describe - : describe.skip +export const playgroundTrue = process.env.RUN_PLAYGROUND === "true" +export const paymasterTruthy = !!process.env.PAYMASTER_URL diff --git a/tests/src/testUtils.ts b/src/test/testUtils.ts similarity index 75% rename from tests/src/testUtils.ts rename to src/test/testUtils.ts index 95b9e44a1..ac6be0d16 100644 --- a/tests/src/testUtils.ts +++ b/src/test/testUtils.ts @@ -1,5 +1,6 @@ import { config } from "dotenv" import getPort from "get-port" +// @ts-ignore import { alto, anvil } from "prool/instances" import { http, @@ -18,24 +19,25 @@ import { parseAbiParameters, publicActions, toBytes, - walletActions + walletActions, + zeroAddress } from "viem" +import { createBundlerClient } from "viem/account-abstraction" import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts" +import contracts from "../sdk/__contracts" +import { getChain, getCustomChain } from "../sdk/account/utils" +import { Logger } from "../sdk/account/utils/Logger" import { - type EIP712DomainReturn, - type NexusSmartAccount, - createSmartAccountClient -} from "../../src" -import contracts from "../../src/__contracts" -import { getChain, getCustomChain } from "../../src/account/utils" -import { Logger } from "../../src/account/utils/Logger" -import { createBundler } from "../../src/bundler" + type NexusClient, + createNexusClient +} from "../sdk/clients/createNexusClient" + import { ENTRY_POINT_SIMULATIONS_CREATECALL, ENTRY_POINT_V07_CREATECALL, TEST_CONTRACTS } from "./callDatas" -import { clean, deploy, init } from "./executables" +import * as hardhatExec from "./executables" config() @@ -179,14 +181,14 @@ export const ensureBundlerIsReady = async ( bundlerUrl: string, chain: Chain ) => { - const bundler = await createBundler({ + const bundler = await createBundlerClient({ chain, - bundlerUrl + transport: http(bundlerUrl) }) while (true) { try { - await bundler.getGasFeeValues() + await bundler.getChainId() return } catch { await new Promise((resolve) => setTimeout(resolve, 1000)) @@ -205,18 +207,35 @@ export const toConfiguredAnvil = async ({ // forkUrl: "https://base-sepolia.gateway.tenderly.co/2oxlNZ7oiNCUpXzrWFuIHx" }) await instance.start() - console.log("") - console.log(`configuring module bytecode on http://localhost:${rpcPort}`) - await deployContracts(rpcPort) - await init() - await clean() - console.log(`deploying nexus contracts to http://localhost:${rpcPort}`) - await deploy(rpcPort) - console.log("deployment complete") - console.log("") + await initDeployments(rpcPort) return instance } +export const initDeployments = async (rpcPort: number) => { + // Hardhat deployment of nexus repo: + console.log( + `using hardhat to deploy nexus contracts to http://localhost:${rpcPort}` + ) + await hardhatExec.init() + await hardhatExec.clean() + await hardhatExec.deploy(rpcPort) + console.log("hardhat deployment complete.") + + // Hardcoded bytecode deployment of contracts using setCode: + console.log("setting bytecode with hardcoded calldata.") + const chain = getTestChainFromPort(rpcPort) + const account = getTestAccount() + const testClient = toTestClient(chain, account) + + // Dynamic bytecode deployment of contracts using setCode: + console.log("setting bytecode with dynamic calldata from a testnet") + await setByteCodeHardcoded(testClient) + await setByteCodeDynamic(testClient, TEST_CONTRACTS) + + console.log("bytecode deployment complete.") + console.log("") +} + const portOptions = { exclude: [] as number[] } export const initAnvilPayload = async (): Promise => { const rpcPort = await getPort(portOptions) @@ -236,12 +255,11 @@ export const initBundlerInstance = async ({ const bundlerInstance = await toBundlerInstance({ rpcUrl, bundlerPort }) return { bundlerInstance, bundlerUrl, bundlerPort } } - -export const checkBalance = ( +export const getBalance = ( testClient: MasterClient, owner: Hex, tokenAddress?: Hex -) => { +): Promise => { if (!tokenAddress) { return testClient.getBalance({ address: owner }) } @@ -252,7 +270,7 @@ export const checkBalance = ( ]), functionName: "balanceOf", args: [owner] - }) + }) as Promise } export const nonZeroBalance = async ( @@ -260,7 +278,7 @@ export const nonZeroBalance = async ( address: Hex, tokenAddress?: Hex ) => { - const balance = await checkBalance(testClient, address, tokenAddress) + const balance = await getBalance(testClient, address, tokenAddress) if (balance > BigInt(0)) return throw new Error( `Insufficient balance ${ @@ -291,21 +309,15 @@ export const toFundedTestClients = async ({ const testClient = toTestClient(chain, getTestAccount()) - const smartAccount = await createSmartAccountClient({ - signer: walletClient, - bundlerUrl, + const nexus = await createNexusClient({ + holder: account, + transport: http(), + bundlerTransport: http(bundlerUrl), chain }) - const recipientSmartAccount = await createSmartAccountClient({ - signer: recipientWalletClient, - bundlerUrl, - chain - }) - - const smartAccountAddress = await smartAccount.getAddress() - const recipientSmartAccountAddress = await recipientSmartAccount.getAddress() - await fundAndDeploy(testClient, [smartAccount, recipientSmartAccount]) + const smartAccountAddress = await nexus.account.getAddress() + await fundAndDeployClients(testClient, [nexus]) return { account, @@ -313,37 +325,50 @@ export const toFundedTestClients = async ({ walletClient, recipientWalletClient, testClient, - smartAccount, - recipientSmartAccount, - smartAccountAddress, - recipientSmartAccountAddress + nexus, + smartAccountAddress } } -export const fundAndDeploy = async ( +export const fundAndDeployClients = async ( testClient: MasterClient, - smartAccounts: NexusSmartAccount[] -) => - Promise.all( - smartAccounts.map((smartAccount) => - fundAndDeploySingleAccount(testClient, smartAccount) + nexusClients: NexusClient[] +) => { + return await Promise.all( + nexusClients.map((nexusClient) => + fundAndDeploySingleClient(testClient, nexusClient) ) ) +} -export const fundAndDeploySingleAccount = async ( +export const fundAndDeploySingleClient = async ( testClient: MasterClient, - smartAccount: NexusSmartAccount + nexusClient: NexusClient ) => { try { - const accountAddress = await smartAccount.getAddress() + const accountAddress = await nexusClient.account.getAddress() await topUp(testClient, accountAddress) - const { wait } = await smartAccount.deploy() - const { success } = await wait() - if (!success) { + + const hash = await nexusClient.sendTransaction({ + calls: [ + { + to: zeroAddress, + value: 0n + } + ] + }) + const { status, transactionHash } = + await testClient.waitForTransactionReceipt({ + hash + }) + + if (status !== "success") { throw new Error("Failed to deploy smart account") } + return transactionHash } catch (e) { - Logger.error(`Error initializing smart account: ${e}`) + console.error(`Error initializing smart account: ${e}`) + return Promise.resolve() } } @@ -366,7 +391,7 @@ export const topUp = async ( amount = 100000000000000000000n, token?: Hex ) => { - const balanceOfRecipient = await checkBalance(testClient, recipient, token) + const balanceOfRecipient = await getBalance(testClient, recipient, token) if (balanceOfRecipient > amount) { Logger.log( @@ -388,56 +413,25 @@ export const topUp = async ( functionName: "transfer", args: [recipient, amount] }) - await testClient.waitForTransactionReceipt({ hash }) + return await testClient.waitForTransactionReceipt({ hash }) } const hash = await testClient.sendTransaction({ to: recipient, value: amount }) - return testClient.waitForTransactionReceipt({ hash }) -} - -export const getAccountDomainStructFields = async ( - testClient: MasterClient, - accountAddress: Address -) => { - const accountDomainStructFields = (await testClient.readContract({ - address: accountAddress, - abi: parseAbi([ - "function eip712Domain() public view returns (bytes1 fields, string memory name, string memory version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] memory extensions)" - ]), - functionName: "eip712Domain" - })) as EIP712DomainReturn - - const [fields, name, version, chainId, verifyingContract, salt, extensions] = - accountDomainStructFields - - const params = parseAbiParameters([ - "bytes1, bytes32, bytes32, uint256, address, bytes32, bytes32" - ]) - - return encodeAbiParameters(params, [ - fields, - keccak256(toBytes(name)), - keccak256(toBytes(version)), - chainId, - verifyingContract, - salt, - keccak256(encodePacked(["uint256[]"], [extensions])) - ]) + return await testClient.waitForTransactionReceipt({ hash }) } export const getBundlerUrl = (chainId: number) => - `https://bundler.biconomy.io/api/v2/${chainId}/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f14` + `https://bundler.biconomy.io/api/v3/${chainId}/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f14` const getTestChainFromPort = (port: number): Chain => getCustomChain(`Anvil-${port}`, port, `http://localhost:${port}`, "") -const deployContracts = async (rpcPort: number): Promise => { +const setByteCodeHardcoded = async ( + testClient: MasterClient +): Promise => { const DETERMINISTIC_DEPLOYER = "0x4e59b44847b379578588920ca78fbf26c0b4956c" - const chain = getTestChainFromPort(rpcPort) - const account = getTestAccount() - const testClient = toTestClient(chain, account) const entrypointSimulationHash = await testClient.sendTransaction({ to: DETERMINISTIC_DEPLOYER, @@ -455,8 +449,6 @@ const deployContracts = async (rpcPort: number): Promise => { testClient.waitForTransactionReceipt({ hash: entrypointSimulationHash }), testClient.waitForTransactionReceipt({ hash: entrypointHash }) ]) - - await byteCodeDeployer(testClient, TEST_CONTRACTS) } export const sleep = (ms: number) => @@ -467,7 +459,7 @@ export type DeployerParams = { chainId: number address: Address } -export const byteCodeDeployer = async ( +export const setByteCodeDynamic = async ( testClient: MasterClient, deployParams: Record ) => { @@ -480,7 +472,7 @@ export const byteCodeDeployer = async ( chain: fetchChain, transport: http() }) - return publicClient.getBytecode({ address }) + return publicClient.getCode({ address }) }) )) as Hex[] diff --git a/tests/vitest.config.ts b/src/test/vitest.config.ts similarity index 77% rename from tests/vitest.config.ts rename to src/test/vitest.config.ts index ef719a810..48ba25f39 100644 --- a/tests/vitest.config.ts +++ b/src/test/vitest.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ "**/*.test.ts", "**/test/**" ], - include: ["src/**/*.ts"], + include: ["./src/test/**/*.test.ts", "./src/sdk/**/*.test.ts"], thresholds: { lines: 80, functions: 50, @@ -25,8 +25,8 @@ export default defineConfig({ statements: 80 } }, - include: ["tests/**/*.test.ts"], - globalSetup: join(__dirname, "src/globalSetup.ts"), + include: ["./src/test/**/*.test.ts", "./src/sdk/**/*.test.ts"], + globalSetup: join(__dirname, "globalSetup.ts"), environment: "node", testTimeout: 60_000, hookTimeout: 60_000 diff --git a/tests/account.read.test.ts b/tests/account.read.test.ts deleted file mode 100644 index 604344354..000000000 --- a/tests/account.read.test.ts +++ /dev/null @@ -1,829 +0,0 @@ -import { JsonRpcProvider, ParamType, Wallet, ethers } from "ethers" -import { - http, - type AbiParameter, - type Account, - type Chain, - type Hex, - type PublicClient, - type WalletClient, - concat, - concatHex, - createPublicClient, - createWalletClient, - domainSeparator, - encodeAbiParameters, - encodeFunctionData, - encodePacked, - getContract, - hashMessage, - keccak256, - parseAbi, - parseAbiParameters, - parseEther, - toBytes, - toHex -} from "viem" -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" -import { baseSepolia } from "viem/chains" -import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { K1ValidatorFactoryAbi, NexusAbi } from "../src/__contracts/abi" -import addresses from "../src/__contracts/addresses" -import { - ERROR_MESSAGES, - NATIVE_TOKEN_ALIAS, - type NexusSmartAccount, - type SupportedSigner, - createSmartAccountClient, - eip1271MagicValue, - getChain, - makeInstallDataAndHash -} from "../src/account" -import { CounterAbi, TokenWithPermitAbi } from "./src/__contracts/abi" -import mockAddresses from "./src/__contracts/mockAddresses" -import { type TestFileNetworkType, toNetwork } from "./src/testSetup" -import { - checkBalance, - getAccountDomainStructFields, - getBundlerUrl, - getTestAccount, - killNetwork, - pKey, - toTestClient, - topUp -} from "./src/testUtils" -import type { MasterClient, NetworkConfig } from "./src/testUtils" - -const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" - -describe("account.read", () => { - let network: NetworkConfig - // Nexus Config - let chain: Chain - let bundlerUrl: string - let walletClient: WalletClient - let publicClient: PublicClient - - // Test utils - let testClient: MasterClient - let account: Account - let recipientAccount: Account - let smartAccount: NexusSmartAccount - let smartAccountAddress: Hex - - beforeAll(async () => { - network = (await toNetwork(NETWORK_TYPE)) as NetworkConfig - - chain = network.chain - bundlerUrl = network.bundlerUrl - - account = getTestAccount(0) - recipientAccount = getTestAccount(3) - - publicClient = createPublicClient({ - chain, - transport: http() - }) - - walletClient = createWalletClient({ - account, - chain, - transport: http() - }) - - testClient = toTestClient(chain, getTestAccount(0)) - - smartAccount = await createSmartAccountClient({ - signer: walletClient, - bundlerUrl, - chain - }) - - smartAccountAddress = await smartAccount.getAddress() - }) - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should deploy smart account if not deployed", async () => { - const isDeployed = await smartAccount.isAccountDeployed() - - if (!isDeployed) { - console.log("Smart account not deployed. Deploying...") - - // Fund the account first - await topUp(testClient, smartAccountAddress, parseEther("0.01")) - - // Create a dummy transaction to trigger deployment - const dummyTx = { - to: smartAccountAddress, - value: 0n, - data: "0x" - } - - const userOp = await smartAccount.sendTransaction([dummyTx]) - await userOp.wait() - - const isNowDeployed = await smartAccount.isAccountDeployed() - expect(isNowDeployed).toBe(true) - - console.log("Smart account deployed successfully") - } else { - console.log("Smart account already deployed") - } - - // Verify the account is now deployed - const finalDeploymentStatus = await smartAccount.isAccountDeployed() - expect(finalDeploymentStatus).toBe(true) - }) - - test("should fund the smart account", async () => { - await topUp(testClient, smartAccountAddress, parseEther("0.01")) - const [balance] = await smartAccount.getBalances() - expect(balance.amount > 0) - }) - - // @note @todo this test is only valid for anvil - test.skip("should have account addresses", async () => { - const addresses = await Promise.all([ - account.address, - smartAccount.getAddress() - ]) - expect(addresses.every(Boolean)).to.be.true - expect(addresses).toStrictEqual([ - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account - ]) - }) - - test("should estimate gas for minting an NFT", async () => { - const encodedCall = encodeFunctionData({ - abi: CounterAbi, - functionName: "incrementNumber" - }) - const transaction = { - to: mockAddresses.Counter, - data: encodedCall - } - const results = await Promise.all([ - smartAccount.getGasEstimate([transaction]), - smartAccount.getGasEstimate([transaction, transaction]) - ]) - - const increasingGasExpenditure = results.every( - (result, i) => result > (results[i - 1] ?? 0) - ) - - expect(increasingGasExpenditure).toBeTruthy() - }, 60000) - - test("should throw if PrivateKeyAccount is used as signer and rpcUrl is not provided", async () => { - const createSmartAccount = createSmartAccountClient({ - chain, - signer: account as SupportedSigner, - bundlerUrl - }) - - await expect(createSmartAccount).rejects.toThrow( - ERROR_MESSAGES.MISSING_RPC_URL - ) - }, 50000) - - test("should get all modules", async () => { - const modules = smartAccount.getInstalledModules() - if (await smartAccount.isAccountDeployed()) { - expect(modules).resolves - } else { - expect(modules).rejects.toThrow("Account is not deployed") - } - }, 30000) - - test("should check if module is enabled on the smart account", async () => { - const isEnabled = smartAccount.isModuleInstalled({ - type: "validator", - moduleAddress: addresses.K1Validator - }) - if (await smartAccount.isAccountDeployed()) { - expect(isEnabled).resolves.toBeTruthy() - } else { - expect(isEnabled).rejects.toThrow("Account is not deployed") - } - }, 30000) - - test("enable mode", async () => { - const result = makeInstallDataAndHash(account.address, [ - { - moduleType: "validator", - config: account.address - } - ]) - expect(result).toBeTruthy() - }, 30000) - - test("should create a smartAccountClient from an ethers signer", async () => { - const ethersProvider = new JsonRpcProvider(chain.rpcUrls.default.http[0]) - const ethersSigner = new Wallet(pKey, ethersProvider) - - const smartAccount = await createSmartAccountClient({ - chain, - signer: ethersSigner, - bundlerUrl, - rpcUrl: chain.rpcUrls.default.http[0] - }) - - expect(smartAccount).toBeTruthy() - }) - - test.skip("should pickup the rpcUrl from viem wallet and ethers", async () => { - const newRpcUrl = "http://localhost:8545" - const defaultRpcUrl = chain.rpcUrls.default.http[0] //http://127.0.0.1:8545" - - const ethersProvider = new JsonRpcProvider(newRpcUrl) - const ethersSignerWithNewRpcUrl = new Wallet(pKey, ethersProvider) - - const originalEthersProvider = new JsonRpcProvider( - chain.rpcUrls.default.http[0] - ) - const ethersSigner = new Wallet(pKey, originalEthersProvider) - - const walletClientWithNewRpcUrl = createWalletClient({ - account, - chain, - transport: http(newRpcUrl) - }) - const [ - smartAccountFromEthersWithNewRpc, - smartAccountFromViemWithNewRpc, - smartAccountFromEthersWithOldRpc, - smartAccountFromViemWithOldRpc - ] = await Promise.all([ - createSmartAccountClient({ - chain, - signer: ethersSignerWithNewRpcUrl, - bundlerUrl: getBundlerUrl(1337), - rpcUrl: newRpcUrl - }), - createSmartAccountClient({ - chain, - signer: walletClientWithNewRpcUrl, - bundlerUrl: getBundlerUrl(1337), - rpcUrl: newRpcUrl - }), - createSmartAccountClient({ - chain, - signer: ethersSigner, - bundlerUrl: getBundlerUrl(1337), - rpcUrl: chain.rpcUrls.default.http[0] - }), - createSmartAccountClient({ - chain, - signer: walletClient, - bundlerUrl: getBundlerUrl(1337), - rpcUrl: chain.rpcUrls.default.http[0] - }) - ]) - - const [ - smartAccountFromEthersWithNewRpcAddress, - smartAccountFromViemWithNewRpcAddress, - smartAccountFromEthersWithOldRpcAddress, - smartAccountFromViemWithOldRpcAddress - ] = await Promise.all([ - smartAccountFromEthersWithNewRpc.getAccountAddress(), - smartAccountFromViemWithNewRpc.getAccountAddress(), - smartAccountFromEthersWithOldRpc.getAccountAddress(), - smartAccountFromViemWithOldRpc.getAccountAddress() - ]) - - expect( - [ - smartAccountFromEthersWithNewRpcAddress, - smartAccountFromViemWithNewRpcAddress, - smartAccountFromEthersWithOldRpcAddress, - smartAccountFromViemWithOldRpcAddress - ].every(Boolean) - ).toBeTruthy() - - expect(smartAccountFromEthersWithNewRpc.publicClient.transport.url).toBe( - newRpcUrl - ) - expect(smartAccountFromViemWithNewRpc.publicClient.transport.url).toBe( - newRpcUrl - ) - expect(smartAccountFromEthersWithOldRpc.publicClient.transport.url).toBe( - defaultRpcUrl - ) - expect(smartAccountFromViemWithOldRpc.publicClient.transport.url).toBe( - defaultRpcUrl - ) - }) - - test("should read estimated user op gas values", async () => { - const tx = { - to: recipientAccount.address, - data: "0x" - } - - const userOp = await smartAccount.buildUserOp([tx]) - - const estimatedGas = await smartAccount.estimateUserOpGas(userOp) - expect(estimatedGas.maxFeePerGas).toBeTruthy() - expect(estimatedGas.maxPriorityFeePerGas).toBeTruthy() - expect(estimatedGas.verificationGasLimit).toBeTruthy() - expect(estimatedGas.callGasLimit).toBeTruthy() - expect(estimatedGas.preVerificationGas).toBeTruthy() - }, 30000) - - test("should have an active validation module", async () => { - const module = smartAccount.activeValidationModule - expect(module).toBeTruthy() - }) - - // test.skip( - // "should create a smart account with paymaster by creating instance", - // async () => { - // const paymaster = new Paymaster({ paymasterUrl }) - - // const smartAccount = await createSmartAccountClient({ - // signer: walletClient, - // bundlerUrl, - // paymaster - // }) - // expect(smartAccount.paymaster).not.toBeNull() - // expect(smartAccount.paymaster).not.toBeUndefined() - // } - // ) - - test("should fail to create a smartAccountClient from a walletClient without an account", async () => { - const viemWalletNoAccount = createWalletClient({ - transport: http(chain.rpcUrls.default.http[0]) - }) - - expect(async () => - createSmartAccountClient({ - chain, - signer: viemWalletNoAccount, - bundlerUrl, - rpcUrl: chain.rpcUrls.default.http[0] - }) - ).rejects.toThrow("Cannot consume a viem wallet without an account") - }) - - test.skip("should create a smart account with paymaster with an api key", async () => { - const paymaster = smartAccount.paymaster - expect(paymaster).not.toBeNull() - expect(paymaster).not.toBeUndefined() - }) - - test("should return chain object for chain id 1", async () => { - const chainId = 1 - const chain = getChain(chainId) - expect(chain.id).toBe(chainId) - }) - - test("should have correct fields", async () => { - const chainId = 1 - const chain = getChain(chainId) - ;[ - "blockExplorers", - "contracts", - "fees", - "formatters", - "id", - "name", - "nativeCurrency", - "rpcUrls", - "serializers" - ].every((field) => { - expect(chain).toHaveProperty(field) - }) - }) - - test("should throw an error, chain id not found", async () => { - const chainId = 0 - expect(() => getChain(chainId)).toThrow(ERROR_MESSAGES.CHAIN_NOT_FOUND) - }) - - test("should have matching counterFactual address from the contracts with smartAccount.getAddress()", async () => { - const client = createWalletClient({ - account, - chain, - transport: http() - }) - - const smartAccount = await createSmartAccountClient({ - chain, - signer: client, - bundlerUrl - }) - - const smartAccountAddressFromSDK = await smartAccount.getAccountAddress() - - const factoryContract = getContract({ - address: addresses.K1ValidatorFactory, - abi: K1ValidatorFactoryAbi, - client: { public: publicClient, wallet: client } - }) - - const smartAccountAddressFromContracts = - await factoryContract.read.computeAccountAddress([ - await smartAccount.getSmartAccountOwner().getAddress(), - BigInt(0), - [], - 0 - ]) - - expect(smartAccountAddressFromSDK).toBe(smartAccountAddressFromContracts) - }) - - test("should be deployed to counterfactual address", async () => { - const accountAddress = await smartAccount.getAccountAddress() - const byteCode = await testClient.getBytecode({ - address: accountAddress as Hex - }) - if (await smartAccount.isAccountDeployed()) { - expect(byteCode?.length).toBeGreaterThan(2) - } else { - expect(byteCode?.length).toBe(undefined) - } - }, 10000) - - test("should check if K1Validator is enabled", async () => { - const ecdsaOwnershipModule = addresses.K1Validator - - expect(ecdsaOwnershipModule).toBe( - smartAccount.activeValidationModule.getAddress() - ) - }) - - test("should fail to deploy a smart account if no native token balance or paymaster", async () => { - const newPrivateKey = generatePrivateKey() - const newAccount = privateKeyToAccount(newPrivateKey) - - const newViemWallet = createWalletClient({ - account: newAccount, - chain, - transport: http() - }) - - const smartAccount = await createSmartAccountClient({ - chain, - signer: newViemWallet, - bundlerUrl - }) - - expect(async () => smartAccount.deploy()).rejects.toThrow( - ERROR_MESSAGES.NO_NATIVE_TOKEN_BALANCE_DURING_DEPLOY - ) - }) - - test("should fail to deploy a smart account if already deployed", async () => { - if (await smartAccount.isAccountDeployed()) { - expect(async () => smartAccount.deploy()).rejects.toThrow( - ERROR_MESSAGES.ACCOUNT_ALREADY_DEPLOYED - ) - } else { - expect(smartAccount.deploy()).resolves - } - }, 60000) - - test.skip("should fetch balances for smartAccount", async () => { - const token = "0x69835C1f31ed0721A05d5711C1d669C10802a3E1" - const tokenBalanceBefore = await checkBalance( - testClient, - smartAccountAddress, - token - ) - const [tokenBalanceFromSmartAccount] = await smartAccount.getBalances([ - token - ]) - - expect(tokenBalanceBefore).toBe(tokenBalanceFromSmartAccount.amount) - }) - - test("should error if no recipient exists", async () => { - const token: Hex = "0x69835C1f31ed0721A05d5711C1d669C10802a3E1" - - const txs = [ - { address: token, amount: BigInt(1), recipient: account.address }, - { address: NATIVE_TOKEN_ALIAS, amount: BigInt(1) } - ] - - expect(async () => smartAccount.withdraw(txs)).rejects.toThrow( - ERROR_MESSAGES.NO_RECIPIENT - ) - }) - - test("should error when withdraw all of native token is attempted without an amount explicitly set", async () => { - expect(async () => - smartAccount.withdraw(null, account.address) - ).rejects.toThrow(ERROR_MESSAGES.NATIVE_TOKEN_WITHDRAWAL_WITHOUT_AMOUNT) - }, 6000) - - test("should check native token balance and more token info for smartAccount", async () => { - const [ethBalanceFromSmartAccount] = await smartAccount.getBalances() - - expect(ethBalanceFromSmartAccount.amount).toBeGreaterThan(0n) - expect(ethBalanceFromSmartAccount.address).toBe(NATIVE_TOKEN_ALIAS) - expect(ethBalanceFromSmartAccount.chainId).toBe(chain.id) - expect(ethBalanceFromSmartAccount.decimals).toBe(18) - }, 60000) - - test.skip("should check balance of supported token", async () => { - const tokens = await smartAccount.getSupportedTokens() - const [firstToken] = tokens - - expect(tokens.length).toBeGreaterThan(0) - expect(tokens[0]).toHaveProperty("balance") - expect(firstToken.balance.amount).toBeGreaterThanOrEqual(0n) - }, 60000) - - // @note Nexus SA signature needs to contain the validator module address in the first 20 bytes - test("should test isValidSignature PersonalSign to be valid", async () => { - if (await smartAccount.isAccountDeployed()) { - const data = hashMessage("0x1234") - - // Define constants as per the original Solidity function - const DOMAIN_NAME = "Nexus" - const DOMAIN_VERSION = "1.0.0-beta" - const DOMAIN_TYPEHASH = - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - const PARENT_TYPEHASH = "PersonalSign(bytes prefixed)" - - // Calculate the domain separator - const domainSeparator = keccak256( - encodeAbiParameters( - parseAbiParameters("bytes32, bytes32, bytes32, uint256, address"), - [ - keccak256(toBytes(DOMAIN_TYPEHASH)), - keccak256(toBytes(DOMAIN_NAME)), - keccak256(toBytes(DOMAIN_VERSION)), - BigInt(chain.id), - smartAccountAddress - ] - ) - ) - - // Calculate the parent struct hash - const parentStructHash = keccak256( - encodeAbiParameters(parseAbiParameters("bytes32, bytes32"), [ - keccak256(toBytes(PARENT_TYPEHASH)), - hashMessage(data) - ]) - ) - - // Calculate the final hash - const resultHash: Hex = keccak256( - concat(["0x1901", domainSeparator, parentStructHash]) - ) - - const signature = await smartAccount.signMessage(resultHash) - - const contractResponse = await testClient.readContract({ - address: await smartAccount.getAddress(), - abi: NexusAbi, - functionName: "isValidSignature", - args: [hashMessage(data), signature] - }) - - const viemResponse = await testClient.verifyMessage({ - address: smartAccountAddress, - message: data, - signature - }) - - expect(contractResponse).toBe(eip1271MagicValue) - expect(viemResponse).toBe(true) - } - }) - - test("should have consistent behaviour between ethers.AbiCoder.defaultAbiCoder() and viem.encodeAbiParameters()", async () => { - const expectedResult = - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b90600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b906000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" - - const Executions = ParamType.from({ - type: "tuple(address,uint256,bytes)[]", - baseType: "tuple", - name: "executions", - arrayLength: null, - components: [ - { name: "target", type: "address" }, - { name: "value", type: "uint256" }, - { name: "callData", type: "bytes" } - ] - }) - - const viemExecutions: AbiParameter = { - type: "tuple[]", - components: [ - { name: "target", type: "address" }, - { name: "value", type: "uint256" }, - { name: "callData", type: "bytes" } - ] - } - - const txs = [ - { - target: "0x90F79bf6EB2c4f870365E785982E1f101E93b906", - callData: "0x", - value: 1n - }, - { - target: "0x90F79bf6EB2c4f870365E785982E1f101E93b906", - callData: "0x", - value: 1n - } - ] - - const executionCalldataPrepWithEthers = - ethers.AbiCoder.defaultAbiCoder().encode([Executions], [txs]) - - const executionCalldataPrepWithViem = encodeAbiParameters( - [viemExecutions], - [txs] - ) - - expect(executionCalldataPrepWithEthers).toBe(expectedResult) - expect(executionCalldataPrepWithViem).toBe(expectedResult) - }) - - test.concurrent( - "should test isValidSignature EIP712Sign to be valid with viem", - async () => { - if (await smartAccount.isAccountDeployed()) { - const PARENT_TYPEHASH = - "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)" - - const message = { - contents: keccak256(toBytes("test", { size: 32 })) - } - - const domainSeparator = await publicClient.readContract({ - address: await smartAccount.getAddress(), - abi: parseAbi([ - "function DOMAIN_SEPARATOR() external view returns (bytes32)" - ]), - functionName: "DOMAIN_SEPARATOR" - }) - - const typedHashHashed = keccak256( - concat(["0x1901", domainSeparator, message.contents]) - ) - - const accountDomainStructFields = await getAccountDomainStructFields( - testClient, - await smartAccount.getAddress() - ) - - const parentStructHash = keccak256( - encodePacked( - ["bytes", "bytes"], - [ - encodeAbiParameters(parseAbiParameters(["bytes32, bytes32"]), [ - keccak256(toBytes(PARENT_TYPEHASH)), - message.contents - ]), - accountDomainStructFields - ] - ) - ) - - const dataToSign = keccak256( - concat(["0x1901", domainSeparator, parentStructHash]) - ) - - const signature = await walletClient.signMessage({ - message: { raw: toBytes(dataToSign) }, - account - }) - - const contentsType = toBytes("Contents(bytes32 stuff)") - - const signatureData = concatHex([ - signature, - domainSeparator, - message.contents, - toHex(contentsType), - toHex(contentsType.length, { size: 2 }) - ]) - - const finalSignature = encodePacked( - ["address", "bytes"], - [addresses.K1Validator, signatureData] - ) - - const contractResponse = await publicClient.readContract({ - address: await smartAccount.getAddress(), - abi: NexusAbi, - functionName: "isValidSignature", - args: [typedHashHashed, finalSignature] - }) - - expect(contractResponse).toBe(eip1271MagicValue) - } else { - throw new Error("Smart account is not deployed") - } - } - ) - - test("should sign using signTypedData SDK method", async () => { - const permitTestTokenAddress = mockAddresses.TokenWithPermit; - const appDomain = { - chainId: network.chain.id, - name: "TokenWithPermit", - verifyingContract: permitTestTokenAddress, - version: "1" - } - - const primaryType = "Contents" - const types = { - Contents: [ - { - name: "stuff", - type: "bytes32" - } - ] - } - - const permitTypehash = keccak256( - toBytes( - "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" - ) - ) - const nonce = (await publicClient.readContract({ - address: permitTestTokenAddress, - abi: TokenWithPermitAbi, - functionName: "nonces", - args: [smartAccountAddress] - })) as bigint - - const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600) // 1 hour from now - - const message = { - stuff: keccak256( - encodeAbiParameters( - parseAbiParameters( - "bytes32, address, address, uint256, uint256, uint256" - ), - [ - permitTypehash, - smartAccountAddress, - smartAccountAddress, - parseEther("2"), - nonce, - deadline - ] - ) - ) - } - - const appDomainSeparator = domainSeparator({ - domain: appDomain - }) - - const contentsHash = keccak256( - concat(["0x1901", appDomainSeparator, message.stuff]) - ) - - const finalSignature = await smartAccount.signTypedData({ - domain: appDomain, - primaryType, - types, - message - }) - - const nexusResponse = await publicClient.readContract({ - address: await smartAccount.getAddress(), - abi: NexusAbi, - functionName: "isValidSignature", - args: [contentsHash, finalSignature] - }) - - const permitTokenResponse = await walletClient.writeContract({ - account: account, - address: permitTestTokenAddress, - abi: TokenWithPermitAbi, - functionName: "permitWith1271", - chain: network.chain, - args: [ - await smartAccount.getAddress(), - await smartAccount.getAddress(), - parseEther("2"), - deadline, - finalSignature - ] - }) - - await publicClient.waitForTransactionReceipt({ hash: permitTokenResponse }) - - const allowance = await publicClient.readContract({ - address: permitTestTokenAddress, - abi: TokenWithPermitAbi, - functionName: "allowance", - args: [await smartAccount.getAddress(), await smartAccount.getAddress()] - }) - - expect(allowance).toEqual(parseEther("2")) - expect(nexusResponse).toEqual("0x1626ba7e") - }) -}) diff --git a/tests/account.write.test.ts b/tests/account.write.test.ts deleted file mode 100644 index 8af5a0fb8..000000000 --- a/tests/account.write.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { - http, - type Account, - type Chain, - type Hex, - type WalletClient, - createWalletClient -} from "viem" -import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { - type NexusSmartAccount, - type Transaction, - createSmartAccountClient -} from "../src/account" -import { type TestFileNetworkType, toNetwork } from "./src/testSetup" -import { - getTestAccount, - killNetwork, - toTestClient, - topUp -} from "./src/testUtils" -import type { MasterClient, NetworkConfig } from "./src/testUtils" - -const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" - -describe("account.write", () => { - let network: NetworkConfig - // Nexus Config - let chain: Chain - let bundlerUrl: string - let walletClient: WalletClient - - // Test utils - let testClient: MasterClient - let account: Account - let recipientAccount: Account - let smartAccount: NexusSmartAccount - let smartAccountAddress: Hex - - beforeAll(async () => { - network = await toNetwork(NETWORK_TYPE) - - chain = network.chain - bundlerUrl = network.bundlerUrl - - account = getTestAccount(0) - recipientAccount = getTestAccount(3) - - walletClient = createWalletClient({ - account, - chain, - transport: http() - }) - - testClient = toTestClient(chain, getTestAccount(0)) - - smartAccount = await createSmartAccountClient({ - signer: walletClient, - bundlerUrl, - chain - }) - - smartAccountAddress = await smartAccount.getAddress() - }) - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should fund the smart account", async () => { - await topUp(testClient, smartAccountAddress) - const [balance] = await smartAccount.getBalances() - expect(balance.amount > 0) - }) - - test("should have account addresses", async () => { - const addresses = await Promise.all([ - account.address, - smartAccount.getAddress() - ]) - expect(addresses.every(Boolean)).to.be.true - expect(addresses).toStrictEqual([ - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account - ]) - }) - - test("should send eth", async () => { - const balanceBefore = await testClient.getBalance({ - address: recipientAccount.address - }) - const tx: Transaction = { - to: recipientAccount.address, - value: 1n - } - const { wait } = await smartAccount.sendTransaction(tx) - const { success } = await wait() - const balanceAfter = await testClient.getBalance({ - address: recipientAccount.address - }) - expect(success).toBe(true) - expect(balanceAfter - balanceBefore).toBe(1n) - }) - - test("should send eth twice", async () => { - const balanceBefore = await testClient.getBalance({ - address: recipientAccount.address - }) - const tx: Transaction = { - to: recipientAccount.address, - value: 1n - } - const { wait } = await smartAccount.sendTransaction([tx, tx]) - const { success } = await wait() - const balanceAfter = await testClient.getBalance({ - address: recipientAccount.address - }) - expect(success).toBe(true) - expect(balanceAfter - balanceBefore).toBe(2n) - }) - - // test("install a mock Hook module", async () => { - // const isSupported = await smartAccount.supportsModule(ModuleType.Hook) - // console.log(isSupported, "is supported") - - // const isInstalledBefore = await smartAccount.isModuleInstalled( - // ModuleType.Hook, - // MOCK_HOOK - // ) - // console.log(isInstalledBefore, "is installed before") - - // const userOpReceipt = await smartAccount.installModule(MOCK_HOOK, ModuleType.Hook) - // console.log(userOpReceipt, "user op receipt") - - // const isInstalled = await smartAccount.isModuleInstalled( - // ModuleType.Hook, - // MOCK_HOOK - // ) - - // expect(userOpReceipt.success).toBe(true) - // expect(isInstalled).toBeTruthy() - // }, 60000) - - // test("get active hook", async () => { - // const activeHook: Address = await smartAccount.getActiveHook() - // console.log(activeHook, "active hook") - // expect(activeHook).toBe(MOCK_HOOK) - // }, 60000) - - // test("uninstall hook module", async () => { - // const prevAddress: Hex = "0x0000000000000000000000000000000000000001" - // const deInitData = encodeAbiParameters( - // [ - // { name: "prev", type: "address" }, - // { name: "disableModuleData", type: "bytes" } - // ], - // [prevAddress, toHex(stringToBytes(""))] - // ) - // const userOpReceipt = await smartAccount.uninstallModule(MOCK_HOOK, ModuleType.Hook, deInitData) - - // const isInstalled = await smartAccount.isModuleInstalled( - // ModuleType.Hook, - // MOCK_HOOK - // ) - - // expect(userOpReceipt.success).toBe(true) - // expect(isInstalled).toBeFalsy() - // expect(userOpReceipt).toBeTruthy() - // }, 60000) - - // test("install a fallback handler Hook module", async () => { - // const isSupported = await smartAccount.supportsModule(ModuleType.Fallback) - // console.log(isSupported, "is supported") - - // const isInstalledBefore = await smartAccount.isModuleInstalled( - // ModuleType.Fallback, - // MOCK_FALLBACK_HANDLER, - // ethers.AbiCoder.defaultAbiCoder().encode( - // ["bytes4"], - // [GENERIC_FALLBACK_SELECTOR as Hex] - // ) as Hex - // ) - // console.log(isInstalledBefore, "is installed before") - - // const userOpReceipt = await smartAccount.installModule(MOCK_FALLBACK_HANDLER, ModuleType.Fallback, ethers.AbiCoder.defaultAbiCoder().encode( - // ["bytes4"], - // [GENERIC_FALLBACK_SELECTOR as Hex] - // ) as Hex) - - // const isInstalled = await smartAccount.isModuleInstalled( - // ModuleType.Fallback, - // MOCK_FALLBACK_HANDLER, - // ethers.AbiCoder.defaultAbiCoder().encode( - // ["bytes4"], - // [GENERIC_FALLBACK_SELECTOR as Hex] - // ) as Hex - // ) - - // expect(userOpReceipt.success).toBe(true) - // expect(isInstalled).toBeTruthy() - // }, 60000) - - // test("uninstall handler module", async () => { - // const prevAddress: Hex = "0x0000000000000000000000000000000000000001" - // const deInitData = ethers.AbiCoder.defaultAbiCoder().encode( - // ["bytes4"], - // [GENERIC_FALLBACK_SELECTOR as Hex] - // ) as Hex - // const userOpReceipt = await smartAccount.uninstallModule( - // MOCK_FALLBACK_HANDLER, - // ModuleType.Fallback, - // deInitData - // ) - - // const isInstalled = await smartAccount.isModuleInstalled( - // ModuleType.Fallback, - // MOCK_FALLBACK_HANDLER, - // ethers.AbiCoder.defaultAbiCoder().encode( - // ["bytes4"], - // [GENERIC_FALLBACK_SELECTOR as Hex] - // ) as Hex - // ) - - // expect(userOpReceipt.success).toBe(true) - // expect(isInstalled).toBeFalsy() - // expect(userOpReceipt).toBeTruthy() - // }, 60000) -}) diff --git a/tests/modules.k1Validator.write.test.ts b/tests/modules.k1Validator.write.test.ts deleted file mode 100644 index d267c342e..000000000 --- a/tests/modules.k1Validator.write.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { - http, - type Account, - type Chain, - type Hex, - type WalletClient, - createWalletClient, - encodeFunctionData, - encodePacked -} from "viem" -import { afterAll, beforeAll, describe, expect, test } from "vitest" -import addresses from "../src/__contracts/addresses" -import { - type NexusSmartAccount, - type Transaction, - createSmartAccountClient -} from "../src/account" -import { CounterAbi } from "./src/__contracts/abi" -import { mockAddresses } from "./src/__contracts/mockAddresses" -import { OWNABLE_VALIDATOR } from "./src/callDatas" -import { type TestFileNetworkType, toNetwork } from "./src/testSetup" -import { - getTestAccount, - killNetwork, - toTestClient, - topUp -} from "./src/testUtils" -import type { MasterClient, NetworkConfig } from "./src/testUtils" - -const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" - -describe("modules.k1Validator.write", () => { - let network: NetworkConfig - // Nexus Config - let chain: Chain - let bundlerUrl: string - let walletClient: WalletClient - - // Test utils - let testClient: MasterClient - let account: Account - let recipientAccount: Account - let smartAccount: NexusSmartAccount - let smartAccountAddress: Hex - - beforeAll(async () => { - network = await toNetwork(NETWORK_TYPE) - - chain = network.chain - bundlerUrl = network.bundlerUrl - - account = getTestAccount(0) - recipientAccount = getTestAccount(3) - - walletClient = createWalletClient({ - account, - chain, - transport: http() - }) - - testClient = toTestClient(chain, getTestAccount(0)) - - smartAccount = await createSmartAccountClient({ - signer: walletClient, - bundlerUrl, - chain - }) - - smartAccountAddress = await smartAccount.getAddress() - }) - - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should fund the smart account", async () => { - await topUp(testClient, smartAccountAddress) - const [balance] = await smartAccount.getBalances() - expect(balance.amount > 0) - }) - - test("should have account addresses", async () => { - const addresses = await Promise.all([ - account.address, - smartAccount.getAddress() - ]) - expect(addresses.every(Boolean)).to.be.true - expect(addresses).toStrictEqual([ - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account - ]) - }) - - test("should send eth", async () => { - const balanceBefore = await testClient.getBalance({ - address: recipientAccount.address - }) - const tx: Transaction = { - to: recipientAccount.address, - value: 1n - } - const { wait } = await smartAccount.sendTransaction(tx) - const { success } = await wait() - const balanceAfter = await testClient.getBalance({ - address: recipientAccount.address - }) - expect(success).toBe(true) - expect(balanceAfter - balanceBefore).toBe(1n) - }) - - test("should install k1 Validator with 1 owner", async () => { - const isInstalledBefore = await smartAccount.isModuleInstalled({ - type: "validator", - moduleAddress: addresses.K1Validator - }) - - if (!isInstalledBefore) { - const { wait } = await smartAccount.installModule({ - moduleAddress: addresses.K1Validator, - type: "validator", - data: encodePacked(["address"], [await smartAccount.getAddress()]) - }) - - const { success: installSuccess } = await wait() - expect(installSuccess).toBe(true) - - const { wait: uninstallWait } = await smartAccount.uninstallModule({ - moduleAddress: addresses.K1Validator, - type: "validator", - data: encodePacked(["address"], [await smartAccount.getAddress()]) - }) - const { success: uninstallSuccess } = await uninstallWait() - expect(uninstallSuccess).toBe(true) - } - }, 60000) - - test.skip("should have the Ownable Validator Module installed", async () => { - const isInstalled = await smartAccount.isModuleInstalled({ - type: "validator", - moduleAddress: OWNABLE_VALIDATOR - }) - expect(isInstalled).toBeTruthy() - }, 60000) - - test("should perform a contract interaction", async () => { - const encodedCall = encodeFunctionData({ - abi: CounterAbi, - functionName: "incrementNumber" - }) - - const transaction = { - to: mockAddresses.Counter, - data: encodedCall - } - - const response = await smartAccount.sendTransaction([transaction]) - const receipt = await response.wait() - - expect(receipt.success).toBe(true) - }, 60000) -}) diff --git a/tests/modules.ownableExecutor.read.test.ts b/tests/modules.ownableExecutor.read.test.ts deleted file mode 100644 index 9e9a13624..000000000 --- a/tests/modules.ownableExecutor.read.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { - http, - type Account, - type Chain, - type Hex, - type WalletClient, - createWalletClient -} from "viem" -import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { - type NexusSmartAccount, - type Transaction, - createSmartAccountClient -} from "../src/account" -import { createOwnableExecutorModule } from "../src/modules" -import { OWNABLE_EXECUTOR } from "./src/callDatas" -import { type TestFileNetworkType, toNetwork } from "./src/testSetup" -import { - getTestAccount, - killNetwork, - toTestClient, - topUp -} from "./src/testUtils" -import type { MasterClient, NetworkConfig } from "./src/testUtils" - -const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" - -describe("modules.ownable.executor.read", () => { - let network: NetworkConfig - // Nexus Config - let chain: Chain - let bundlerUrl: string - let walletClient: WalletClient - - // Test utils - let testClient: MasterClient - let account: Account - let recipientAccount: Account - let smartAccount: NexusSmartAccount - let smartAccountAddress: Hex - - beforeAll(async () => { - network = await toNetwork(NETWORK_TYPE) - - chain = network.chain - bundlerUrl = network.bundlerUrl - - account = getTestAccount(0) - recipientAccount = getTestAccount(3) - - walletClient = createWalletClient({ - account, - chain, - transport: http() - }) - - testClient = toTestClient(chain, getTestAccount(0)) - - smartAccount = await createSmartAccountClient({ - signer: walletClient, - bundlerUrl, - chain - }) - - smartAccountAddress = await smartAccount.getAddress() - }) - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should fund the smart account", async () => { - await topUp(testClient, smartAccountAddress) - const [balance] = await smartAccount.getBalances() - expect(balance.amount > 0) - }) - - test("should have account addresses", async () => { - const addresses = await Promise.all([ - account.address, - smartAccount.getAddress() - ]) - expect(addresses.every(Boolean)).to.be.true - expect(addresses).toStrictEqual([ - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account - ]) - }) - - test("should send eth", async () => { - const balanceBefore = await testClient.getBalance({ - address: recipientAccount.address - }) - const tx: Transaction = { - to: recipientAccount.address, - value: 1n - } - const { wait } = await smartAccount.sendTransaction(tx) - const { success } = await wait() - const balanceAfter = await testClient.getBalance({ - address: recipientAccount.address - }) - expect(success).toBe(true) - expect(balanceAfter - balanceBefore).toBe(1n) - }) - - // test.skip("should initialize Ownable Executor Module with correct owners", async () => { - // const ownableExecutorModule = await createOwnableExecutorModule( - // smartAccount, - // OWNABLE_EXECUTOR - // ) - // const owners = await ownableExecutorModule.getOwners(OWNABLE_EXECUTOR) - // expect(owners).toStrictEqual(ownableExecutorModule.owners) - // }) -}) diff --git a/tests/modules.ownableExecutor.write.test.ts b/tests/modules.ownableExecutor.write.test.ts deleted file mode 100644 index 0094e7087..000000000 --- a/tests/modules.ownableExecutor.write.test.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { - http, - type Account, - type Chain, - type Hex, - type WalletClient, - createWalletClient -} from "viem" -import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { - type NexusSmartAccount, - type Transaction, - createSmartAccountClient -} from "../src/account" -import { type TestFileNetworkType, toNetwork } from "./src/testSetup" -import { - getTestAccount, - killNetwork, - toTestClient, - topUp -} from "./src/testUtils" -import type { MasterClient, NetworkConfig } from "./src/testUtils" - -const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" - -describe("modules.ownable.executor.write", () => { - let network: NetworkConfig - // Nexus Config - let chain: Chain - let bundlerUrl: string - let walletClient: WalletClient - - // Test utils - let testClient: MasterClient - let account: Account - let recipientAccount: Account - let smartAccount: NexusSmartAccount - let smartAccountAddress: Hex - - beforeAll(async () => { - network = await toNetwork(NETWORK_TYPE) - - chain = network.chain - bundlerUrl = network.bundlerUrl - - account = getTestAccount(0) - recipientAccount = getTestAccount(3) - - walletClient = createWalletClient({ - account, - chain, - transport: http() - }) - - testClient = toTestClient(chain, getTestAccount(0)) - - smartAccount = await createSmartAccountClient({ - signer: walletClient, - bundlerUrl, - chain - }) - - smartAccountAddress = await smartAccount.getAddress() - }) - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should fund the smart account", async () => { - await topUp(testClient, smartAccountAddress) - const [balance] = await smartAccount.getBalances() - expect(balance.amount > 0) - }) - - test("should have account addresses", async () => { - const addresses = await Promise.all([ - account.address, - smartAccount.getAddress() - ]) - expect(addresses.every(Boolean)).to.be.true - expect(addresses).toStrictEqual([ - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account - ]) - }) - - test("should send eth", async () => { - const balanceBefore = await testClient.getBalance({ - address: recipientAccount.address - }) - const tx: Transaction = { - to: recipientAccount.address, - value: 1n - } - const { wait } = await smartAccount.sendTransaction(tx) - const { success } = await wait() - const balanceAfter = await testClient.getBalance({ - address: recipientAccount.address - }) - expect(success).toBe(true) - expect(balanceAfter - balanceBefore).toBe(1n) - }) - - // test.skip("install Ownable Executor", async () => { - // let isInstalled = await smartAccount.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - - // if (!isInstalled) { - // const receipt = await smartAccount.installModule({ - // moduleAddress: ownableExecutorModule.moduleAddress, - // type: ownableExecutorModule.type, - // data: ownableExecutorModule.data - // }) - - // smartAccount.setActiveExecutionModule(ownableExecutorModule) - - // expect(receipt.success).toBe(true) - // } - // }, 60000) - - // test.skip("uninstall Ownable Executor", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const ownableExecutorModule2 = await createOwnableExecutorModule(smartAccount2, OWNABLE_EXECUTOR) - - // let isInstalled = await smartAccount2.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - - // if (isInstalled) { - // await smartAccount2.uninstallModule({ - // moduleAddress: ownableExecutorModule2.moduleAddress, - // type: ownableExecutorModule2.type, - // data: ownableExecutorModule2.data - // }) - // } - - // }, 60000) - - // test.skip("Ownable Executor Module should be installed", async () => { - // const isInstalled = await smartAccount.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - // console.log(isInstalled, "isInstalled") - // expect(isInstalled).toBeTruthy() - // }, 60000) - - // test.skip("should add an owner to the module", async () => { - // const ownersBefore = await ownableExecutorModule.getOwners() - // const isOwnerBefore = ownersBefore.includes(accountTwo.address) - - // if (isOwnerBefore) { - // console.log("Owner already exists in list, skipping test case ...") - // return - // } - - // const userOpReceipt = await ownableExecutorModule.addOwner( - // accountTwo.address - // ) - - // const owners = await ownableExecutorModule.getOwners() - // const isOwner = owners.includes(accountTwo.address) - - // expect(isOwner).toBeTruthy() - // expect(userOpReceipt.success).toBeTruthy() - // }, 60000) - - // test.skip("EOA 2 can execute actions on behalf of SA 1", async () => { - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const owners = await ownableExecutorModule.getOwners() - // const isOwner = owners.includes(accountTwo.address) - // expect(isOwner).toBeTruthy() - - // const balanceBefore = await smartAccount.getBalances([token]) - // console.log("balanceBefore", balanceBefore) - - // const calldata = encodeFunctionData({ - // abi: parseAbi([ - // "function executeOnOwnedAccount(address ownedAccount, bytes callData)" - // ]), - // functionName: "executeOnOwnedAccount", - // args: [ - // await smartAccount.getAddress(), - // encodePacked( - // ["address", "uint256", "bytes"], - // [token, BigInt(Number(0)), transferEncodedCall] - // ) - // ] - // }) - - // // EOA 2 (walletClientTwo) executes an action on behalf of SA 1 which is owned by EOA 1 (walletClientOne) - // const txHash = await walletClientTwo.sendTransaction({ - // account: accountTwo, // Called by delegated EOA owner - // to: ownableExecutorModule.moduleAddress, - // data: calldata, - // value: 0n - // }) - - // const balanceAfter = await smartAccount.getBalances([token]) - // console.log("balanceAfter", balanceAfter) - - // expect(txHash).toBeTruthy() - // }, 60000) - - // test("SA 2 can execute actions on behalf of SA 1", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const transferTransaction = { - // to: token, - // data: transferEncodedCall, - // value: 0n - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule) - // const receipt = await smartAccount2.sendTransactionWithExecutor([transferTransaction], await smartAccount.getAddress()); - // console.log(receipt, "receipt"); - - // expect(receipt.userOpHash).toBeTruthy() - // expect(receipt.success).toBe(true) - // }, 60000) - - // test.skip("SA 2 can execute actions on behalf of SA 1 using module instance instead of smart account instance", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const initData = encodePacked( - // ["address"], - // [await smartAccount2.getAddress()] - // ) - // const ownableExecutorModule2 = await createOwnableExecutorModule(smartAccount2, OWNABLE_EXECUTOR, initData) - - // // First, we need to install the OwnableExecutor module on SA 2 - // let isInstalled = await smartAccount2.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - - // if (!isInstalled) { - // await smartAccount2.installModule({ - // moduleAddress: ownableExecutorModule2.moduleAddress, - // type: ownableExecutorModule2.type, - // data: ownableExecutorModule2.data - // }) - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule) - - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const owners = await ownableExecutorModule2.getOwners() - - // // check if SA 2 is as an owner of SA 1 - // const isOwner = owners.includes(await smartAccount2.getAddress()) - // if(!isOwner) { - // const userOpReceipt = await ownableExecutorModule2.addOwner( - // await smartAccount2.getAddress() - // ) - // expect(userOpReceipt.success).toBeTruthy() - // } - - // const transferTransaction = { - // target: token as `0x${string}`, - // callData: transferEncodedCall, - // value: 0n - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule2) - // // SA 2 will execute the transferTransaction on behalf of SA 1 (smartAccount) - // const receipt = await ownableExecutorModule2.execute(transferTransaction, await smartAccount.getAddress()); - // console.log(receipt, "receipt"); - - // expect(receipt.userOpHash).toBeTruthy() - // expect(receipt.success).toBe(true) - // }, 60000) - - // test.skip("should remove an owner from the module", async () => { - // const userOpReceipt = await ownableExecutorModule.removeOwner( - // accountTwo.address - // ) - // const owners = await ownableExecutorModule.getOwners() - // const isOwner = owners.includes(accountTwo.address) - - // expect(isOwner).toBeFalsy() - // expect(userOpReceipt.success).toBeTruthy() - // }, 60000) - - // test.skip("should use rhinestone to call ownable executor", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const initData = encodePacked( - // ["address"], - // [await smartAccount2.getAddress()] - // ) - - // const ownableExecutorModule2 = getOwnableExecuter({ - // owner: await smartAccount2.getAddress(), - // }); - - // // First, we need to install the OwnableExecutor module on SA 2 - // let isInstalled = await smartAccount2.isModuleInstalled({ - // type: 'executor', - // module: OWNABLE_EXECUTOR - // }) - - // if (!isInstalled) { - // await smartAccount2.installModule({ - // module: ownableExecutorModule2.module, - // type: ownableExecutorModule2.type, - // data: ownableExecutorModule2.initData - // }) - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule) - - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const transferTransaction = { - // target: token as `0x${string}`, - // callData: transferEncodedCall, - // value: 0n - // } - - // const execution = getExecuteOnOwnedAccountAction({ownedAccount: await smartAccount.getAddress(), execution: transferTransaction}) - // const receipt = await smartAccount2.sendTransaction([{to: execution.target, data: execution.callData, value: 0n}]); - // console.log(receipt, "receipt"); - - // expect(receipt.userOpHash).toBeTruthy() - // }, 60000) -}) diff --git a/tests/modules.ownableValidator.install.write.test.ts b/tests/modules.ownableValidator.install.write.test.ts deleted file mode 100644 index 6ccbf0969..000000000 --- a/tests/modules.ownableValidator.install.write.test.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { - http, - type Account, - type Chain, - type Hex, - type WalletClient, - createWalletClient -} from "viem" -import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { - type NexusSmartAccount, - type Transaction, - createSmartAccountClient -} from "../src/account" -import { type TestFileNetworkType, toNetwork } from "./src/testSetup" -import { - getTestAccount, - killNetwork, - toTestClient, - topUp -} from "./src/testUtils" -import type { MasterClient, NetworkConfig } from "./src/testUtils" - -const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" - -describe("modules.ownable.validator.install.write", () => { - let network: NetworkConfig - // Nexus Config - let chain: Chain - let bundlerUrl: string - let walletClient: WalletClient - - // Test utils - let testClient: MasterClient - let account: Account - let recipientAccount: Account - let smartAccount: NexusSmartAccount - let smartAccountAddress: Hex - - beforeAll(async () => { - network = await toNetwork(NETWORK_TYPE) - - chain = network.chain - bundlerUrl = network.bundlerUrl - - account = getTestAccount(0) - recipientAccount = getTestAccount(3) - - walletClient = createWalletClient({ - account, - chain, - transport: http() - }) - - testClient = toTestClient(chain, getTestAccount(0)) - - smartAccount = await createSmartAccountClient({ - signer: walletClient, - bundlerUrl, - chain - }) - - smartAccountAddress = await smartAccount.getAddress() - }) - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should fund the smart account", async () => { - await topUp(testClient, smartAccountAddress) - const [balance] = await smartAccount.getBalances() - expect(balance.amount > 0) - }) - - test("should have account addresses", async () => { - const addresses = await Promise.all([ - account.address, - smartAccount.getAddress() - ]) - expect(addresses.every(Boolean)).to.be.true - expect(addresses).toStrictEqual([ - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account - ]) - }) - - test("should send eth", async () => { - const balanceBefore = await testClient.getBalance({ - address: recipientAccount.address - }) - const tx: Transaction = { - to: recipientAccount.address, - value: 1n - } - const { wait } = await smartAccount.sendTransaction(tx) - const { success } = await wait() - const balanceAfter = await testClient.getBalance({ - address: recipientAccount.address - }) - expect(success).toBe(true) - expect(balanceAfter - balanceBefore).toBe(1n) - }) - - // test.skip("install Ownable Executor", async () => { - // let isInstalled = await smartAccount.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - - // if (!isInstalled) { - // const receipt = await smartAccount.installModule({ - // moduleAddress: ownableExecutorModule.moduleAddress, - // type: ownableExecutorModule.type, - // data: ownableExecutorModule.data - // }) - - // smartAccount.setActiveExecutionModule(ownableExecutorModule) - - // expect(receipt.success).toBe(true) - // } - // }, 60000) - - // test.skip("uninstall Ownable Executor", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const ownableExecutorModule2 = await createOwnableExecutorModule(smartAccount2, OWNABLE_EXECUTOR) - - // let isInstalled = await smartAccount2.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - - // if (isInstalled) { - // await smartAccount2.uninstallModule({ - // moduleAddress: ownableExecutorModule2.moduleAddress, - // type: ownableExecutorModule2.type, - // data: ownableExecutorModule2.data - // }) - // } - - // }, 60000) - - // test.skip("Ownable Executor Module should be installed", async () => { - // const isInstalled = await smartAccount.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - // console.log(isInstalled, "isInstalled") - // expect(isInstalled).toBeTruthy() - // }, 60000) - - // test.skip("should add an owner to the module", async () => { - // const ownersBefore = await ownableExecutorModule.getOwners() - // const isOwnerBefore = ownersBefore.includes(accountTwo.address) - - // if (isOwnerBefore) { - // console.log("Owner already exists in list, skipping test case ...") - // return - // } - - // const userOpReceipt = await ownableExecutorModule.addOwner( - // accountTwo.address - // ) - - // const owners = await ownableExecutorModule.getOwners() - // const isOwner = owners.includes(accountTwo.address) - - // expect(isOwner).toBeTruthy() - // expect(userOpReceipt.success).toBeTruthy() - // }, 60000) - - // test.skip("EOA 2 can execute actions on behalf of SA 1", async () => { - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const owners = await ownableExecutorModule.getOwners() - // const isOwner = owners.includes(accountTwo.address) - // expect(isOwner).toBeTruthy() - - // const balanceBefore = await smartAccount.getBalances([token]) - // console.log("balanceBefore", balanceBefore) - - // const calldata = encodeFunctionData({ - // abi: parseAbi([ - // "function executeOnOwnedAccount(address ownedAccount, bytes callData)" - // ]), - // functionName: "executeOnOwnedAccount", - // args: [ - // await smartAccount.getAddress(), - // encodePacked( - // ["address", "uint256", "bytes"], - // [token, BigInt(Number(0)), transferEncodedCall] - // ) - // ] - // }) - - // // EOA 2 (walletClientTwo) executes an action on behalf of SA 1 which is owned by EOA 1 (walletClientOne) - // const txHash = await walletClientTwo.sendTransaction({ - // account: accountTwo, // Called by delegated EOA owner - // to: ownableExecutorModule.moduleAddress, - // data: calldata, - // value: 0n - // }) - - // const balanceAfter = await smartAccount.getBalances([token]) - // console.log("balanceAfter", balanceAfter) - - // expect(txHash).toBeTruthy() - // }, 60000) - - // test("SA 2 can execute actions on behalf of SA 1", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const transferTransaction = { - // to: token, - // data: transferEncodedCall, - // value: 0n - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule) - // const receipt = await smartAccount2.sendTransactionWithExecutor([transferTransaction], await smartAccount.getAddress()); - // console.log(receipt, "receipt"); - - // expect(receipt.userOpHash).toBeTruthy() - // expect(receipt.success).toBe(true) - // }, 60000) - - // test.skip("SA 2 can execute actions on behalf of SA 1 using module instance instead of smart account instance", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const initData = encodePacked( - // ["address"], - // [await smartAccount2.getAddress()] - // ) - // const ownableExecutorModule2 = await createOwnableExecutorModule(smartAccount2, OWNABLE_EXECUTOR, initData) - - // // First, we need to install the OwnableExecutor module on SA 2 - // let isInstalled = await smartAccount2.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - - // if (!isInstalled) { - // await smartAccount2.installModule({ - // moduleAddress: ownableExecutorModule2.moduleAddress, - // type: ownableExecutorModule2.type, - // data: ownableExecutorModule2.data - // }) - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule) - - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const owners = await ownableExecutorModule2.getOwners() - - // // check if SA 2 is as an owner of SA 1 - // const isOwner = owners.includes(await smartAccount2.getAddress()) - // if(!isOwner) { - // const userOpReceipt = await ownableExecutorModule2.addOwner( - // await smartAccount2.getAddress() - // ) - // expect(userOpReceipt.success).toBeTruthy() - // } - - // const transferTransaction = { - // target: token as `0x${string}`, - // callData: transferEncodedCall, - // value: 0n - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule2) - // // SA 2 will execute the transferTransaction on behalf of SA 1 (smartAccount) - // const receipt = await ownableExecutorModule2.execute(transferTransaction, await smartAccount.getAddress()); - // console.log(receipt, "receipt"); - - // expect(receipt.userOpHash).toBeTruthy() - // expect(receipt.success).toBe(true) - // }, 60000) - - // test.skip("should remove an owner from the module", async () => { - // const userOpReceipt = await ownableExecutorModule.removeOwner( - // accountTwo.address - // ) - // const owners = await ownableExecutorModule.getOwners() - // const isOwner = owners.includes(accountTwo.address) - - // expect(isOwner).toBeFalsy() - // expect(userOpReceipt.success).toBeTruthy() - // }, 60000) - - // test.skip("should use rhinestone to call ownable executor", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const initData = encodePacked( - // ["address"], - // [await smartAccount2.getAddress()] - // ) - - // const ownableExecutorModule2 = getOwnableExecuter({ - // owner: await smartAccount2.getAddress(), - // }); - - // // First, we need to install the OwnableExecutor module on SA 2 - // let isInstalled = await smartAccount2.isModuleInstalled({ - // type: 'executor', - // module: OWNABLE_EXECUTOR - // }) - - // if (!isInstalled) { - // await smartAccount2.installModule({ - // module: ownableExecutorModule2.module, - // type: ownableExecutorModule2.type, - // data: ownableExecutorModule2.initData - // }) - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule) - - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const transferTransaction = { - // target: token as `0x${string}`, - // callData: transferEncodedCall, - // value: 0n - // } - - // const execution = getExecuteOnOwnedAccountAction({ownedAccount: await smartAccount.getAddress(), execution: transferTransaction}) - // const receipt = await smartAccount2.sendTransaction([{to: execution.target, data: execution.callData, value: 0n}]); - // console.log(receipt, "receipt"); - - // expect(receipt.userOpHash).toBeTruthy() - // }, 60000) -}) diff --git a/tests/modules.ownableValidator.uninstall.write.test.ts b/tests/modules.ownableValidator.uninstall.write.test.ts deleted file mode 100644 index 136e2b340..000000000 --- a/tests/modules.ownableValidator.uninstall.write.test.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { - http, - type Account, - type Chain, - type Hex, - type WalletClient, - createWalletClient -} from "viem" -import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { - type NexusSmartAccount, - type Transaction, - createSmartAccountClient -} from "../src/account" -import { type TestFileNetworkType, toNetwork } from "./src/testSetup" -import { - getTestAccount, - killNetwork, - toTestClient, - topUp -} from "./src/testUtils" -import type { MasterClient, NetworkConfig } from "./src/testUtils" - -const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" - -describe("modules.ownable.validator.uninstall.write", () => { - let network: NetworkConfig - // Nexus Config - let chain: Chain - let bundlerUrl: string - let walletClient: WalletClient - - // Test utils - let testClient: MasterClient - let account: Account - let recipientAccount: Account - let smartAccount: NexusSmartAccount - let smartAccountAddress: Hex - - beforeAll(async () => { - network = await toNetwork(NETWORK_TYPE) - - chain = network.chain - bundlerUrl = network.bundlerUrl - - account = getTestAccount(0) - recipientAccount = getTestAccount(3) - - walletClient = createWalletClient({ - account, - chain, - transport: http() - }) - - testClient = toTestClient(chain, getTestAccount(0)) - - smartAccount = await createSmartAccountClient({ - signer: walletClient, - bundlerUrl, - chain - }) - - smartAccountAddress = await smartAccount.getAddress() - }) - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should fund the smart account", async () => { - await topUp(testClient, smartAccountAddress) - const [balance] = await smartAccount.getBalances() - expect(balance.amount > 0) - }) - - test("should have account addresses", async () => { - const addresses = await Promise.all([ - account.address, - smartAccount.getAddress() - ]) - expect(addresses.every(Boolean)).to.be.true - expect(addresses).toStrictEqual([ - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account - ]) - }) - - test("should send eth", async () => { - const balanceBefore = await testClient.getBalance({ - address: recipientAccount.address - }) - const tx: Transaction = { - to: recipientAccount.address, - value: 1n - } - const { wait } = await smartAccount.sendTransaction(tx) - const { success } = await wait() - const balanceAfter = await testClient.getBalance({ - address: recipientAccount.address - }) - expect(success).toBe(true) - expect(balanceAfter - balanceBefore).toBe(1n) - }) - - // test.skip("install Ownable Executor", async () => { - // let isInstalled = await smartAccount.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - - // if (!isInstalled) { - // const receipt = await smartAccount.installModule({ - // moduleAddress: ownableExecutorModule.moduleAddress, - // type: ownableExecutorModule.type, - // data: ownableExecutorModule.data - // }) - - // smartAccount.setActiveExecutionModule(ownableExecutorModule) - - // expect(receipt.success).toBe(true) - // } - // }, 60000) - - // test.skip("uninstall Ownable Executor", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const ownableExecutorModule2 = await createOwnableExecutorModule(smartAccount2, OWNABLE_EXECUTOR) - - // let isInstalled = await smartAccount2.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - - // if (isInstalled) { - // await smartAccount2.uninstallModule({ - // moduleAddress: ownableExecutorModule2.moduleAddress, - // type: ownableExecutorModule2.type, - // data: ownableExecutorModule2.data - // }) - // } - - // }, 60000) - - // test.skip("Ownable Executor Module should be installed", async () => { - // const isInstalled = await smartAccount.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - // console.log(isInstalled, "isInstalled") - // expect(isInstalled).toBeTruthy() - // }, 60000) - - // test.skip("should add an owner to the module", async () => { - // const ownersBefore = await ownableExecutorModule.getOwners() - // const isOwnerBefore = ownersBefore.includes(accountTwo.address) - - // if (isOwnerBefore) { - // console.log("Owner already exists in list, skipping test case ...") - // return - // } - - // const userOpReceipt = await ownableExecutorModule.addOwner( - // accountTwo.address - // ) - - // const owners = await ownableExecutorModule.getOwners() - // const isOwner = owners.includes(accountTwo.address) - - // expect(isOwner).toBeTruthy() - // expect(userOpReceipt.success).toBeTruthy() - // }, 60000) - - // test.skip("EOA 2 can execute actions on behalf of SA 1", async () => { - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const owners = await ownableExecutorModule.getOwners() - // const isOwner = owners.includes(accountTwo.address) - // expect(isOwner).toBeTruthy() - - // const balanceBefore = await smartAccount.getBalances([token]) - // console.log("balanceBefore", balanceBefore) - - // const calldata = encodeFunctionData({ - // abi: parseAbi([ - // "function executeOnOwnedAccount(address ownedAccount, bytes callData)" - // ]), - // functionName: "executeOnOwnedAccount", - // args: [ - // await smartAccount.getAddress(), - // encodePacked( - // ["address", "uint256", "bytes"], - // [token, BigInt(Number(0)), transferEncodedCall] - // ) - // ] - // }) - - // // EOA 2 (walletClientTwo) executes an action on behalf of SA 1 which is owned by EOA 1 (walletClientOne) - // const txHash = await walletClientTwo.sendTransaction({ - // account: accountTwo, // Called by delegated EOA owner - // to: ownableExecutorModule.moduleAddress, - // data: calldata, - // value: 0n - // }) - - // const balanceAfter = await smartAccount.getBalances([token]) - // console.log("balanceAfter", balanceAfter) - - // expect(txHash).toBeTruthy() - // }, 60000) - - // test("SA 2 can execute actions on behalf of SA 1", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const transferTransaction = { - // to: token, - // data: transferEncodedCall, - // value: 0n - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule) - // const receipt = await smartAccount2.sendTransactionWithExecutor([transferTransaction], await smartAccount.getAddress()); - // console.log(receipt, "receipt"); - - // expect(receipt.userOpHash).toBeTruthy() - // expect(receipt.success).toBe(true) - // }, 60000) - - // test.skip("SA 2 can execute actions on behalf of SA 1 using module instance instead of smart account instance", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const initData = encodePacked( - // ["address"], - // [await smartAccount2.getAddress()] - // ) - // const ownableExecutorModule2 = await createOwnableExecutorModule(smartAccount2, OWNABLE_EXECUTOR, initData) - - // // First, we need to install the OwnableExecutor module on SA 2 - // let isInstalled = await smartAccount2.isModuleInstalled({ - // type: 'executor', - // moduleAddress: OWNABLE_EXECUTOR - // }) - - // if (!isInstalled) { - // await smartAccount2.installModule({ - // moduleAddress: ownableExecutorModule2.moduleAddress, - // type: ownableExecutorModule2.type, - // data: ownableExecutorModule2.data - // }) - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule) - - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const owners = await ownableExecutorModule2.getOwners() - - // // check if SA 2 is as an owner of SA 1 - // const isOwner = owners.includes(await smartAccount2.getAddress()) - // if(!isOwner) { - // const userOpReceipt = await ownableExecutorModule2.addOwner( - // await smartAccount2.getAddress() - // ) - // expect(userOpReceipt.success).toBeTruthy() - // } - - // const transferTransaction = { - // target: token as `0x${string}`, - // callData: transferEncodedCall, - // value: 0n - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule2) - // // SA 2 will execute the transferTransaction on behalf of SA 1 (smartAccount) - // const receipt = await ownableExecutorModule2.execute(transferTransaction, await smartAccount.getAddress()); - // console.log(receipt, "receipt"); - - // expect(receipt.userOpHash).toBeTruthy() - // expect(receipt.success).toBe(true) - // }, 60000) - - // test.skip("should remove an owner from the module", async () => { - // const userOpReceipt = await ownableExecutorModule.removeOwner( - // accountTwo.address - // ) - // const owners = await ownableExecutorModule.getOwners() - // const isOwner = owners.includes(accountTwo.address) - - // expect(isOwner).toBeFalsy() - // expect(userOpReceipt.success).toBeTruthy() - // }, 60000) - - // test.skip("should use rhinestone to call ownable executor", async () => { - // const smartAccount2: NexusSmartAccount = await createSmartAccountClient({ - // signer: walletClientTwo, - // bundlerUrl - // }) - - // const initData = encodePacked( - // ["address"], - // [await smartAccount2.getAddress()] - // ) - - // const ownableExecutorModule2 = getOwnableExecuter({ - // owner: await smartAccount2.getAddress(), - // }); - - // // First, we need to install the OwnableExecutor module on SA 2 - // let isInstalled = await smartAccount2.isModuleInstalled({ - // type: 'executor', - // module: OWNABLE_EXECUTOR - // }) - - // if (!isInstalled) { - // await smartAccount2.installModule({ - // module: ownableExecutorModule2.module, - // type: ownableExecutorModule2.type, - // data: ownableExecutorModule2.initData - // }) - // } - - // smartAccount2.setActiveExecutionModule(ownableExecutorModule) - - // const valueToTransfer = parseEther("0.1") - // const recipient = accountTwo.address - // const transferEncodedCall = encodeFunctionData({ - // abi: parseAbi(["function transfer(address to, uint256 value)"]), - // functionName: "transfer", - // args: [recipient, valueToTransfer] - // }) - - // const transferTransaction = { - // target: token as `0x${string}`, - // callData: transferEncodedCall, - // value: 0n - // } - - // const execution = getExecuteOnOwnedAccountAction({ownedAccount: await smartAccount.getAddress(), execution: transferTransaction}) - // const receipt = await smartAccount2.sendTransaction([{to: execution.target, data: execution.callData, value: 0n}]); - // console.log(receipt, "receipt"); - - // expect(receipt.userOpHash).toBeTruthy() - // }, 60000) -}) diff --git a/tsconfig/tsconfig.cjs.json b/tsconfig/tsconfig.cjs.json index d5607ac12..0b9054568 100644 --- a/tsconfig/tsconfig.cjs.json +++ b/tsconfig/tsconfig.cjs.json @@ -1,10 +1,16 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "commonjs", - "outDir": "../dist/_cjs", - "removeComments": true, - "verbatimModuleSyntax": false, - "noEmit": false - } -} + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../dist/_cjs", + "removeComments": true, + "verbatimModuleSyntax": false, + "noEmit": false, + "rootDir": "../src/sdk" + }, + "exclude": [ + "../src/test/**/*.*", + "../src/sdk/**/*.test.*", + "../src/sdk/**/*.spec.*", + ] +} \ No newline at end of file diff --git a/tsconfig/tsconfig.esm.json b/tsconfig/tsconfig.esm.json index 136a81fc1..35640e490 100644 --- a/tsconfig/tsconfig.esm.json +++ b/tsconfig/tsconfig.esm.json @@ -3,6 +3,12 @@ "compilerOptions": { "module": "es2015", "outDir": "../dist/_esm", - "noEmit": false - } -} + "noEmit": false, + "rootDir": "../src/sdk" + }, + "exclude": [ + "../src/test/**/*.*", + "../src/sdk/**/*.test.*", + "../src/sdk/**/*.spec.*", + ] +} \ No newline at end of file diff --git a/tsconfig/tsconfig.json b/tsconfig/tsconfig.json index d820773c9..4d8f6dbae 100644 --- a/tsconfig/tsconfig.json +++ b/tsconfig/tsconfig.json @@ -7,7 +7,6 @@ "../src/**/*.test.ts", "../src/**/*.test-d.ts", "../src/**/*.bench.ts", - "../tests/**/*.*" ], "compilerOptions": { "moduleResolution": "node", diff --git a/tsconfig/tsconfig.types.json b/tsconfig/tsconfig.types.json index bb70b996d..6ee2578c0 100644 --- a/tsconfig/tsconfig.types.json +++ b/tsconfig/tsconfig.types.json @@ -7,6 +7,12 @@ "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, - "noEmit": false - } -} + "noEmit": false, + "rootDir": "../src/sdk" + }, + "exclude": [ + "../src/test/**/*.*", + "../src/sdk/**/*.test.*", + "../src/sdk/**/*.spec.*", + ] +} \ No newline at end of file diff --git a/typedoc.json b/typedoc.json index 6a717b22a..1b6162e2c 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,6 +1,11 @@ { "$schema": "https://typedoc.org/schema.json", "entryPoints": ["src/index.ts"], + "exclude": [ + "src/sdk/account/utils/**/*.ts", + "src/sdk/__contracts/**/*.ts", + "src/test/**/*.ts" + ], "basePath": "src", "includes": "src", "out": "docs",