diff --git a/.size-limit.json b/.size-limit.json index 56e5b3fb3..3a3d9b3ef 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,27 +2,27 @@ { "name": "core (esm)", "path": "./dist/_esm/index.js", - "limit": "30 kB", + "limit": "50 kB", "import": "*", "ignore": ["node:fs", "fs", "path", "os", "crypto"] }, { "name": "core (cjs)", "path": "./dist/_cjs/index.js", - "limit": "50 kB", + "limit": "80 kB", "ignore": ["node:fs", "fs", "path", "os", "crypto"] }, { "name": "bundler (tree-shaking)", "path": "./dist/_esm/clients/createBicoBundlerClient.js", - "limit": "20 kB", + "limit": "30 kB", "import": "{ createBicoBundlerClient }", "ignore": ["node:fs", "fs", "path", "os", "crypto"] }, { "name": "paymaster (tree-shaking)", "path": "./dist/_esm/clients/createBicoPaymasterClient.js", - "limit": "20 kB", + "limit": "30 kB", "import": "{ createBicoPaymasterClient }", "ignore": ["node:fs", "fs", "path", "os", "crypto"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0c1c130..4dc51bd59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ # @biconomy/sdk +## 0.0.10 + +### Patch Changes + +- Added Distributed Session Keys w/ Ownable & Session examples + +## 0.0.9 + +### Patch Changes + +- Added DAN helpers, keyGen + sigGen + +## 0.0.8 + +### Patch Changes + +- Paymaster script fix + +## 0.0.7 + +### Patch Changes + +- Include missing deps + +## 0.0.5 + +### Patch Changes + +- Alter sessions terminology + +## 0.0.4 + +### Patch Changes + +- renamed validator modules + +## 0.0.3 + +### Patch Changes + +- modules dx improvements + ## 0.0.0 diff --git a/README.md b/README.md index 85127b474..90f7a4536 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The Biconomy SDK is your all-in-one toolkit for building decentralized applicati 1. **Add the package and install dependencies:** ```bash -bun add @biconomy/sdk viem +bun add @biconomy/sdk viem @rhinestone/module-sdk ``` 2. **Install dependencies:** @@ -41,22 +41,17 @@ bun i ```typescript import { createNexusClient } from "@biconomy/sdk"; -import { http, publicClient } from "viem"; - -const publicClient = createPublicClient({ - chain: mainnet, - transport: http(), -}); +import { http } from "viem"; const nexusClient = await createNexusClient({ - holder: account, + signer: account, chain, transport: http(), bundlerTransport: http(bundlerUrl), }); const hash = await nexusClient.sendTransaction({ calls: [to: "0x...", value: 1] }); -const { status, transactionHash } = await publicClient.waitForTransactionReceipt({ hash }); +const { status, transactionHash } = await nexusClient.waitForTransactionReceipt({ hash }); ``` diff --git a/bun.lockb b/bun.lockb index d5e385874..3d7bee684 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 89ec5540e..57a69c9a2 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,49 @@ { - "type": "module", - "main": "./dist/_cjs/index.js", - "module": "./dist/_esm/index.js", - "types": "./dist/_types/index.d.ts", - "typings": "./dist/_types/index.d.ts", - "homepage": "https://biconomy.io", - "sideEffects": false, "name": "@biconomy/sdk", + "version": "0.0.10", "author": "Biconomy", - "version": "0.0.0", - "description": "SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.", - "keywords": [ - "erc-7579", - "modular smart account", - "account abstraction", - "biconomy", - "sdk" - ], - "license": "MIT", "repository": "github:bcnmy/sdk", + "main": "./dist/_cjs/index.js", + "module": "./dist/_esm/index.js", + "devDependencies": { + "@biomejs/biome": "1.6.0", + "@changesets/cli": "^2.27.1", + "@commitlint/cli": "^19.4.1", + "@commitlint/config-conventional": "^19.4.1", + "@ethersproject/abi": "^5.7.0", + "@ethersproject/providers": "^5.7.2", + "@ethersproject/wallet": "^5.7.0", + "@pimlico/alto": "^0.0.4", + "@size-limit/esbuild-why": "^11", + "@size-limit/preset-small-lib": "^11", + "@types/bun": "latest", + "@types/yargs": "^17.0.33", + "@vitest/coverage-v8": "^1.3.1", + "buffer": "^6.0.3", + "concurrently": "^8.2.2", + "dotenv": "^16.4.5", + "ethers": "^6.13.2", + "execa": "^9.3.1", + "get-port": "^7.1.0", + "gh-pages": "^6.1.1", + "nexus": "github:bcnmy/nexus#773943fb7bf6cd14a0dc6dcb9f513db53213d1d5", + "prool": "^0.0.16", + "rimraf": "^5.0.5", + "simple-git-hooks": "^2.9.0", + "size-limit": "^11.1.5", + "ts-node": "^10.9.2", + "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.20.0", + "@rhinestone/module-sdk": "^0.1.25" + }, "exports": { ".": { "types": "./dist/_types/index.d.ts", @@ -46,10 +71,25 @@ "default": "./_cjs/modules/index.js" } }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "description": "SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.", "files": [ "dist/*", "README.md" ], + "homepage": "https://biconomy.io", + "keywords": [ + "erc-7579", + "modular smart account", + "account abstraction", + "biconomy", + "sdk" + ], + "license": "MIT", "scripts": { "format": "biome format . --write", "lint": "biome check .", @@ -78,54 +118,15 @@ "fetch:deployment": "bun run ./scripts/fetch:deployment.ts && bun run lint --apply-unsafe", "fetch:deployment:raw": "bun run ./scripts/fetch:deployment.ts" }, - "devDependencies": { - "@biomejs/biome": "1.6.0", - "@changesets/cli": "^2.27.1", - "@commitlint/cli": "^19.4.1", - "@commitlint/config-conventional": "^19.4.1", - "@ethersproject/abi": "^5.7.0", - "@ethersproject/providers": "^5.7.2", - "@ethersproject/wallet": "^5.7.0", - "@pimlico/alto": "^0.0.4", - "@size-limit/esbuild-why": "^11", - "@size-limit/preset-small-lib": "^11", - "@types/bun": "latest", - "@types/yargs": "^17.0.33", - "@vitest/coverage-v8": "^1.3.1", - "buffer": "^6.0.3", - "concurrently": "^8.2.2", - "dotenv": "^16.4.5", - "ethers": "^6.13.2", - "execa": "^9.3.1", - "get-port": "^7.1.0", - "gh-pages": "^6.1.1", - "nexus": "github:bcnmy/nexus#87fc1ed2e9e91cd35ec2c9b2e5c40d311fcb28bb", - "prool": "^0.0.16", - "rimraf": "^5.0.5", - "simple-git-hooks": "^2.9.0", - "size-limit": "^11.1.5", - "ts-node": "^10.9.2", - "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.20.0" - }, - "commitlint": { - "extends": [ - "@commitlint/config-conventional" - ] - }, + "sideEffects": false, "simple-git-hooks": { "pre-commit": "bun run format && bun run lint:fix", "commit-msg": "npx --no -- commitlint --edit ${1}" }, + "type": "module", + "types": "./dist/_types/index.d.ts", + "typings": "./dist/_types/index.d.ts", "dependencies": { - "@rhinestone/module-sdk": "^0.1.17" + "@silencelaboratories/walletprovider-sdk": "^0.3.0" } } \ No newline at end of file diff --git a/scripts/fetch:deployment.ts b/scripts/fetch:deployment.ts index e2a45aa4b..8d7dc7e6d 100644 --- a/scripts/fetch:deployment.ts +++ b/scripts/fetch:deployment.ts @@ -10,7 +10,7 @@ type FetchDetails = { } const { nexusDeploymentPath = "../node_modules/nexus/deployments", - chainName = "anvil-50981", + chainName = "anvil-51502", forSrc = ["K1ValidatorFactory", "Nexus", "K1Validator"] } = yargs(hideBin(process.argv)).argv as unknown as FetchDetails diff --git a/scripts/send:userOp.ts b/scripts/send:userOp.ts index 0e530d5f6..c76dce507 100644 --- a/scripts/send:userOp.ts +++ b/scripts/send:userOp.ts @@ -9,9 +9,6 @@ import { biconomyPaymasterContext } from "../src/sdk/clients/createBicoPaymaster config() -const k1ValidatorAddress = "0x663E709f60477f07885230E213b8149a7027239B" -const factoryAddress = "0x887Ca6FaFD62737D0E79A2b8Da41f0B15A864778" - export const getConfig = () => { const chainId = Number.parseInt(process.env.CHAIN_ID || "0") const chain = getChain(chainId) @@ -43,11 +40,17 @@ const main = async () => { const nexusAccount = await toNexusAccount({ signer: account, chain, - transport: http(), - k1ValidatorAddress, - factoryAddress + transport: http() + }) + + const nexusBalance = await publicClient.getBalance({ + address: nexusAccount.address }) + if (nexusBalance === 0n) { + throw new Error(`Insufficient balance at address: ${nexusAccount.address}`) + } + const bicoBundler = createBicoBundlerClient({ chain, bundlerUrl, @@ -74,28 +77,30 @@ const main = async () => { 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, + calls: [ + { + to: recipient, + value: 1n + } + ], account: nexusAccount }) ]) console.timeEnd("read methods") const successCount = results.filter((result) => result.status === "fulfilled") + const failures = results.filter((result) => result.status === "rejected") console.log( `running the ${usesAltoBundler ? "Alto" : "Bico"} bundler with ${ successCount.length - } successful calls` + } successful calls and ${results.length - successCount.length} failed calls` ) + if (failures.length > 0) { + console.log({ failures }) + process.exit(1) + } + console.time("write methods") const hash = await bicoBundler.sendUserOperation({ calls: [ diff --git a/src/sdk/__contracts/abi/K1ValidatorAbi.ts b/src/sdk/__contracts/abi/K1ValidatorAbi.ts deleted file mode 100644 index 4071d9efb..000000000 --- a/src/sdk/__contracts/abi/K1ValidatorAbi.ts +++ /dev/null @@ -1,317 +0,0 @@ -export const K1ValidatorAbi = [ - { - inputs: [], - name: "InvalidDataLength", - type: "error" - }, - { - inputs: [], - name: "ModuleAlreadyInitialized", - type: "error" - }, - { - inputs: [], - name: "NewOwnerIsContract", - type: "error" - }, - { - inputs: [], - name: "NoOwnerProvided", - type: "error" - }, - { - inputs: [], - name: "ZeroAddressNotAllowed", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "sender", - type: "address" - } - ], - name: "addSafeSender", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "smartAccount", - type: "address" - } - ], - name: "isInitialized", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "uint256", - name: "typeId", - type: "uint256" - } - ], - name: "isModuleType", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool" - } - ], - stateMutability: "pure", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "sender", - type: "address" - }, - { - internalType: "bytes32", - name: "hash", - type: "bytes32" - }, - { - internalType: "bytes", - name: "signature", - type: "bytes" - } - ], - name: "isValidSignatureWithSender", - outputs: [ - { - internalType: "bytes4", - name: "sigValidationResult", - type: "bytes4" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "name", - outputs: [ - { - internalType: "string", - name: "", - type: "string" - } - ], - stateMutability: "pure", - type: "function" - }, - { - inputs: [ - { - internalType: "bytes", - name: "data", - type: "bytes" - } - ], - name: "onInstall", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "bytes", - name: "", - type: "bytes" - } - ], - name: "onUninstall", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "sender", - type: "address" - } - ], - name: "removeSafeSender", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "", - type: "address" - } - ], - name: "smartAccountOwners", - outputs: [ - { - internalType: "address", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "supportsNestedTypedDataSign", - outputs: [ - { - internalType: "bytes32", - name: "result", - type: "bytes32" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "newOwner", - type: "address" - } - ], - name: "transferOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "bytes32", - name: "hash", - type: "bytes32" - }, - { - internalType: "bytes", - name: "sig", - type: "bytes" - }, - { - internalType: "bytes", - name: "data", - type: "bytes" - } - ], - name: "validateSignatureWithData", - outputs: [ - { - internalType: "bool", - name: "validSig", - type: "bool" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - components: [ - { - internalType: "address", - name: "sender", - type: "address" - }, - { - internalType: "uint256", - name: "nonce", - type: "uint256" - }, - { - internalType: "bytes", - name: "initCode", - type: "bytes" - }, - { - internalType: "bytes", - name: "callData", - type: "bytes" - }, - { - internalType: "bytes32", - name: "accountGasLimits", - type: "bytes32" - }, - { - internalType: "uint256", - name: "preVerificationGas", - type: "uint256" - }, - { - internalType: "bytes32", - name: "gasFees", - type: "bytes32" - }, - { - internalType: "bytes", - name: "paymasterAndData", - type: "bytes" - }, - { - internalType: "bytes", - name: "signature", - type: "bytes" - } - ], - internalType: "struct PackedUserOperation", - name: "userOp", - type: "tuple" - }, - { - internalType: "bytes32", - name: "userOpHash", - type: "bytes32" - } - ], - name: "validateUserOp", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "version", - outputs: [ - { - internalType: "string", - name: "", - type: "string" - } - ], - stateMutability: "pure", - type: "function" - } -] as const diff --git a/src/sdk/__contracts/abi/K1ValidatorFactoryAbi.ts b/src/sdk/__contracts/abi/K1ValidatorFactoryAbi.ts deleted file mode 100644 index 74a7570f1..000000000 --- a/src/sdk/__contracts/abi/K1ValidatorFactoryAbi.ts +++ /dev/null @@ -1,386 +0,0 @@ -export const K1ValidatorFactoryAbi = [ - { - inputs: [ - { - internalType: "address", - name: "implementation", - type: "address" - }, - { - internalType: "address", - name: "factoryOwner", - type: "address" - }, - { - internalType: "address", - name: "k1Validator", - type: "address" - }, - { - internalType: "contract NexusBootstrap", - name: "bootstrapper", - type: "address" - }, - { - internalType: "contract IERC7484", - name: "registry", - type: "address" - } - ], - stateMutability: "nonpayable", - type: "constructor" - }, - { - inputs: [], - name: "AlreadyInitialized", - type: "error" - }, - { - inputs: [], - name: "InnerCallFailed", - type: "error" - }, - { - inputs: [], - name: "InvalidEntryPointAddress", - type: "error" - }, - { - inputs: [], - name: "NewOwnerIsZeroAddress", - type: "error" - }, - { - inputs: [], - name: "NoHandoverRequest", - type: "error" - }, - { - inputs: [], - name: "Unauthorized", - type: "error" - }, - { - inputs: [], - name: "ZeroAddressNotAllowed", - type: "error" - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "account", - type: "address" - }, - { - indexed: true, - internalType: "address", - name: "owner", - type: "address" - }, - { - indexed: true, - internalType: "uint256", - name: "index", - type: "uint256" - } - ], - name: "AccountCreated", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "pendingOwner", - type: "address" - } - ], - name: "OwnershipHandoverCanceled", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "pendingOwner", - type: "address" - } - ], - name: "OwnershipHandoverRequested", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "oldOwner", - type: "address" - }, - { - indexed: true, - internalType: "address", - name: "newOwner", - type: "address" - } - ], - name: "OwnershipTransferred", - type: "event" - }, - { - inputs: [], - name: "ACCOUNT_IMPLEMENTATION", - outputs: [ - { - internalType: "address", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "BOOTSTRAPPER", - outputs: [ - { - internalType: "contract NexusBootstrap", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "K1_VALIDATOR", - outputs: [ - { - internalType: "address", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "REGISTRY", - outputs: [ - { - internalType: "contract IERC7484", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "epAddress", - type: "address" - }, - { - internalType: "uint32", - name: "unstakeDelaySec", - type: "uint32" - } - ], - name: "addStake", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [], - name: "cancelOwnershipHandover", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "pendingOwner", - type: "address" - } - ], - name: "completeOwnershipHandover", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "eoaOwner", - type: "address" - }, - { - internalType: "uint256", - name: "index", - type: "uint256" - }, - { - internalType: "address[]", - name: "attesters", - type: "address[]" - }, - { - internalType: "uint8", - name: "threshold", - type: "uint8" - } - ], - name: "computeAccountAddress", - outputs: [ - { - internalType: "address payable", - name: "expectedAddress", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "eoaOwner", - type: "address" - }, - { - internalType: "uint256", - name: "index", - type: "uint256" - }, - { - internalType: "address[]", - name: "attesters", - type: "address[]" - }, - { - internalType: "uint8", - name: "threshold", - type: "uint8" - } - ], - name: "createAccount", - outputs: [ - { - internalType: "address payable", - name: "", - type: "address" - } - ], - stateMutability: "payable", - type: "function" - }, - { - inputs: [], - name: "owner", - outputs: [ - { - internalType: "address", - name: "result", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "pendingOwner", - type: "address" - } - ], - name: "ownershipHandoverExpiresAt", - outputs: [ - { - internalType: "uint256", - name: "result", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "renounceOwnership", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [], - name: "requestOwnershipHandover", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "newOwner", - type: "address" - } - ], - name: "transferOwnership", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "epAddress", - type: "address" - } - ], - name: "unlockStake", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "epAddress", - type: "address" - }, - { - internalType: "address payable", - name: "withdrawAddress", - type: "address" - } - ], - name: "withdrawStake", - outputs: [], - stateMutability: "nonpayable", - type: "function" - } -] as const diff --git a/src/sdk/__contracts/abi/NexusAbi.ts b/src/sdk/__contracts/abi/NexusAbi.ts deleted file mode 100644 index 67e3a49d3..000000000 --- a/src/sdk/__contracts/abi/NexusAbi.ts +++ /dev/null @@ -1,1215 +0,0 @@ -export const NexusAbi = [ - { - inputs: [ - { - internalType: "address", - name: "anEntryPoint", - type: "address" - } - ], - stateMutability: "nonpayable", - type: "constructor" - }, - { - inputs: [], - name: "AccountAccessUnauthorized", - type: "error" - }, - { - inputs: [], - name: "CanNotRemoveLastValidator", - type: "error" - }, - { - inputs: [], - name: "EmergencyTimeLockNotExpired", - type: "error" - }, - { - inputs: [], - name: "EnableModeSigError", - type: "error" - }, - { - inputs: [], - name: "EntryPointCanNotBeZero", - type: "error" - }, - { - inputs: [], - name: "ExecutionFailed", - type: "error" - }, - { - inputs: [ - { - internalType: "bytes4", - name: "selector", - type: "bytes4" - } - ], - name: "FallbackAlreadyInstalledForSelector", - type: "error" - }, - { - inputs: [], - name: "FallbackCallTypeInvalid", - type: "error" - }, - { - inputs: [], - name: "FallbackHandlerUninstallFailed", - type: "error" - }, - { - inputs: [ - { - internalType: "bytes4", - name: "selector", - type: "bytes4" - } - ], - name: "FallbackNotInstalledForSelector", - type: "error" - }, - { - inputs: [], - name: "FallbackSelectorForbidden", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "currentHook", - type: "address" - } - ], - name: "HookAlreadyInstalled", - type: "error" - }, - { - inputs: [], - name: "HookPostCheckFailed", - type: "error" - }, - { - inputs: [], - name: "ImplementationIsNotAContract", - type: "error" - }, - { - inputs: [], - name: "InnerCallFailed", - type: "error" - }, - { - inputs: [], - name: "InvalidImplementationAddress", - type: "error" - }, - { - inputs: [], - name: "InvalidInput", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "module", - type: "address" - } - ], - name: "InvalidModule", - type: "error" - }, - { - inputs: [ - { - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - } - ], - name: "InvalidModuleTypeId", - type: "error" - }, - { - inputs: [], - name: "LinkedList_AlreadyInitialized", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "entry", - type: "address" - } - ], - name: "LinkedList_EntryAlreadyInList", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "entry", - type: "address" - } - ], - name: "LinkedList_InvalidEntry", - type: "error" - }, - { - inputs: [], - name: "LinkedList_InvalidPage", - type: "error" - }, - { - inputs: [ - { - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - } - ], - name: "MismatchModuleTypeId", - type: "error" - }, - { - inputs: [ - { - internalType: "bytes4", - name: "selector", - type: "bytes4" - } - ], - name: "MissingFallbackHandler", - type: "error" - }, - { - inputs: [], - name: "ModuleAddressCanNotBeZero", - type: "error" - }, - { - inputs: [ - { - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - }, - { - internalType: "address", - name: "module", - type: "address" - } - ], - name: "ModuleAlreadyInstalled", - type: "error" - }, - { - inputs: [ - { - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - }, - { - internalType: "address", - name: "module", - type: "address" - } - ], - name: "ModuleNotInstalled", - type: "error" - }, - { - inputs: [], - name: "NexusInitializationFailed", - type: "error" - }, - { - inputs: [], - name: "NoValidatorInstalled", - type: "error" - }, - { - inputs: [], - name: "UnauthorizedCallContext", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "operator", - type: "address" - } - ], - name: "UnauthorizedOperation", - type: "error" - }, - { - inputs: [ - { - internalType: "CallType", - name: "callType", - type: "bytes1" - } - ], - name: "UnsupportedCallType", - type: "error" - }, - { - inputs: [ - { - internalType: "ExecType", - name: "execType", - type: "bytes1" - } - ], - name: "UnsupportedExecType", - type: "error" - }, - { - inputs: [ - { - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - } - ], - name: "UnsupportedModuleType", - type: "error" - }, - { - inputs: [], - name: "UpgradeFailed", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "module", - type: "address" - } - ], - name: "ValidatorNotInstalled", - type: "error" - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "contract IERC7484", - name: "registry", - type: "address" - } - ], - name: "ERC7484RegistryConfigured", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "address", - name: "hook", - type: "address" - }, - { - indexed: false, - internalType: "uint256", - name: "timestamp", - type: "uint256" - } - ], - name: "EmergencyHookUninstallRequest", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "address", - name: "hook", - type: "address" - }, - { - indexed: false, - internalType: "uint256", - name: "timestamp", - type: "uint256" - } - ], - name: "EmergencyHookUninstallRequestReset", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - components: [ - { - internalType: "address", - name: "sender", - type: "address" - }, - { - internalType: "uint256", - name: "nonce", - type: "uint256" - }, - { - internalType: "bytes", - name: "initCode", - type: "bytes" - }, - { - internalType: "bytes", - name: "callData", - type: "bytes" - }, - { - internalType: "bytes32", - name: "accountGasLimits", - type: "bytes32" - }, - { - internalType: "uint256", - name: "preVerificationGas", - type: "uint256" - }, - { - internalType: "bytes32", - name: "gasFees", - type: "bytes32" - }, - { - internalType: "bytes", - name: "paymasterAndData", - type: "bytes" - }, - { - internalType: "bytes", - name: "signature", - type: "bytes" - } - ], - indexed: false, - internalType: "struct PackedUserOperation", - name: "userOp", - type: "tuple" - }, - { - indexed: false, - internalType: "bytes", - name: "innerCallRet", - type: "bytes" - } - ], - name: "Executed", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - }, - { - indexed: false, - internalType: "address", - name: "module", - type: "address" - } - ], - name: "ModuleInstalled", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - }, - { - indexed: false, - internalType: "address", - name: "module", - type: "address" - } - ], - name: "ModuleUninstalled", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "bytes", - name: "callData", - type: "bytes" - }, - { - indexed: false, - internalType: "bytes", - name: "result", - type: "bytes" - } - ], - name: "TryDelegateCallUnsuccessful", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "bytes", - name: "callData", - type: "bytes" - }, - { - indexed: false, - internalType: "bytes", - name: "result", - type: "bytes" - } - ], - name: "TryExecuteUnsuccessful", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "implementation", - type: "address" - } - ], - name: "Upgraded", - type: "event" - }, - { - stateMutability: "payable", - type: "fallback" - }, - { - inputs: [], - name: "DOMAIN_SEPARATOR", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "accountId", - outputs: [ - { - internalType: "string", - name: "", - type: "string" - } - ], - stateMutability: "pure", - type: "function" - }, - { - inputs: [], - name: "addDeposit", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [], - name: "eip712Domain", - outputs: [ - { - internalType: "bytes1", - name: "fields", - type: "bytes1" - }, - { - internalType: "string", - name: "name", - type: "string" - }, - { - internalType: "string", - name: "version", - type: "string" - }, - { - internalType: "uint256", - name: "chainId", - type: "uint256" - }, - { - internalType: "address", - name: "verifyingContract", - type: "address" - }, - { - internalType: "bytes32", - name: "salt", - type: "bytes32" - }, - { - internalType: "uint256[]", - name: "extensions", - type: "uint256[]" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "hook", - type: "address" - }, - { - internalType: "bytes", - name: "deInitData", - type: "bytes" - } - ], - name: "emergencyUninstallHook", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [], - name: "entryPoint", - outputs: [ - { - internalType: "address", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "ExecutionMode", - name: "mode", - type: "bytes32" - }, - { - internalType: "bytes", - name: "executionCalldata", - type: "bytes" - } - ], - name: "execute", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - internalType: "ExecutionMode", - name: "mode", - type: "bytes32" - }, - { - internalType: "bytes", - name: "executionCalldata", - type: "bytes" - } - ], - name: "executeFromExecutor", - outputs: [ - { - internalType: "bytes[]", - name: "returnData", - type: "bytes[]" - } - ], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - components: [ - { - internalType: "address", - name: "sender", - type: "address" - }, - { - internalType: "uint256", - name: "nonce", - type: "uint256" - }, - { - internalType: "bytes", - name: "initCode", - type: "bytes" - }, - { - internalType: "bytes", - name: "callData", - type: "bytes" - }, - { - internalType: "bytes32", - name: "accountGasLimits", - type: "bytes32" - }, - { - internalType: "uint256", - name: "preVerificationGas", - type: "uint256" - }, - { - internalType: "bytes32", - name: "gasFees", - type: "bytes32" - }, - { - internalType: "bytes", - name: "paymasterAndData", - type: "bytes" - }, - { - internalType: "bytes", - name: "signature", - type: "bytes" - } - ], - internalType: "struct PackedUserOperation", - name: "userOp", - type: "tuple" - }, - { - internalType: "bytes32", - name: "", - type: "bytes32" - } - ], - name: "executeUserOp", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [], - name: "getActiveHook", - outputs: [ - { - internalType: "address", - name: "hook", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "getDeposit", - outputs: [ - { - internalType: "uint256", - name: "result", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - 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" - }, - { - inputs: [ - { - internalType: "bytes4", - name: "selector", - type: "bytes4" - } - ], - name: "getFallbackHandlerBySelector", - outputs: [ - { - internalType: "CallType", - name: "", - type: "bytes1" - }, - { - internalType: "address", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "getImplementation", - outputs: [ - { - internalType: "address", - name: "implementation", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - 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" - }, - { - inputs: [ - { - internalType: "bytes32", - name: "structHash", - type: "bytes32" - } - ], - name: "hashTypedData", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "bytes", - name: "initData", - type: "bytes" - } - ], - name: "initializeAccount", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - }, - { - internalType: "address", - name: "module", - type: "address" - }, - { - internalType: "bytes", - name: "initData", - type: "bytes" - } - ], - name: "installModule", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - }, - { - internalType: "address", - name: "module", - type: "address" - }, - { - internalType: "bytes", - name: "additionalContext", - type: "bytes" - } - ], - name: "isModuleInstalled", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "bytes32", - name: "hash", - type: "bytes32" - }, - { - internalType: "bytes", - name: "signature", - type: "bytes" - } - ], - name: "isValidSignature", - outputs: [ - { - internalType: "bytes4", - name: "", - type: "bytes4" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "uint192", - name: "key", - type: "uint192" - } - ], - name: "nonce", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "proxiableUUID", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "registry", - outputs: [ - { - internalType: "contract IERC7484", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "contract IERC7484", - name: "newRegistry", - type: "address" - }, - { - internalType: "address[]", - name: "attesters", - type: "address[]" - }, - { - internalType: "uint8", - name: "threshold", - type: "uint8" - } - ], - name: "setRegistry", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - internalType: "ExecutionMode", - name: "mode", - type: "bytes32" - } - ], - name: "supportsExecutionMode", - outputs: [ - { - internalType: "bool", - name: "isSupported", - type: "bool" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - } - ], - name: "supportsModule", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "supportsNestedTypedDataSign", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "uint256", - name: "moduleTypeId", - type: "uint256" - }, - { - internalType: "address", - name: "module", - type: "address" - }, - { - internalType: "bytes", - name: "deInitData", - type: "bytes" - } - ], - name: "uninstallModule", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "newImplementation", - type: "address" - }, - { - internalType: "bytes", - name: "data", - type: "bytes" - } - ], - name: "upgradeToAndCall", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [ - { - components: [ - { - internalType: "address", - name: "sender", - type: "address" - }, - { - internalType: "uint256", - name: "nonce", - type: "uint256" - }, - { - internalType: "bytes", - name: "initCode", - type: "bytes" - }, - { - internalType: "bytes", - name: "callData", - type: "bytes" - }, - { - internalType: "bytes32", - name: "accountGasLimits", - type: "bytes32" - }, - { - internalType: "uint256", - name: "preVerificationGas", - type: "uint256" - }, - { - internalType: "bytes32", - name: "gasFees", - type: "bytes32" - }, - { - internalType: "bytes", - name: "paymasterAndData", - type: "bytes" - }, - { - internalType: "bytes", - name: "signature", - type: "bytes" - } - ], - internalType: "struct PackedUserOperation", - name: "op", - type: "tuple" - }, - { - internalType: "bytes32", - name: "userOpHash", - type: "bytes32" - }, - { - internalType: "uint256", - name: "missingAccountFunds", - type: "uint256" - } - ], - name: "validateUserOp", - outputs: [ - { - internalType: "uint256", - name: "validationData", - type: "uint256" - } - ], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "to", - type: "address" - }, - { - internalType: "uint256", - name: "amount", - type: "uint256" - } - ], - name: "withdrawDepositTo", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - stateMutability: "payable", - type: "receive" - } -] as const diff --git a/src/sdk/__contracts/abi/SmartSessionAbi.ts b/src/sdk/__contracts/abi/SmartSessionAbi.ts deleted file mode 100644 index e945f2e78..000000000 --- a/src/sdk/__contracts/abi/SmartSessionAbi.ts +++ /dev/null @@ -1,827 +0,0 @@ -export const SmartSessionAbi = [ - { - inputs: [{ internalType: "uint256", name: "index", type: "uint256" }], - name: "AssociatedArray_OutOfBounds", - type: "error" - }, - { - inputs: [{ internalType: "uint256", name: "index", type: "uint256" }], - name: "AssociatedArray_OutOfBounds", - type: "error" - }, - { - inputs: [ - { internalType: "uint64", name: "providedChainId", type: "uint64" } - ], - name: "ChainIdMismatch", - type: "error" - }, - { - inputs: [ - { internalType: "uint64", name: "providedChainId", type: "uint64" } - ], - name: "ChainIdMismatch", - type: "error" - }, - { - inputs: [{ internalType: "uint256", name: "index", type: "uint256" }], - name: "HashIndexOutOfBounds", - type: "error" - }, - { - inputs: [ - { internalType: "bytes32", name: "providedHash", type: "bytes32" }, - { internalType: "bytes32", name: "computedHash", type: "bytes32" } - ], - name: "HashMismatch", - type: "error" - }, - { - inputs: [ - { internalType: "bytes32", name: "providedHash", type: "bytes32" }, - { internalType: "bytes32", name: "computedHash", type: "bytes32" } - ], - name: "HashMismatch", - type: "error" - }, - { inputs: [], name: "InvalidActionId", type: "error" }, - { inputs: [], name: "InvalidCallTarget", type: "error" }, - { inputs: [], name: "InvalidData", type: "error" }, - { - inputs: [ - { internalType: "address", name: "account", type: "address" }, - { internalType: "bytes32", name: "hash", type: "bytes32" } - ], - name: "InvalidEnableSignature", - type: "error" - }, - { - inputs: [ - { - internalType: "contract ISessionValidator", - name: "sessionValidator", - type: "address" - } - ], - name: "InvalidISessionValidator", - type: "error" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" } - ], - name: "InvalidPermissionId", - type: "error" - }, - { inputs: [], name: "InvalidSelfCall", type: "error" }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" } - ], - name: "InvalidSession", - type: "error" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "address", name: "sessionValidator", type: "address" }, - { internalType: "address", name: "account", type: "address" }, - { internalType: "bytes32", name: "userOpHash", type: "bytes32" } - ], - name: "InvalidSessionKeySignature", - type: "error" - }, - { - inputs: [{ internalType: "address", name: "sender", type: "address" }], - name: "InvalidUserOpSender", - type: "error" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" } - ], - name: "NoPoliciesSet", - type: "error" - }, - { inputs: [], name: "PartlyEnabledActions", type: "error" }, - { inputs: [], name: "PartlyEnabledPolicies", type: "error" }, - { inputs: [], name: "PermissionPartlyEnabled", type: "error" }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "address", name: "policy", type: "address" } - ], - name: "PolicyViolation", - type: "error" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "address", name: "account", type: "address" } - ], - name: "SignerNotFound", - type: "error" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "address", name: "account", type: "address" } - ], - name: "SignerNotFound", - type: "error" - }, - { inputs: [], name: "UnsupportedExecutionType", type: "error" }, - { - inputs: [{ internalType: "address", name: "policy", type: "address" }], - name: "UnsupportedPolicy", - type: "error" - }, - { - inputs: [{ internalType: "address", name: "policy", type: "address" }], - name: "UnsupportedPolicy", - type: "error" - }, - { - inputs: [ - { internalType: "enum SmartSessionMode", name: "mode", type: "uint8" } - ], - name: "UnsupportedSmartSessionMode", - type: "error" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "PermissionId", - name: "permissionId", - type: "bytes32" - }, - { - indexed: false, - internalType: "address", - name: "account", - type: "address" - }, - { - indexed: false, - internalType: "uint256", - name: "newValue", - type: "uint256" - } - ], - name: "NonceIterated", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "PermissionId", - name: "permissionId", - type: "bytes32" - }, - { - indexed: false, - internalType: "enum PolicyType", - name: "policyType", - type: "uint8" - }, - { - indexed: false, - internalType: "address", - name: "policy", - type: "address" - }, - { - indexed: false, - internalType: "address", - name: "smartAccount", - type: "address" - } - ], - name: "PolicyDisabled", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "PermissionId", - name: "permissionId", - type: "bytes32" - }, - { - indexed: false, - internalType: "enum PolicyType", - name: "policyType", - type: "uint8" - }, - { - indexed: false, - internalType: "address", - name: "policy", - type: "address" - }, - { - indexed: false, - internalType: "address", - name: "smartAccount", - type: "address" - } - ], - name: "PolicyEnabled", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "PermissionId", - name: "permissionId", - type: "bytes32" - }, - { - indexed: false, - internalType: "address", - name: "account", - type: "address" - } - ], - name: "SessionCreated", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "PermissionId", - name: "permissionId", - type: "bytes32" - }, - { - indexed: false, - internalType: "address", - name: "smartAccount", - type: "address" - } - ], - name: "SessionRemoved", - type: "event" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "ActionId", name: "actionId", type: "bytes32" }, - { internalType: "address[]", name: "policies", type: "address[]" } - ], - name: "disableActionPolicies", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "address[]", name: "policies", type: "address[]" } - ], - name: "disableERC1271Policies", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "address[]", name: "policies", type: "address[]" } - ], - name: "disableUserOpPolicies", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [], - name: "eip712Domain", - outputs: [ - { internalType: "bytes1", name: "fields", type: "bytes1" }, - { internalType: "string", name: "name", type: "string" }, - { internalType: "string", name: "version", type: "string" }, - { internalType: "uint256", name: "chainId", type: "uint256" }, - { internalType: "address", name: "verifyingContract", type: "address" }, - { internalType: "bytes32", name: "salt", type: "bytes32" }, - { internalType: "uint256[]", name: "extensions", type: "uint256[]" } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { - components: [ - { - internalType: "bytes4", - name: "actionTargetSelector", - type: "bytes4" - }, - { internalType: "address", name: "actionTarget", type: "address" }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "actionPolicies", - type: "tuple[]" - } - ], - internalType: "struct ActionData[]", - name: "actionPolicies", - type: "tuple[]" - } - ], - name: "enableActionPolicies", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { - components: [ - { - internalType: "string[]", - name: "allowedERC7739Content", - type: "string[]" - }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "erc1271Policies", - type: "tuple[]" - } - ], - internalType: "struct ERC7739Data", - name: "erc1271Policies", - type: "tuple" - } - ], - name: "enableERC1271Policies", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - components: [ - { - internalType: "contract ISessionValidator", - name: "sessionValidator", - type: "address" - }, - { - internalType: "bytes", - name: "sessionValidatorInitData", - type: "bytes" - }, - { internalType: "bytes32", name: "salt", type: "bytes32" }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "userOpPolicies", - type: "tuple[]" - }, - { - components: [ - { - internalType: "string[]", - name: "allowedERC7739Content", - type: "string[]" - }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "erc1271Policies", - type: "tuple[]" - } - ], - internalType: "struct ERC7739Data", - name: "erc7739Policies", - type: "tuple" - }, - { - components: [ - { - internalType: "bytes4", - name: "actionTargetSelector", - type: "bytes4" - }, - { - internalType: "address", - name: "actionTarget", - type: "address" - }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "actionPolicies", - type: "tuple[]" - } - ], - internalType: "struct ActionData[]", - name: "actions", - type: "tuple[]" - } - ], - internalType: "struct Session[]", - name: "sessions", - type: "tuple[]" - } - ], - name: "enableSessions", - outputs: [ - { - internalType: "PermissionId[]", - name: "permissionIds", - type: "bytes32[]" - } - ], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "userOpPolicies", - type: "tuple[]" - } - ], - name: "enableUserOpPolicies", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "address", name: "account", type: "address" } - ], - name: "getNonce", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - components: [ - { - internalType: "contract ISessionValidator", - name: "sessionValidator", - type: "address" - }, - { - internalType: "bytes", - name: "sessionValidatorInitData", - type: "bytes" - }, - { internalType: "bytes32", name: "salt", type: "bytes32" }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "userOpPolicies", - type: "tuple[]" - }, - { - components: [ - { - internalType: "string[]", - name: "allowedERC7739Content", - type: "string[]" - }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "erc1271Policies", - type: "tuple[]" - } - ], - internalType: "struct ERC7739Data", - name: "erc7739Policies", - type: "tuple" - }, - { - components: [ - { - internalType: "bytes4", - name: "actionTargetSelector", - type: "bytes4" - }, - { - internalType: "address", - name: "actionTarget", - type: "address" - }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "actionPolicies", - type: "tuple[]" - } - ], - internalType: "struct ActionData[]", - name: "actions", - type: "tuple[]" - } - ], - internalType: "struct Session", - name: "session", - type: "tuple" - } - ], - name: "getPermissionId", - outputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" } - ], - stateMutability: "pure", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "address", name: "account", type: "address" }, - { - components: [ - { - internalType: "contract ISessionValidator", - name: "sessionValidator", - type: "address" - }, - { - internalType: "bytes", - name: "sessionValidatorInitData", - type: "bytes" - }, - { internalType: "bytes32", name: "salt", type: "bytes32" }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "userOpPolicies", - type: "tuple[]" - }, - { - components: [ - { - internalType: "string[]", - name: "allowedERC7739Content", - type: "string[]" - }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "erc1271Policies", - type: "tuple[]" - } - ], - internalType: "struct ERC7739Data", - name: "erc7739Policies", - type: "tuple" - }, - { - components: [ - { - internalType: "bytes4", - name: "actionTargetSelector", - type: "bytes4" - }, - { - internalType: "address", - name: "actionTarget", - type: "address" - }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "actionPolicies", - type: "tuple[]" - } - ], - internalType: "struct ActionData[]", - name: "actions", - type: "tuple[]" - } - ], - internalType: "struct Session", - name: "data", - type: "tuple" - }, - { internalType: "enum SmartSessionMode", name: "mode", type: "uint8" } - ], - name: "getSessionDigest", - outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { internalType: "address", name: "smartAccount", type: "address" } - ], - name: "isInitialized", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "view", - type: "function" - }, - { - inputs: [{ internalType: "uint256", name: "typeID", type: "uint256" }], - name: "isModuleType", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "pure", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "address", name: "account", type: "address" }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "userOpPolicies", - type: "tuple[]" - }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "erc1271Policies", - type: "tuple[]" - }, - { - components: [ - { - internalType: "bytes4", - name: "actionTargetSelector", - type: "bytes4" - }, - { internalType: "address", name: "actionTarget", type: "address" }, - { - components: [ - { internalType: "address", name: "policy", type: "address" }, - { internalType: "bytes", name: "initData", type: "bytes" } - ], - internalType: "struct PolicyData[]", - name: "actionPolicies", - type: "tuple[]" - } - ], - internalType: "struct ActionData[]", - name: "actions", - type: "tuple[]" - } - ], - name: "isPermissionEnabled", - outputs: [{ internalType: "bool", name: "isEnabled", type: "bool" }], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" }, - { internalType: "address", name: "account", type: "address" } - ], - name: "isSessionEnabled", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { internalType: "address", name: "sender", type: "address" }, - { internalType: "bytes32", name: "hash", type: "bytes32" }, - { internalType: "bytes", name: "signature", type: "bytes" } - ], - name: "isValidSignatureWithSender", - outputs: [{ internalType: "bytes4", name: "result", type: "bytes4" }], - stateMutability: "view", - type: "function" - }, - { - inputs: [{ internalType: "bytes", name: "data", type: "bytes" }], - name: "onInstall", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [{ internalType: "bytes", name: "", type: "bytes" }], - name: "onUninstall", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" } - ], - name: "removeSession", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { internalType: "PermissionId", name: "permissionId", type: "bytes32" } - ], - name: "revokeEnableSignature", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [], - name: "supportsNestedTypedDataSign", - outputs: [{ internalType: "bytes32", name: "result", type: "bytes32" }], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - components: [ - { internalType: "address", name: "sender", type: "address" }, - { internalType: "uint256", name: "nonce", type: "uint256" }, - { internalType: "bytes", name: "initCode", type: "bytes" }, - { internalType: "bytes", name: "callData", type: "bytes" }, - { - internalType: "bytes32", - name: "accountGasLimits", - type: "bytes32" - }, - { - internalType: "uint256", - name: "preVerificationGas", - type: "uint256" - }, - { internalType: "bytes32", name: "gasFees", type: "bytes32" }, - { internalType: "bytes", name: "paymasterAndData", type: "bytes" }, - { internalType: "bytes", name: "signature", type: "bytes" } - ], - internalType: "struct PackedUserOperation", - name: "userOp", - type: "tuple" - }, - { internalType: "bytes32", name: "userOpHash", type: "bytes32" } - ], - name: "validateUserOp", - outputs: [{ internalType: "ValidationData", name: "vd", type: "uint256" }], - stateMutability: "nonpayable", - type: "function" - } -] diff --git a/src/sdk/__contracts/abi/index.ts b/src/sdk/__contracts/abi/index.ts deleted file mode 100644 index ae9088d4d..000000000 --- a/src/sdk/__contracts/abi/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./EIP1271Abi" -export * from "./UniActionPolicyAbi" -export * from "./EntryPointABI" -export * from "./NexusAbi" -export * from "./K1ValidatorAbi" -export * from "./K1ValidatorFactoryAbi" diff --git a/src/sdk/__contracts/addresses.ts b/src/sdk/__contracts/addresses.ts deleted file mode 100644 index c5edfe97f..000000000 --- a/src/sdk/__contracts/addresses.ts +++ /dev/null @@ -1,12 +0,0 @@ -// The contents of this folder is auto-generated. Please do not edit as your changes are likely to be overwritten - -export const addresses = { - Nexus: "0x3346Dfd37306E29CEbA92Cf865B413C3F5C25D85", - K1Validator: "0x9091D0F9A54985237954046cf230fA8fb054BA8E", - K1ValidatorFactory: "0xcc5D5a6Ac5661DB1f0b91B4e555D7D0135C7c489", - UniActionPolicy: "0x28120dC008C36d95DE5fa0603526f219c1Ba80f6", - TimeframePolicy: "0x0B7BB9bD65858593D97f12001FaDa94828307805", - SmartSession: "0x3834aD7f5f73fAd19C089a924F18e6F3417d1ac2", - SimpleSessionValidator: "0xAAAdFd794A1781e4Fd3eA64985F107a7Ac2b3872" // K1 algorithm - single session key -} as const -export default addresses diff --git a/src/sdk/__contracts/index.ts b/src/sdk/__contracts/index.ts deleted file mode 100644 index 38595002c..000000000 --- a/src/sdk/__contracts/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -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" - -const entryPoint = { - address: entryPoint07Address, - abi: EntrypointAbi -} as const - -const entryPointSimulations = { - address: ENTRYPOINT_SIMULATIONS -} as const - -const k1ValidatorFactory = { - address: addresses.K1ValidatorFactory, - abi: K1ValidatorFactoryAbi -} as const - -const k1Validator = { - address: addresses.K1Validator, - abi: K1ValidatorAbi -} as const - -export const contracts = { - entryPoint, - entryPointSimulations, - k1ValidatorFactory, - k1Validator -} as const - -export default contracts diff --git a/src/sdk/account/index.ts b/src/sdk/account/index.ts index 57756f3eb..db3dc1554 100644 --- a/src/sdk/account/index.ts +++ b/src/sdk/account/index.ts @@ -1,2 +1,2 @@ -export * from "./utils/index.js" +export * from "./utils" export * from "./toNexusAccount.js" diff --git a/src/sdk/account/toNexusAccount.test.ts b/src/sdk/account/toNexusAccount.test.ts index 0f5a61296..8c1e0a6ba 100644 --- a/src/sdk/account/toNexusAccount.test.ts +++ b/src/sdk/account/toNexusAccount.test.ts @@ -1,10 +1,10 @@ import { getAddress, getBytes, hexlify } from "ethers" import { http, - type Account, type Address, type Chain, type Hex, + type LocalAccount, type PublicClient, type WalletClient, concat, @@ -13,6 +13,7 @@ import { domainSeparator, encodeAbiParameters, encodePacked, + getContract, hashMessage, isAddress, isHex, @@ -23,9 +24,11 @@ import { toBytes, toHex } from "viem" +import type { UserOperation } from "viem/account-abstraction" import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { MockSignatureValidatorAbi } from "../../test/__contracts/abi/MockSignatureValidatorAbi" import { TokenWithPermitAbi } from "../../test/__contracts/abi/TokenWithPermitAbi" -import { mockAddresses } from "../../test/__contracts/mockAddresses" +import { testAddresses } from "../../test/callDatas" import { toNetwork } from "../../test/testSetup" import { fundAndDeployClients, @@ -34,12 +37,11 @@ import { 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 { k1ValidatorAddress } from "../constants" import type { NexusAccount } from "./toNexusAccount" import { addressEquals, @@ -51,7 +53,7 @@ import { PARENT_TYPEHASH, eip1271MagicValue } from "./utils/Constants" -import type { BytesLike, UserOperationStruct } from "./utils/Types" +import type { BytesLike } from "./utils/Types" describe("nexus.account", async () => { let network: NetworkConfig @@ -60,7 +62,8 @@ describe("nexus.account", async () => { // Test utils let testClient: MasterClient - let eoaAccount: Account + let eoaAccount: LocalAccount + let userTwo: LocalAccount let nexusAccountAddress: Address let nexusClient: NexusClient let nexusAccount: NexusAccount @@ -72,6 +75,7 @@ describe("nexus.account", async () => { chain = network.chain bundlerUrl = network.bundlerUrl eoaAccount = getTestAccount(0) + userTwo = getTestAccount(1) testClient = toTestClient(chain, getTestAccount(5)) walletClient = createWalletClient({ @@ -97,11 +101,11 @@ describe("nexus.account", async () => { test("should override account address", async () => { const newNexusClient = await createNexusClient({ - signer: eoaAccount, chain, transport: http(), bundlerTransport: http(bundlerUrl), - accountAddress: "0xf0479e036343bC66dc49dd374aFAF98402D0Ae5f" + accountAddress: "0xf0479e036343bC66dc49dd374aFAF98402D0Ae5f", + signer: eoaAccount }) const accountAddress = await newNexusClient.account.getAddress() expect(accountAddress).toBe("0xf0479e036343bC66dc49dd374aFAF98402D0Ae5f") @@ -145,7 +149,9 @@ describe("nexus.account", async () => { const contractResponse = await testClient.readContract({ address: nexusAccountAddress, - abi: NexusAbi, + abi: parseAbi([ + "function isValidSignature(bytes32,bytes) external view returns (bytes4)" + ]), functionName: "isValidSignature", args: [hashMessage(data), signature] }) @@ -160,6 +166,66 @@ describe("nexus.account", async () => { expect(viemResponse).toBe(true) }) + test("should verify signatures", async () => { + const mockSigVerifierContract = getContract({ + address: testAddresses.MockSignatureValidator, + abi: MockSignatureValidatorAbi, + client: testClient + }) + + const message = "Hello World" + const messageHash = keccak256(toBytes(message)) + + // Sign with regular hash + const signature = await eoaAccount.signMessage({ + message: { raw: messageHash } + }) + + // Sign with Ethereum signed message + const ethSignature = await eoaAccount.signMessage({ + message + }) + + const isValidRegular = await mockSigVerifierContract.read.verify([ + messageHash, + signature, + eoaAccount.address + ]) + + // Verify Ethereum signed message + const ethMessageHash = hashMessage(message) + const isValidEthSigned = await mockSigVerifierContract.read.verify([ + ethMessageHash, + ethSignature, + eoaAccount.address + ]) + + expect(isValidRegular).toBe(true) + expect(isValidEthSigned).toBe(true) + }) + + test.skip("should verify signatures from prepared UserOperation", async () => { + const mockSigVerifierContract = getContract({ + address: testAddresses.MockSignatureValidator, + abi: MockSignatureValidatorAbi, + client: testClient + }) + + const userOperation = await nexusClient.prepareUserOperation({ + calls: [{ to: userTwo.address, value: 1n }] + }) + + const userOpHash = await nexusClient.account.getUserOpHash(userOperation) + + const isValid = await mockSigVerifierContract.read.verify([ + userOpHash, + userOperation.signature, + eoaAccount.address + ]) + + expect(isValid).toBe(true) + }) + test("should have 4337 account actions", async () => { const [ isDeployed, @@ -188,7 +254,7 @@ describe("nexus.account", async () => { callGasLimit: 1n, maxFeePerGas: 1n, maxPriorityFeePerGas: 1n - } as UserOperationStruct), + } as UserOperation), nexusAccount.getAddress(), nexusAccount.getFactoryArgs(), nexusAccount.getStubSignature(), @@ -284,12 +350,14 @@ describe("nexus.account", async () => { const finalSignature = encodePacked( ["address", "bytes"], - [addresses.K1Validator, signatureData] + [k1ValidatorAddress, signatureData] ) const contractResponse = await testClient.readContract({ address: nexusAccountAddress, - abi: NexusAbi, + abi: parseAbi([ + "function isValidSignature(bytes32,bytes) external view returns (bytes4)" + ]), functionName: "isValidSignature", args: [typedHashHashed, finalSignature] }) @@ -301,7 +369,7 @@ describe("nexus.account", async () => { const appDomain = { chainId: chain.id, name: "TokenWithPermit", - verifyingContract: mockAddresses.TokenWithPermit, + verifyingContract: testAddresses.TokenWithPermit, version: "1" } @@ -321,7 +389,7 @@ describe("nexus.account", async () => { ) ) const nonce = (await testClient.readContract({ - address: mockAddresses.TokenWithPermit, + address: testAddresses.TokenWithPermit, abi: TokenWithPermitAbi, functionName: "nonces", args: [nexusAccountAddress] @@ -364,13 +432,15 @@ describe("nexus.account", async () => { const nexusResponse = await testClient.readContract({ address: nexusAccountAddress, - abi: NexusAbi, + abi: parseAbi([ + "function isValidSignature(bytes32,bytes) external view returns (bytes4)" + ]), functionName: "isValidSignature", args: [contentsHash, finalSignature] }) const permitTokenResponse = await nexusClient.writeContract({ - address: mockAddresses.TokenWithPermit, + address: testAddresses.TokenWithPermit, abi: TokenWithPermitAbi, functionName: "permitWith1271", chain: network.chain, @@ -386,7 +456,7 @@ describe("nexus.account", async () => { await nexusClient.waitForTransactionReceipt({ hash: permitTokenResponse }) const allowance = await testClient.readContract({ - address: mockAddresses.TokenWithPermit, + address: testAddresses.TokenWithPermit, abi: TokenWithPermitAbi, functionName: "allowance", args: [nexusAccountAddress, nexusAccountAddress] diff --git a/src/sdk/account/toNexusAccount.ts b/src/sdk/account/toNexusAccount.ts index 1a722ef70..ee6551a8b 100644 --- a/src/sdk/account/toNexusAccount.ts +++ b/src/sdk/account/toNexusAccount.ts @@ -1,3 +1,4 @@ +// viem import { type AbiParameter, type Account, @@ -13,8 +14,10 @@ import { type TypedData, type TypedDataDefinition, type UnionPartialBy, + type WalletClient, concat, concatHex, + createPublicClient, createWalletClient, domainSeparator, encodeAbiParameters, @@ -28,7 +31,7 @@ import { toBytes, toHex, validateTypedData, - walletActions + zeroAddress } from "viem" import { type SmartAccount, @@ -38,27 +41,34 @@ import { getUserOperationHash, toSmartAccount } from "viem/account-abstraction" -import contracts from "../__contracts" -import { EntrypointAbi, K1ValidatorFactoryAbi } from "../__contracts/abi" -import type { Call, UserOperationStruct } from "./utils/Types" import { - ERROR_MESSAGES, + ENTRY_POINT_ADDRESS, + k1ValidatorAddress as k1ValidatorAddress_, + k1ValidatorFactoryAddress +} from "../constants" +// Constants +import { EntrypointAbi } from "../constants/abi" + +// Modules +import { toK1Validator } from "../modules/k1Validator/toK1Validator" +import type { Module } from "../modules/utils/Types" + +import { EXECUTE_BATCH, EXECUTE_SINGLE, MAGIC_BYTES, PARENT_TYPEHASH } from "./utils/Constants" - -import { toK1 } from "../modules/k1/toK1" -import type { Module } from "../modules/utils/Types" +// Utils +import type { Call } from "./utils/Types" import { type TypedDataWith712, + addressEquals, eip712WrapHash, getAccountDomainStructFields, getTypesForEIP712Domain, isNullOrUndefined, - packUserOp, typeToString } from "./utils/Utils" import { type Signer, type UnknownSigner, toSigner } from "./utils/toSigner" @@ -115,12 +125,14 @@ export type NexusSmartAccountImplementation = SmartAccountImplementation< getInitCode: () => Hex encodeExecute: (call: Call) => Promise encodeExecuteBatch: (calls: readonly Call[]) => Promise - getUserOpHash: (userOp: Partial) => Promise + getUserOpHash: (userOp: UserOperation) => Hex setModule: (validationModule: Module) => void getModule: () => Module factoryData: Hex factoryAddress: Address signer: Signer + publicClient: PublicClient + walletClient: WalletClient } > @@ -150,80 +162,56 @@ export const toNexusAccount = async ( signer: _signer, index = 0n, module: module_, - factoryAddress = contracts.k1ValidatorFactory.address, - k1ValidatorAddress = contracts.k1Validator.address, + factoryAddress = k1ValidatorFactoryAddress, + k1ValidatorAddress = k1ValidatorAddress_, key = "nexus account", name = "Nexus Account" } = parameters + // @ts-ignore const signer = await toSigner({ signer: _signer }) - const masterClient = createWalletClient({ + const walletClient = createWalletClient({ account: signer, chain, transport, key, name + }).extend(publicActions) + + const publicClient = createPublicClient({ + chain, + transport }) - .extend(walletActions) - .extend(publicActions) - const signerAddress = masterClient.account.address + const signerAddress = walletClient.account.address + const entryPointContract = getContract({ - address: contracts.entryPoint.address, + address: ENTRY_POINT_ADDRESS, abi: EntrypointAbi, client: { - public: masterClient, - wallet: masterClient + public: publicClient, + wallet: walletClient } }) + // Review: + // Todo: attesters can be added here to do one time setup upon deployment. const factoryData = encodeFunctionData({ - abi: K1ValidatorFactoryAbi, + abi: parseAbi([ + "function createAccount(address eoaOwner, uint256 index, address[] attesters, uint8 threshold) external returns (address)" + ]), functionName: "createAccount", args: [signerAddress, index, [], 0] }) let _accountAddress: Address | undefined = parameters.accountAddress + /** - * @description Gets the address of the account - * @returns The address of the account + * @description Gets the init code for the account + * @returns The init code as a hexadecimal string */ - const getAddress = async (): Promise
=> { - if (!isNullOrUndefined(_accountAddress)) return _accountAddress - - try { - _accountAddress = (await masterClient.readContract({ - address: factoryAddress, - abi: K1ValidatorFactoryAbi, - functionName: "computeAccountAddress", - args: [signerAddress, index, [], 0] - })) as Address - // biome-ignore lint/suspicious/noExplicitAny: - } catch (e: any) { - if (e.shortMessage?.includes(ERROR_MESSAGES.MISSING_ACCOUNT_CONTRACT)) { - throw new Error( - "Failed to compute account address. Possible reasons:\n" + - "- The factory contract does not have the function 'computeAccountAddress'\n" + - "- The parameters passed to the factory contract function may be invalid\n" + - "- The provided factory address is not a contract" - ) - } - throw e - } - - return _accountAddress - } - - let module = - module_ ?? - toK1({ - address: k1ValidatorAddress, - accountAddress: await getAddress(), - initData: signerAddress, - deInitData: "0x", - signer - }) + const getInitCode = () => concatHex([factoryAddress, factoryData]) /** * @description Gets the counterfactual address of the account @@ -231,24 +219,30 @@ export const toNexusAccount = async ( * @throws {Error} If unable to get the counterfactual address */ const getCounterFactualAddress = async (): Promise
=> { - if (_accountAddress) return _accountAddress + if (!isNullOrUndefined(_accountAddress)) return _accountAddress try { await entryPointContract.simulate.getSenderAddress([getInitCode()]) // biome-ignore lint/suspicious/noExplicitAny: } catch (e: any) { if (e?.cause?.data?.errorName === "SenderAddressResult") { _accountAddress = e?.cause.data.args[0] as Address - return _accountAddress + if (!addressEquals(_accountAddress, zeroAddress)) { + return _accountAddress + } } } 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]) + let module = + module_ ?? + toK1Validator({ + address: k1ValidatorAddress, + accountAddress: await getCounterFactualAddress(), + initData: signerAddress, + deInitData: "0x", + signer + }) /** * @description Checks if the account is deployed @@ -256,7 +250,7 @@ export const toNexusAccount = async ( */ const isDeployed = async (): Promise => { const address = await getCounterFactualAddress() - const contractCode = await masterClient.getCode({ address }) + const contractCode = await publicClient.getCode({ address }) return (contractCode?.length ?? 0) > 2 } @@ -265,16 +259,13 @@ export const toNexusAccount = async ( * @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) + const getUserOpHash = (userOp: UserOperation): Hex => { + return getUserOperationHash({ + chainId: chain.id, + entryPointAddress: entryPoint07Address, + entryPointVersion: "0.7", + userOperation: userOp + }) } /** @@ -355,10 +346,10 @@ export const toNexusAccount = async ( const key: string = concat([ toHex(defaultedKey, { size: 3 }), defaultedValidationMode, - module.address + module.address as Hex ]) - const accountAddress = await getAddress() + const accountAddress = await getCounterFactualAddress() return await entryPointContract.read.getNonce([ accountAddress, BigInt(key) @@ -389,7 +380,7 @@ export const toNexusAccount = async ( const signature = encodePacked( ["address", "bytes"], - [module.address, tempSignature] + [module.address as Hex, tempSignature] ) const erc6492Signature = concat([ @@ -449,8 +440,8 @@ export const toNexusAccount = async ( const appDomainSeparator = domainSeparator({ domain }) const accountDomainStructFields = await getAccountDomainStructFields( - masterClient as unknown as PublicClient, - await getAddress() + publicClient, + await getCounterFactualAddress() ) const parentStructHash = keccak256( @@ -485,20 +476,20 @@ export const toNexusAccount = async ( signature = encodePacked( ["address", "bytes"], - [module.address, signatureData] + [module.address as Hex, signatureData] ) return signature } return toSmartAccount({ - client: masterClient, + client: walletClient, entryPoint: { abi: EntrypointAbi, - address: contracts.entryPoint.address, + address: ENTRY_POINT_ADDRESS, version: "0.7" }, - getAddress, + getAddress: getCounterFactualAddress, encodeCalls: (calls: readonly Call[]): Promise => { return calls.length === 1 ? encodeExecute(calls[0]) @@ -513,7 +504,7 @@ export const toNexusAccount = async ( chainId?: number | undefined } ): Promise => { - const { chainId = masterClient.chain.id, ...userOpWithoutSender } = + const { chainId = publicClient.chain.id, ...userOpWithoutSender } = parameters const address = await getCounterFactualAddress() @@ -543,7 +534,9 @@ export const toNexusAccount = async ( getModule: () => module, factoryData, factoryAddress, - signer + signer, + walletClient, + publicClient } }) } diff --git a/src/sdk/account/utils/Constants.ts b/src/sdk/account/utils/Constants.ts index a281cf177..73e3e5a15 100644 --- a/src/sdk/account/utils/Constants.ts +++ b/src/sdk/account/utils/Constants.ts @@ -21,6 +21,13 @@ export const DefaultGasLimit = { } export const ERROR_MESSAGES = { + KEY_GEN_DATA_NOT_FOUND: "Key generation data is not available", + SIGNATURE_NOT_FOUND: "Signature not found from Dan", + FAILED_COMPUTE_ACCOUNT_ADDRESS: + "Failed to compute account address. Possible reasons:\n" + + "- The factory contract does not have the function 'computeAccountAddress'\n" + + "- The parameters passed to the factory contract function may be invalid\n" + + "- The provided factory address is not a contract", SIGNER_REQUIRED_FOR_CREATE_SESSION: "Signer is required", ACCOUNT_REQUIRED: "Account is required", MODULE_NOT_ACTIVATED: "Module not activated", diff --git a/src/sdk/account/utils/Types.ts b/src/sdk/account/utils/Types.ts index bb759b0bd..6c723a534 100644 --- a/src/sdk/account/utils/Types.ts +++ b/src/sdk/account/utils/Types.ts @@ -67,30 +67,6 @@ 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, diff --git a/src/sdk/account/utils/Utils.ts b/src/sdk/account/utils/Utils.ts index fbcb24ddb..d8e17a813 100644 --- a/src/sdk/account/utils/Utils.ts +++ b/src/sdk/account/utils/Utils.ts @@ -14,7 +14,6 @@ import { encodePacked, hexToBytes, keccak256, - pad, parseAbi, parseAbiParameters, publicActions, @@ -22,7 +21,6 @@ import { toBytes, toHex } from "viem" -import { EIP1271Abi } from "../../__contracts/abi" import { MOCK_MULTI_MODULE_ADDRESS, MODULE_ENABLE_MODE_TYPE_HASH, @@ -30,96 +28,63 @@ import { NEXUS_DOMAIN_TYPEHASH, NEXUS_DOMAIN_VERSION } from "../../account/utils/Constants" -import { type ModuleType, moduleTypeIds } from "../../modules/utils/Types" -import type { - AccountMetadata, - EIP712DomainReturn, - UserOperationStruct -} from "./Types" +import { EIP1271Abi } from "../../constants/abi" +import { + type AnyData, + type ModuleType, + moduleTypeIds +} from "../../modules/utils/Types" +import type { AccountMetadata, EIP712DomainReturn } from "./Types" /** - * pack the userOperation - * @param op - * @param forSignature "true" if the hash is needed to calculate the getUserOpHash() - * "false" to pack entire UserOp, for calculating the calldata cost of putting it on-chain. + * Type guard to check if a value is null or undefined. + * + * @param value - The value to check + * @returns True if the value is null or undefined */ -export function packUserOp( - userOperation: Partial -): string { - const hashedInitCode = keccak256( - userOperation.factory && userOperation.factoryData - ? concat([userOperation.factory, userOperation.factoryData]) - : "0x" - ) - const hashedCallData = keccak256(userOperation.callData ?? "0x") - const hashedPaymasterAndData = keccak256( - userOperation.paymaster - ? concat([ - userOperation.paymaster, - pad(toHex(userOperation.paymasterVerificationGasLimit || BigInt(0)), { - size: 16 - }), - pad(toHex(userOperation.paymasterPostOpGasLimit || BigInt(0)), { - size: 16 - }), - userOperation.paymasterData || "0x" - ]) - : "0x" - ) - - return encodeAbiParameters( - [ - { type: "address" }, - { type: "uint256" }, - { type: "bytes32" }, - { type: "bytes32" }, - { type: "bytes32" }, - { type: "uint256" }, - { type: "bytes32" }, - { type: "bytes32" } - ], - [ - userOperation.sender as Address, - userOperation.nonce ?? 0n, - hashedInitCode, - hashedCallData, - concat([ - pad(toHex(userOperation.verificationGasLimit ?? 0n), { - size: 16 - }), - pad(toHex(userOperation.callGasLimit ?? 0n), { size: 16 }) - ]), - userOperation.preVerificationGas ?? 0n, - concat([ - pad(toHex(userOperation.maxPriorityFeePerGas ?? 0n), { - size: 16 - }), - pad(toHex(userOperation.maxFeePerGas ?? 0n), { size: 16 }) - ]), - hashedPaymasterAndData - ] - ) -} - -// biome-ignore lint/suspicious/noExplicitAny: export const isNullOrUndefined = (value: any): value is undefined => { return value === null || value === undefined } +/** + * Validates if a string is a valid RPC URL. + * + * @param url - The URL to validate + * @returns True if the URL is a valid RPC endpoint + */ export const isValidRpcUrl = (url: string): boolean => { const regex = /^(http:\/\/|wss:\/\/|https:\/\/).*/ return regex.test(url) } +/** + * Compares two addresses for equality, case-insensitive. + * + * @param a - First address + * @param b - Second address + * @returns True if addresses are equal + */ export const addressEquals = (a?: string, b?: string): boolean => !!a && !!b && a?.toLowerCase() === b.toLowerCase() +/** + * Parameters for wrapping a signature according to EIP-6492. + */ export type SignWith6492Params = { + /** The factory contract address */ factoryAddress: Address + /** The factory initialization calldata */ factoryCalldata: Hex + /** The original signature to wrap */ signature: Hash } +/** + * Wraps a signature according to EIP-6492 specification. + * + * @param params - Parameters including factory address, calldata, and signature + * @returns The wrapped signature + */ export const wrapSignatureWith6492 = ({ factoryAddress, factoryCalldata, @@ -142,10 +107,24 @@ export const wrapSignatureWith6492 = ({ ]) } +/** + * Calculates the percentage of a partial value relative to a total value. + * + * @param partialValue - The partial value + * @param totalValue - The total value + * @returns The percentage as a number + */ export function percentage(partialValue: number, totalValue: number) { return (100 * partialValue) / totalValue } +/** + * Converts a percentage to a factor (e.g., 50% -> 1.5). + * + * @param percentage - The percentage value (1-100) + * @returns The converted factor + * @throws If percentage is outside valid range + */ export function convertToFactor(percentage: number | undefined): number { // Check if the input is within the valid range if (percentage) { @@ -161,6 +140,15 @@ export function convertToFactor(percentage: number | undefined): number { return 1 } +/** + * Generates installation data and hash for module installation. + * + * @param accountOwner - The account owner address + * @param modules - Array of modules with their types and configurations + * @param domainName - Optional domain name + * @param domainVersion - Optional domain version + * @returns Tuple of [installData, hash] + */ export function makeInstallDataAndHash( accountOwner: Address, modules: { type: ModuleType; config: Hex }[], @@ -247,6 +235,14 @@ export function getTypesForEIP712Domain({ domain?.salt && { name: "salt", type: "bytes32" } ].filter(Boolean) as TypedDataParameter[] } + +/** + * Retrieves account metadata including name, version, and chain ID. + * + * @param client - The viem Client instance + * @param accountAddress - The account address to query + * @returns Promise resolving to account metadata + */ export const getAccountMeta = async ( client: Client, accountAddress: Address @@ -288,6 +284,13 @@ export const getAccountMeta = async ( } } +/** + * Wraps a typed data hash with EIP-712 domain separator. + * + * @param typedHash - The hash to wrap + * @param appDomainSeparator - The domain separator + * @returns The wrapped hash + */ export const eip712WrapHash = (typedHash: Hex, appDomainSeparator: Hex): Hex => keccak256(concat(["0x1901", appDomainSeparator, typedHash])) @@ -305,7 +308,7 @@ export function typeToString(typeDef: TypedDataWith712): string[] { } /** @ignore */ -export function bigIntReplacer(_key: string, value: any): any { +export function bigIntReplacer(_key: string, value: AnyData) { return typeof value === "bigint" ? value.toString() : value } @@ -356,5 +359,12 @@ export const getAccountDomainStructFields = async ( export const playgroundTrue = process?.env?.RUN_PLAYGROUND === "true" export const isTesting = process?.env?.TEST === "true" +/** + * Safely multiplies a bigint by a number, rounding appropriately. + * + * @param bI - The bigint to multiply + * @param multiplier - The multiplication factor + * @returns The multiplied bigint + */ export const safeMultiplier = (bI: bigint, multiplier: number): bigint => BigInt(Math.round(Number(bI) * multiplier)) diff --git a/src/sdk/account/utils/deepHexlify.ts b/src/sdk/account/utils/deepHexlify.ts new file mode 100644 index 000000000..23f9a9bf9 --- /dev/null +++ b/src/sdk/account/utils/deepHexlify.ts @@ -0,0 +1,30 @@ +import { toHex } from "viem" +import type { AnyData } from "../../modules/utils/Types" + +export function deepHexlify(obj: AnyData): AnyData { + 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/sdk/account/utils/toHolder.ts b/src/sdk/account/utils/toHolder.ts deleted file mode 100644 index 94d2948fa..000000000 --- a/src/sdk/account/utils/toHolder.ts +++ /dev/null @@ -1,85 +0,0 @@ -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/toSigner.ts b/src/sdk/account/utils/toSigner.ts index 27c3a77e3..92c9f215a 100644 --- a/src/sdk/account/utils/toSigner.ts +++ b/src/sdk/account/utils/toSigner.ts @@ -5,33 +5,112 @@ import { type EIP1193Provider, type EIP1193RequestFn, type EIP1474Methods, + type Hex, type LocalAccount, type OneOf, type Transport, type WalletClient, createWalletClient, - custom + custom, + getAddress, + hexToBytes } from "viem" import { toAccount } from "viem/accounts" import { signTypedData } from "viem/actions" import { getAction } from "viem/utils" +import type { AnyData } from "../../modules/utils/Types" +/** + * Represents the minimum interface required for a signer implementation. + * Provides basic signing capabilities for transactions, messages, and typed data. + */ +export type MinimalSigner = { + /** Signs a transaction with the provided arguments */ + signTransaction: (...args: AnyData[]) => Promise + /** Signs a message with the provided arguments */ + signMessage: (...args: AnyData[]) => Promise + /** Signs typed data (EIP-712) with the provided arguments */ + signTypedData: (...args: AnyData[]) => Promise + /** Optional method to retrieve the signer's address */ + getAddress?: () => Promise + /** The signer's address */ + address: Address | string + /** Optional provider instance */ + provider?: AnyData + /** Allows for additional properties */ + [key: string]: AnyData +} + +/** Represents a local account that can sign transactions and messages */ export type Signer = LocalAccount + +/** + * Union type of various signer implementations that can be converted to a LocalAccount. + * Supports EIP-1193 providers, WalletClients, LocalAccounts, Accounts, and MinimalSigners. + */ export type UnknownSigner = OneOf< | EIP1193Provider | WalletClient | LocalAccount | Account + | MinimalSigner > + +/** + * Converts various signer types into a standardized LocalAccount format. + * Handles conversion from different wallet implementations including ethers.js wallets, + * EIP-1193 providers, and existing LocalAccounts. + * + * @param signer - The signer to convert, must implement required signing methods + * @param address - Optional address to use for the account + * @returns A Promise resolving to a LocalAccount + * + * @throws {Error} When signTransaction is called (not supported) + * @throws {Error} When address is required but not provided + */ export async function toSigner({ signer, address }: { - signer: UnknownSigner + signer: UnknownSigner & { + getAddress: () => Promise + signMessage: (message: AnyData) => Promise + signTypedData: ( + domain: AnyData, + types: AnyData, + value: AnyData + ) => Promise + } address?: Address }): Promise { - if ("type" in signer && signer.type === "local") { + // ethers Wallet does not have type property + if ("provider" in signer) { + return toAccount({ + address: getAddress((await signer.getAddress()) as string), + async signMessage({ message }): Promise { + if (typeof message === "string") { + return (await signer.signMessage(message)) as Hex + } + // For ethers, raw messages need to be converted to Uint8Array + if (typeof message.raw === "string") { + return (await signer.signMessage(hexToBytes(message.raw))) as Hex + } + return (await signer.signMessage(message.raw)) as Hex + }, + async signTransaction(_) { + throw new Error("Not supported") + }, + async signTypedData(typedData) { + return signer.signTypedData( + typedData.domain as AnyData, + typedData.types as AnyData, + typedData.message as AnyData + ) as Promise + } + }) + } + if ("type" in signer && ["local", "dan"].includes(signer.type)) { return signer as LocalAccount } @@ -77,7 +156,7 @@ export async function toSigner({ walletClient, signTypedData, "signTypedData" - )(typedData as any) + )(typedData as AnyData) }, async signTransaction(_) { throw new Error("Not supported") diff --git a/src/sdk/account/utils/toValidator.ts b/src/sdk/account/utils/toValidator.ts new file mode 100644 index 000000000..f85f3af11 --- /dev/null +++ b/src/sdk/account/utils/toValidator.ts @@ -0,0 +1,63 @@ +import type { Address } from "viem" +import type { AnyData } from "../../modules/utils/Types.js" + +/** + * Represents the minimum interface required for a validator implementation. + * Provides methods for validating signatures and transactions. + */ +export type MinimalValidator = { + /** The validator's address */ + address: Address | string + /** Optional provider instance */ + provider?: AnyData + /** + * Validates a signature against a message + * @param message - The message that was signed + * @param signature - The signature to validate + */ + isValidSignature?: (message: AnyData, signature: AnyData) => Promise + /** + * Validates a typed data signature (EIP-712) + * @param hash - The hash of the typed data + * @param signature - The signature to validate + */ + isValidTypedSignature?: ( + hash: AnyData, + signature: AnyData + ) => Promise + /** Allows for additional properties */ + [key: string]: AnyData +} + +/** + * Union type of various validator implementations. + * Currently only supports MinimalValidator, but can be extended for future implementations. + */ +export type UnknownValidator = MinimalValidator + +/** + * Converts various validator types into a standardized validator format. + * Currently handles MinimalValidator implementations, but can be extended for other types. + * + * @param validator - The validator to convert + * @param address - Optional address to use for the validator + * @returns A Promise resolving to a MinimalValidator + * + * @throws {Error} When address is required but not provided + */ +export async function toValidator({ + validator, + address +}: { + validator: UnknownValidator + address?: Address +}): Promise { + if (!validator.address && !address) { + throw new Error("Address is required") + } + + return { + ...validator, + address: address || validator.address + } +} diff --git a/src/sdk/account/utils/utils.test.ts b/src/sdk/account/utils/utils.test.ts index 3ae0f3882..4a7ac17a9 100644 --- a/src/sdk/account/utils/utils.test.ts +++ b/src/sdk/account/utils/utils.test.ts @@ -1,8 +1,11 @@ import { ParamType, ethers } from "ethers" import { type AbiParameter, encodeAbiParameters } from "viem" +import { generatePrivateKey } from "viem/accounts" import { describe, expect, test } from "vitest" +import { toSigner } from "./toSigner" describe("utils", async () => { + const privKey = generatePrivateKey() test.concurrent( "should have consistent behaviour between ethers.AbiCoder.defaultAbiCoder() and viem.encodeAbiParameters()", async () => { @@ -55,4 +58,43 @@ describe("utils", async () => { expect(executionCalldataPrepWithViem).toBe(expectedResult) } ) + + test.concurrent("should support ethers Wallet", async () => { + const wallet = new ethers.Wallet(privKey) + const signer = await toSigner({ signer: wallet }) + const sig = await signer.signMessage({ message: "test" }) + expect(sig).toBeDefined() + }) + + test.concurrent("should support ethers Wallet signTypedData", async () => { + const wallet = new ethers.Wallet(privKey) + const signer = await toSigner({ signer: wallet }) + const appDomain = { + chainId: 1, + name: "TokenWithPermit", + verifyingContract: + "0x1111111111111111111111111111111111111111" as `0x${string}`, + version: "1" + } + + const primaryType = "Contents" + const types = { + Contents: [ + { + name: "stuff", + type: "bytes32" + } + ] + } + const sig = await signer.signTypedData({ + domain: appDomain, + types, + primaryType, + message: { + stuff: + "0x1111111111111111111111111111111111111111111111111111111111111111" + } + }) + expect(sig).toBeDefined() + }) }) diff --git a/src/sdk/clients/createBicoBundlerClient.test.ts b/src/sdk/clients/createBicoBundlerClient.test.ts index e1cff8cae..bab8bc0e5 100644 --- a/src/sdk/clients/createBicoBundlerClient.test.ts +++ b/src/sdk/clients/createBicoBundlerClient.test.ts @@ -1,4 +1,4 @@ -import { http, type Account, type Address, type Chain, isHex } from "viem" +import { http, type Account, type Address, type Chain } from "viem" import { afterAll, beforeAll, describe, expect, test } from "vitest" import { toNetwork } from "../../test/testSetup" import { @@ -8,8 +8,8 @@ import { topUp } from "../../test/testUtils" import type { MasterClient, NetworkConfig } from "../../test/testUtils" -import contracts from "../__contracts" import { type NexusAccount, toNexusAccount } from "../account/toNexusAccount" +import { ENTRY_POINT_ADDRESS } from "../constants" import { type BicoBundlerClient, createBicoBundlerClient @@ -54,21 +54,12 @@ describe("bico.bundler", async () => { bicoBundler.getChainId(), bicoBundler.getSupportedEntryPoints(), bicoBundler.prepareUserOperation({ - sender: eoaAccount.address, - nonce: 0n, - data: "0x", - signature: "0x", - verificationGasLimit: 1n, - preVerificationGas: 1n, - callData: "0x", - callGasLimit: 1n, - maxFeePerGas: 1n, - maxPriorityFeePerGas: 1n, - account: nexusAccount + account: nexusAccount, + calls: [{ to: eoaAccount.address, data: "0x" }] }) ]) expect(chainId).toEqual(chain.id) - expect(supportedEntrypoints).to.include(contracts.entryPoint.address) + expect(supportedEntrypoints).to.include(ENTRY_POINT_ADDRESS) expect(preparedUserOp).toHaveProperty("signature") }) diff --git a/src/sdk/clients/createBicoPaymasterClient.test.ts b/src/sdk/clients/createBicoPaymasterClient.test.ts index 5eaa37de8..b4d30bd10 100644 --- a/src/sdk/clients/createBicoPaymasterClient.test.ts +++ b/src/sdk/clients/createBicoPaymasterClient.test.ts @@ -10,10 +10,9 @@ import { } from "viem" import { afterAll, beforeAll, describe, expect, test } from "vitest" import { paymasterTruthy, toNetwork } from "../../test/testSetup" -import { killNetwork } from "../../test/testUtils" -import type { NetworkConfig } from "../../test/testUtils" +import { getTestParamsForTestnet, killNetwork } from "../../test/testUtils" +import type { NetworkConfig, TestnetParams } from "../../test/testUtils" import { type NexusAccount, toNexusAccount } from "../account/toNexusAccount" -import { safeMultiplier } from "../account/utils" import { type BicoBundlerClient, createBicoBundlerClient @@ -24,14 +23,11 @@ import { } from "./createBicoPaymasterClient" import { type NexusClient, createNexusClient } from "./createNexusClient" -// 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" - describe.runIf(paymasterTruthy)("bico.paymaster", async () => { let network: NetworkConfig - // Nexus Config + // Required for "PUBLIC_TESTNET" networks + let testParams: TestnetParams + let chain: Chain let bundlerUrl: string let paymasterUrl: undefined | string @@ -68,6 +64,8 @@ describe.runIf(paymasterTruthy)("bico.paymaster", async () => { transport: http() }) + testParams = getTestParamsForTestnet(publicClient) + paymaster = createBicoPaymasterClient({ transport: http(paymasterUrl) }) @@ -76,8 +74,7 @@ describe.runIf(paymasterTruthy)("bico.paymaster", async () => { signer: account, chain, transport: http(), - k1ValidatorAddress, - factoryAddress + ...testParams }) bicoBundler = createBicoBundlerClient({ @@ -92,22 +89,8 @@ describe.runIf(paymasterTruthy)("bico.paymaster", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress, - factoryAddress, paymaster, - // For "PUBLIC_TESTNET" network, the userOperation we can hardcode estimates - userOperation: { - estimateFeesPerGas: async (_) => { - const feeData = await publicClient.estimateFeesPerGas() - return { - maxFeePerGas: safeMultiplier(feeData.maxFeePerGas, 1.25), - maxPriorityFeePerGas: safeMultiplier( - feeData.maxPriorityFeePerGas, - 1.25 - ) - } - } - } + ...testParams }) }) afterAll(async () => { @@ -122,7 +105,7 @@ describe.runIf(paymasterTruthy)("bico.paymaster", async () => { expect(paymaster).not.toHaveProperty("getPaymasterStubData") }) - test.skip("should send a sponsored transaction", async () => { + test("should send a sponsored transaction", async () => { // Get initial balance const initialBalance = await publicClient.getBalance({ address: nexusAccountAddress @@ -145,6 +128,7 @@ describe.runIf(paymasterTruthy)("bico.paymaster", async () => { const finalBalance = await publicClient.getBalance({ address: nexusAccountAddress }) + // Check that the balance hasn't changed // No gas fees were paid, so the balance should have decreased only by 1n expect(finalBalance).toBe(initialBalance - 1n) diff --git a/src/sdk/clients/createNexusClient.test.ts b/src/sdk/clients/createNexusClient.test.ts index 7ba940bc4..a91d51443 100644 --- a/src/sdk/clients/createNexusClient.test.ts +++ b/src/sdk/clients/createNexusClient.test.ts @@ -1,13 +1,16 @@ +import { Wallet, ethers } from "ethers" import { http, type Account, type Address, type Chain, + type Hex, encodeFunctionData, isHex, - parseEther, - toBytes + parseEther } from "viem" +import type { UserOperationReceipt } from "viem/account-abstraction" +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { afterAll, beforeAll, describe, expect, test } from "vitest" import { CounterAbi } from "../../test/__contracts/abi" import mockAddresses from "../../test/__contracts/mockAddresses" @@ -20,10 +23,12 @@ import { topUp } from "../../test/testUtils" import type { MasterClient, NetworkConfig } from "../../test/testUtils" -import { addresses } from "../__contracts/addresses" import { ERROR_MESSAGES } from "../account/utils/Constants" +import { Logger } from "../account/utils/Logger" import { getAccountMeta, makeInstallDataAndHash } from "../account/utils/Utils" import { getChain } from "../account/utils/getChain" +import type { UnknownSigner } from "../account/utils/toSigner" +import { k1ValidatorAddress } from "../constants" import { type NexusClient, createNexusClient } from "./createNexusClient" describe("nexus.client", async () => { @@ -38,6 +43,7 @@ describe("nexus.client", async () => { let recipientAddress: Address let nexusClient: NexusClient let nexusAccountAddress: Address + let privKey: Hex beforeAll(async () => { network = await toNetwork() @@ -50,13 +56,15 @@ describe("nexus.client", async () => { testClient = toTestClient(chain, getTestAccount(5)) + privKey = generatePrivateKey() + const account = privateKeyToAccount(privKey) + nexusClient = await createNexusClient({ - signer: eoaAccount, + signer: account, chain, transport: http(), bundlerTransport: http(bundlerUrl) }) - nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() }) afterAll(async () => { @@ -220,7 +228,7 @@ describe("nexus.client", async () => { nexusClient.isModuleInstalled({ module: { type: "validator", - module: addresses.K1Validator, + address: k1ValidatorAddress, initData: "0x" } }), @@ -246,4 +254,97 @@ describe("nexus.client", async () => { expect(status).toBe("success") expect(balanceAfter - balanceBefore).toBe(2n) }) + + test("should compare signatures of viem and ethers signer", async () => { + const viemSigner = privateKeyToAccount(privKey) + + const wallet = new Wallet(privKey) + + const viemNexusClient = await createNexusClient({ + signer: viemSigner, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + const ethersNexusClient = await createNexusClient({ + signer: wallet, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + const sig1 = await viemNexusClient.signMessage({ message: "123" }) + const sig2 = await ethersNexusClient.signMessage({ message: "123" }) + + expect(sig1).toBe(sig2) + }) + + test("should send user operation using ethers Wallet", async () => { + const ethersSigner = new ethers.Wallet(privKey) + const ethersNexusClient = await createNexusClient({ + signer: ethersSigner as unknown as UnknownSigner, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + const hash = await ethersNexusClient.sendUserOperation({ + calls: [ + { + to: recipientAddress, + data: "0x" + } + ] + }) + const receipt = await ethersNexusClient.waitForUserOperationReceipt({ + hash + }) + expect(receipt.success).toBe(true) + }) + + test("should send sequential user ops", async () => { + const start = performance.now() + const receipts: UserOperationReceipt[] = [] + for (let i = 0; i < 3; i++) { + const hash = await nexusClient.sendUserOperation({ + calls: [ + { + to: recipientAddress, + value: 1n + } + ] + }) + const receipt = await nexusClient.waitForUserOperationReceipt({ hash }) + receipts.push(receipt) + } + expect(receipts.every((receipt) => receipt.success)).toBeTruthy() + const end = performance.now() + Logger.log(`Time taken: ${end - start} milliseconds`) + }) + + test("should send parallel user ops", async () => { + const start = performance.now() + const userOpPromises: Promise<`0x${string}`>[] = [] + for (let i = 0; i < 3; i++) { + userOpPromises.push( + nexusClient.sendUserOperation({ + calls: [ + { + to: recipientAddress, + value: 1n + } + ] + }) + ) + } + const hashes = await Promise.all(userOpPromises) + expect(hashes.length).toBe(3) + const receipts = await Promise.all( + hashes.map((hash) => nexusClient.waitForUserOperationReceipt({ hash })) + ) + expect(receipts.every((receipt) => receipt.success)).toBeTruthy() + const end = performance.now() + Logger.log(`Time taken: ${end - start} milliseconds`) + }) }) diff --git a/src/sdk/clients/createNexusClient.ts b/src/sdk/clients/createNexusClient.ts index 894a51c97..86de43d7a 100644 --- a/src/sdk/clients/createNexusClient.ts +++ b/src/sdk/clients/createNexusClient.ts @@ -16,11 +16,14 @@ import type { SmartAccount, UserOperationRequest } from "viem/account-abstraction" -import contracts from "../__contracts" import { type NexusAccount, toNexusAccount } from "../account/toNexusAccount" import type { UnknownSigner } from "../account/utils/toSigner" -import type { Module } from "../modules/utils/Types" +import { + k1ValidatorAddress as k1ValidatorAddress_, + k1ValidatorFactoryAddress +} from "../constants" +import type { AnyData, Module } from "../modules/utils/Types" import { createBicoBundlerClient } from "./createBicoBundlerClient" import { type Erc7579Actions, erc7579Actions } from "./decorators/erc7579" import { @@ -42,7 +45,7 @@ export type NexusClient< transport, chain extends Chain ? chain - : client extends Client + : client extends Client ? chain : undefined, account, @@ -87,19 +90,12 @@ export type NexusClient< 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" + ClientConfig, + "cacheTime" | "chain" | "key" | "name" | "pollingInterval" | "rpcSchema" > & { /** RPC URL. */ transport: transport @@ -127,7 +123,7 @@ export type NexusClientConfig< /** Prepares fee properties for the User Operation request. */ estimateFeesPerGas?: | ((parameters: { - account: account | SmartAccount + account: SmartAccount | undefined bundlerClient: Client userOperation: UserOperationRequest }) => Promise>) @@ -144,6 +140,7 @@ export type NexusClientConfig< factoryAddress?: Address /** Owner module */ k1ValidatorAddress?: Address + /** Account address */ accountAddress?: Address } > @@ -177,8 +174,8 @@ export async function createNexusClient( key = "nexus client", name = "Nexus Client", module, - factoryAddress = contracts.k1ValidatorFactory.address, - k1ValidatorAddress = contracts.k1Validator.address, + factoryAddress = k1ValidatorFactoryAddress, + k1ValidatorAddress = k1ValidatorAddress_, bundlerTransport, transport, accountAddress, diff --git a/src/sdk/clients/createNexusSessionClient.test.ts b/src/sdk/clients/createNexusSessionClient.test.ts index 8fedc4d12..3cacf61d7 100644 --- a/src/sdk/clients/createNexusSessionClient.test.ts +++ b/src/sdk/clients/createNexusSessionClient.test.ts @@ -1,17 +1,9 @@ -import { - http, - type Account, - type Address, - type Chain, - type Hex, - toBytes, - toHex -} from "viem" +import { http, type Address, type Chain, type Hex, toBytes, toHex } from "viem" import type { LocalAccount, PublicClient } from "viem" import { encodeFunctionData } from "viem" import { afterAll, beforeAll, describe, expect, test } from "vitest" import { CounterAbi } from "../../test/__contracts/abi" -import { TEST_CONTRACTS } from "../../test/callDatas" +import { testAddresses } from "../../test/callDatas" import { toNetwork } from "../../test/testSetup" import { fundAndDeployClients, @@ -20,14 +12,14 @@ import { toTestClient } from "../../test/testUtils" import type { MasterClient, NetworkConfig } from "../../test/testUtils" -import addresses from "../__contracts/addresses" -import { isSessionEnabled } from "../modules/smartSessions/Helpers" -import type { CreateSessionDataParams } from "../modules/smartSessions/Types" +import { SMART_SESSIONS_ADDRESS } from "../constants" +import { isPermissionEnabled } from "../modules/smartSessionsValidator/Helpers" +import type { CreateSessionDataParams } from "../modules/smartSessionsValidator/Types" import { smartSessionCreateActions, smartSessionUseActions -} from "../modules/smartSessions/decorators" -import { toSmartSessions } from "../modules/smartSessions/toSmartSessions" +} from "../modules/smartSessionsValidator/decorators" +import { toSmartSessionsValidator } from "../modules/smartSessionsValidator/toSmartSessionsValidator" import type { Module } from "../modules/utils/Types" import { type NexusClient, createNexusClient } from "./createNexusClient" import { createNexusSessionClient } from "./createNexusSessionClient" @@ -49,7 +41,7 @@ describe("nexus.session.client", async () => { let sessionsModule: Module beforeAll(async () => { - network = await toNetwork() + network = await toNetwork("BASE_SEPOLIA_FORKED") chain = network.chain bundlerUrl = network.bundlerUrl @@ -67,7 +59,7 @@ describe("nexus.session.client", async () => { }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() - sessionsModule = toSmartSessions({ + sessionsModule = toSmartSessionsValidator({ account: nexusClient.account, signer: eoaAccount }) @@ -96,7 +88,7 @@ describe("nexus.session.client", async () => { const isInstalledAfter = await nexusClient.isModuleInstalled({ module: { type: "validator", - module: addresses.SmartSession + address: SMART_SESSIONS_ADDRESS } }) expect(isInstalledAfter).toBe(true) @@ -109,32 +101,35 @@ describe("nexus.session.client", async () => { expect(isInstalledBefore).toBe(true) + const nexusSessionClient = nexusClient.extend( + smartSessionCreateActions(sessionsModule) + ) + + const trustAttestersHash = await nexusSessionClient.trustAttesters() + const userOpReceipt = await nexusSessionClient.waitForUserOperationReceipt({ + hash: trustAttestersHash + }) + const { status } = await testClient.waitForTransactionReceipt({ + hash: userOpReceipt.receipt.transactionHash + }) + expect(status).toBe("success") + // session key signer address is declared here const sessionRequestedInfo: CreateSessionDataParams[] = [ { sessionPublicKey, // session key signer - sessionValidatorAddress: TEST_CONTRACTS.SimpleSessionValidator.address, - sessionKeyData: toHex(toBytes(sessionPublicKey)), - sessionValidAfter: 0, - sessionValidUntil: 0, actionPoliciesInfo: [ { - contractAddress: TEST_CONTRACTS.Counter.address, // counter address - functionSelector: "0x273ea3e3" as Hex, // function selector for increment count - validUntil: 0, - validAfter: 0, - rules: [], // no other rules and conditions applied - valueLimit: BigInt(0) + contractAddress: testAddresses.Counter, // counter address + functionSelector: "0x273ea3e3" as Hex // function selector for increment count } ] } ] - const nexusSessionClient = nexusClient.extend( - smartSessionCreateActions(sessionsModule) - ) + nexusClient.account.getCounterFactualAddress() - const createSessionsResponse = await nexusSessionClient.createSessions({ + const createSessionsResponse = await nexusSessionClient.grantPermission({ sessionRequestedInfo }) @@ -148,7 +143,7 @@ describe("nexus.session.client", async () => { expect(receipt.success).toBe(true) - const isEnabled = await isSessionEnabled({ + const isEnabled = await isPermissionEnabled({ client: nexusClient.account.client as PublicClient, accountAddress: nexusClient.account.address, permissionId: cachedPermissionId @@ -158,7 +153,7 @@ describe("nexus.session.client", async () => { test("session signer should use session to increment a counter for a user (USE MODE)", async () => { const counterBefore = await testClient.readContract({ - address: TEST_CONTRACTS.Counter.address, + address: testAddresses.Counter, abi: CounterAbi, functionName: "getNumber" }) @@ -171,24 +166,23 @@ describe("nexus.session.client", async () => { bundlerTransport: http(bundlerUrl) }) - const useSessionsModule = toSmartSessions({ + const usePermissionsModule = toSmartSessionsValidator({ account: smartSessionNexusClient.account, signer: sessionKeyAccount, moduleData: { - permissionId: cachedPermissionId + permissionIds: [cachedPermissionId] } }) const useSmartSessionNexusClient = smartSessionNexusClient.extend( - smartSessionUseActions(useSessionsModule) + smartSessionUseActions(usePermissionsModule) ) - const userOpHash = await useSmartSessionNexusClient.useSession({ - actions: [ + const userOpHash = await useSmartSessionNexusClient.usePermission({ + calls: [ { - target: TEST_CONTRACTS.Counter.address, - value: 0n, - callData: encodeFunctionData({ + to: testAddresses.Counter, + data: encodeFunctionData({ abi: CounterAbi, functionName: "incrementNumber", args: [] @@ -205,7 +199,7 @@ describe("nexus.session.client", async () => { expect(receipt.success).toBe(true) const counterAfter = await testClient.readContract({ - address: TEST_CONTRACTS.Counter.address, + address: testAddresses.Counter, abi: CounterAbi, functionName: "getNumber", args: [] @@ -215,11 +209,11 @@ describe("nexus.session.client", async () => { }, 60000) test("session signer is not allowed to send unauthorised action", async () => { - const useSessionsModule = toSmartSessions({ + const usePermissionsModule = toSmartSessionsValidator({ account: nexusClient.account, signer: sessionKeyAccount, moduleData: { - permissionId: cachedPermissionId + permissionIds: [cachedPermissionId] } }) @@ -232,10 +226,10 @@ describe("nexus.session.client", async () => { }) const useSmartSessionNexusClient = smartSessionNexusClient.extend( - smartSessionUseActions(useSessionsModule) + smartSessionUseActions(usePermissionsModule) ) - const isEnabled = await isSessionEnabled({ + const isEnabled = await isPermissionEnabled({ client: testClient as unknown as PublicClient, accountAddress: nexusClient.account.address, permissionId: cachedPermissionId @@ -243,7 +237,7 @@ describe("nexus.session.client", async () => { expect(isEnabled).toBe(true) const counterBefore = await testClient.readContract({ - address: TEST_CONTRACTS.Counter.address, + address: testAddresses.Counter, abi: CounterAbi, functionName: "getNumber" }) @@ -252,12 +246,11 @@ describe("nexus.session.client", async () => { // @note session signer is only allowed to call incrementNumber expect( - useSmartSessionNexusClient.useSession({ - actions: [ + useSmartSessionNexusClient.usePermission({ + calls: [ { - target: TEST_CONTRACTS.Counter.address, - value: 0n, - callData: encodeFunctionData({ + to: testAddresses.Counter, + data: encodeFunctionData({ abi: CounterAbi, functionName: "decrementNumber" }) @@ -267,7 +260,7 @@ describe("nexus.session.client", async () => { ).rejects.toThrow() const counterAfter = await testClient.readContract({ - address: TEST_CONTRACTS.Counter.address, + address: testAddresses.Counter, abi: CounterAbi, functionName: "getNumber", args: [] diff --git a/src/sdk/clients/createNexusSessionClient.ts b/src/sdk/clients/createNexusSessionClient.ts index fde3c0462..01042e331 100644 --- a/src/sdk/clients/createNexusSessionClient.ts +++ b/src/sdk/clients/createNexusSessionClient.ts @@ -4,6 +4,4 @@ import { type NexusClientConfig, createNexusClient } from "./createNexusClient" export type NexusSessionClientConfig = NexusClientConfig & { accountAddress: Address } -export const createNexusSessionClient = async ( - parameters: NexusSessionClientConfig -) => await createNexusClient({ ...parameters }) +export const createNexusSessionClient = createNexusClient diff --git a/src/sdk/clients/decorators/dan/Helpers.ts b/src/sdk/clients/decorators/dan/Helpers.ts new file mode 100644 index 000000000..2321509a3 --- /dev/null +++ b/src/sdk/clients/decorators/dan/Helpers.ts @@ -0,0 +1,77 @@ +import type { + IBrowserWallet, + TypedData +} from "@silencelaboratories/walletprovider-sdk" +import { http, type Chain, type WalletClient, createWalletClient } from "viem" +import type { LocalAccount } from "viem/accounts" + +/** + * Implementation of IBrowserWallet for DAN (Distributed Account Network). + * Provides wallet functionality using viem's WalletClient. + */ +export class DanWallet implements IBrowserWallet { + walletClient: WalletClient + + /** + * Creates a new DanWallet instance. + * + * @param account - The local account to use for transactions + * @param chain - The blockchain chain configuration + */ + constructor(account: LocalAccount, chain: Chain) { + this.walletClient = createWalletClient({ + account, + chain, + transport: http() + }) + } + + /** + * Signs typed data according to EIP-712. + * + * @param _ - Unused parameter (kept for interface compatibility) + * @param request - The typed data to sign + * @returns A promise resolving to the signature + */ + async signTypedData(_: string, request: TypedData): Promise { + // @ts-ignore + return await this.walletClient.signTypedData(request) + } +} + +/** + * Converts a hexadecimal string to a Uint8Array. + * + * @param hex - The hexadecimal string to convert (must have even length) + * @returns A Uint8Array representation of the hex string + * @throws If the hex string has an odd number of characters + */ +export const hexToUint8Array = (hex: string): Uint8Array => { + if (hex.length % 2 !== 0) { + throw new Error("Hex string must have an even number of characters") + } + const array = new Uint8Array(hex.length / 2) + for (let i = 0; i < hex.length; i += 2) { + array[i / 2] = Number.parseInt(hex.substr(i, 2), 16) + } + return array +} + +/** + * Generates a random UUID string of specified length. + * + * @param length - The desired length of the UUID (default: 24) + * @returns A random string of the specified length + */ +export const uuid = (length = 24) => { + let result = "" + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + const charactersLength = characters.length + let counter = 0 + while (counter < length) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)) + counter += 1 + } + return result +} diff --git a/src/sdk/clients/decorators/dan/decorators/dan.decorators.test.ts b/src/sdk/clients/decorators/dan/decorators/dan.decorators.test.ts new file mode 100644 index 000000000..4308477aa --- /dev/null +++ b/src/sdk/clients/decorators/dan/decorators/dan.decorators.test.ts @@ -0,0 +1,149 @@ +import { http, type Address, type Chain, type LocalAccount, isHex } from "viem" +import { verifyMessage } from "viem" +import type { UserOperation } from "viem/account-abstraction" +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 { type NexusClient, createNexusClient } from "../../../createNexusClient" +import { DanWallet, hexToUint8Array, uuid } from "../Helpers" +import { danActions } from "./" + +describe("dan.decorators", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let eoaAccount: LocalAccount + let nexusAccountAddress: Address + let nexusClient: NexusClient + let userTwo: LocalAccount + let userThree: LocalAccount + + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = getTestAccount(0) + userTwo = getTestAccount(1) + userThree = getTestAccount(2) + testClient = toTestClient(chain, getTestAccount(5)) + + nexusClient = await createNexusClient({ + signer: eoaAccount, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() + await fundAndDeployClients(testClient, [nexusClient]) + }) + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test("DanWallet should initialize correctly", () => { + const account = getTestAccount(0) + const danWallet = new DanWallet(account, chain) + expect(danWallet.walletClient).toBeDefined() + expect(danWallet.walletClient.account).toBe(account) + expect(danWallet.walletClient.chain).toBe(chain) + }) + + test("DanWallet should sign typed data", async () => { + const account = getTestAccount(0) + const danWallet = new DanWallet(account, chain) + + const typedData = { + types: { + Test: [{ name: "test", type: "string" }] + }, + primaryType: "Test", + domain: { + name: "Test Domain", + version: "1", + chainId: 1 + }, + message: { + test: "Hello World" + } + } + + const signature = await danWallet.signTypedData("", typedData) + expect(signature).toBeDefined() + expect(isHex(signature as string)).toBe(true) + }) + + test("hexToUint8Array should convert hex string correctly", () => { + const hex = "0a0b0c" + const result = hexToUint8Array(hex) + expect(result).toBeInstanceOf(Uint8Array) + expect(result.length).toBe(3) + expect(Array.from(result)).toEqual([10, 11, 12]) + }) + + test("hexToUint8Array should throw on invalid hex string", () => { + expect(() => hexToUint8Array("0a0")).toThrow( + "Hex string must have an even number of characters" + ) + }) + + test("uuid should generate string of correct length", () => { + const length = 32 + const result = uuid(length) + expect(result.length).toBe(length) + expect(typeof result).toBe("string") + }) + + test("uuid should use default length of 24", () => { + const result = uuid() + expect(result.length).toBe(24) + }) + + test("uuid should generate unique values", () => { + const uuid1 = uuid() + const uuid2 = uuid() + expect(uuid1).not.toBe(uuid2) + }) + + test("should check that signature is verified", async () => { + const danNexusClient = nexusClient.extend(danActions()) + const keyGenData = await danNexusClient.keyGen() + + // @ts-ignore + const preparedUserOperation = (await danNexusClient.prepareUserOperation({ + calls: [{ to: userTwo.address, value: 1n }] + })) as UserOperation + + const sendUserOperationParameters = await danNexusClient.sigGen({ + keyGenData, + ...preparedUserOperation + }) + + const userOpHash = await danNexusClient?.account?.getUserOpHash( + preparedUserOperation + ) + + if (!userOpHash || !sendUserOperationParameters.signature) + throw new Error("Missing userOpHash or signature") + + const valid = await verifyMessage({ + address: keyGenData.sessionPublicKey, + message: { raw: userOpHash }, + signature: sendUserOperationParameters.signature + }) + + // Verify transaction success + expect(valid).toBe(true) + }) +}) diff --git a/src/sdk/clients/decorators/dan/decorators/index.ts b/src/sdk/clients/decorators/dan/decorators/index.ts new file mode 100644 index 000000000..5117e96da --- /dev/null +++ b/src/sdk/clients/decorators/dan/decorators/index.ts @@ -0,0 +1,43 @@ +import type { Chain, Client, Transport } from "viem" +import type { UserOperation } from "viem/account-abstraction" +import type { ModularSmartAccount } from "../../../../modules/utils/Types" +import { type KeyGenData, type KeyGenParameters, keyGen } from "./keyGen" +import { type SigGenParameters, sigGen } from "./sigGen" + +/** + * Defines the available DAN (Distributed Account Network) actions for a modular smart account. + * Provides methods for key generation, signature generation, and transaction sending. + * + * @template TModularSmartAccount - The type of modular smart account being used + */ +export type DanActions< + TModularSmartAccount extends ModularSmartAccount | undefined +> = { + /** Generates keys for the smart account with optional parameters */ + keyGen: (args?: KeyGenParameters) => Promise + /** Generates signatures for user operations */ + /** Generates signatures for user operations */ + sigGen: (parameters: SigGenParameters) => Promise> +} + +/** + * Creates a set of DAN-specific actions for interacting with a modular smart account. + * This function is a decorator that adds DAN functionality to a viem Client instance. + * + * @returns A function that takes a client and returns DAN-specific actions + * + * @example + * const client = createClient(...) + * const danClient = client.extend(danActions()) + */ +export function danActions() { + return < + TModularSmartAccount extends ModularSmartAccount | undefined, + chain extends Chain | undefined + >( + client: Client + ): DanActions => ({ + keyGen: (args) => keyGen(client, args), + sigGen: (parameters) => sigGen(client, parameters) + }) +} diff --git a/src/sdk/clients/decorators/dan/decorators/keyGen.ts b/src/sdk/clients/decorators/dan/decorators/keyGen.ts new file mode 100644 index 000000000..2d4ed4ee7 --- /dev/null +++ b/src/sdk/clients/decorators/dan/decorators/keyGen.ts @@ -0,0 +1,138 @@ +import { + EOAAuth, + NetworkSigner, + WalletProviderServiceClient, + computeAddress, + generateEphPrivateKey, + getEphPublicKey +} from "@silencelaboratories/walletprovider-sdk" +import { type Chain, type Client, type Hex, type Transport, toHex } from "viem" +import { ERROR_MESSAGES, type Signer } from "../../../../account" +import type { ModularSmartAccount } from "../../../../modules/utils/Types" +import { DanWallet, uuid } from "../Helpers" + +/** + * Constants for DAN (Distributed Account Network) configuration + */ +export const EPHEMERAL_KEY_TTL = 60 * 60 * 24 // 1 day +export const QUORUM_PARTIES = 3 +export const QUORUM_THRESHOLD = 2 +export const DEFAULT_DAN_URL = "wss://dan.staging.biconomy.io/v1" + +/** + * Response data from the key generation process + */ +export type KeyGenData = { + /** The generated public key */ + publicKey: Hex + /** Unique identifier for the generated key */ + keyId: Hex + /** EOA address derived from the session key */ + sessionPublicKey: Hex + /** Secret key of the ephemeral key pair */ + ephSK: Hex + /** Unique identifier for the ephemeral key */ + ephId: Hex +} + +/** + * Parameters for key generation + */ +export type KeyGenParameters< + TModularSmartAccount extends ModularSmartAccount | undefined +> = { + /** The smart account to add the owner to. If not provided, the client's account will be used */ + account?: TModularSmartAccount + /** Optional Signer, defaults to the one in the client */ + signer?: Signer + /** Secret key of the ephemeral key pair */ + ephSK: Hex +} & DanParameters + +/** + * Configuration parameters for DAN network + */ +export type DanParameters = { + /** Chain configuration */ + chain?: Chain + /** Minimum number of parties required for signing (default: 2) */ + threshold?: number + /** Total number of parties in the signing group (default: 3) */ + partiesNumber?: number + /** Duration of the ephemeral key validity in seconds (default: 24 hours) */ + duration?: number + /** URL of the wallet provider service */ + walletProviderUrl?: string + /** Unique identifier for the ephemeral key */ + ephId?: string +} + +/** + * Generates a key using the Distributed Account Network (DAN). + * + * @typeParam TModularSmartAccount - The type of the modular smart account, or undefined. + * @param client - The viem client instance. + * @param parameters - Optional parameters for key generation. + * @returns A Promise that resolves to the key generation data. + */ +export async function keyGen< + TModularSmartAccount extends ModularSmartAccount | undefined +>( + client: Client, + parameters?: KeyGenParameters +): Promise { + const { + signer: signer_ = client?.account?.client?.account as Signer, + walletProviderUrl = DEFAULT_DAN_URL, + partiesNumber = QUORUM_PARTIES, + threshold = QUORUM_THRESHOLD, + duration = EPHEMERAL_KEY_TTL, + ephId = uuid(), + chain: chain_ = client.account?.client?.chain + } = parameters ?? {} + + if (!signer_) { + throw new Error(ERROR_MESSAGES.SIGNER_REQUIRED) + } + + if (!chain_) { + throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND) + } + + const skArr = generateEphPrivateKey() + const ephPKArr = getEphPublicKey(skArr) + const ephSK = toHex(skArr) + + const wpClient = new WalletProviderServiceClient({ + walletProviderId: "WalletProvider", + walletProviderUrl + }) + + const wallet = new DanWallet(signer_, chain_) + + const eoaAuth = new EOAAuth( + ephId, + signer_.address, + wallet, + ephPKArr, + duration + ) + + const networkSigner = new NetworkSigner( + wpClient, + threshold, + partiesNumber, + eoaAuth + ) + + const createdKey = await networkSigner.generateKey() + + const sessionPublicKey = computeAddress(createdKey.publicKey) + + return { + ...createdKey, + sessionPublicKey, + ephSK, + ephId + } as KeyGenData +} diff --git a/src/sdk/clients/decorators/dan/decorators/sigGen.ts b/src/sdk/clients/decorators/dan/decorators/sigGen.ts new file mode 100644 index 000000000..363348d20 --- /dev/null +++ b/src/sdk/clients/decorators/dan/decorators/sigGen.ts @@ -0,0 +1,118 @@ +import { + EphAuth, + NetworkSigner, + WalletProviderServiceClient +} from "@silencelaboratories/walletprovider-sdk" +import type { Chain, Client, Hex, PartialBy, Transport } from "viem" +import { + type PrepareUserOperationParameters, + type UserOperation, + prepareUserOperation +} from "viem/account-abstraction" +import { getAction } from "viem/utils" +import { ERROR_MESSAGES, type Signer } from "../../../../account" +import { deepHexlify } from "../../../../account/utils/deepHexlify" +import type { + AnyData, + ModularSmartAccount +} from "../../../../modules/utils/Types" +import { hexToUint8Array } from "../Helpers" +import { + DEFAULT_DAN_URL, + type DanParameters, + type KeyGenData, + QUORUM_PARTIES, + QUORUM_THRESHOLD +} from "./keyGen" + +/** + * Parameters required for signature generation + */ +export type SigGenParameters = PartialBy< + PrepareUserOperationParameters & { + /** Optional Signer, defaults to the one in the client */ + signer?: Signer + /** Data from previous key generation step */ + keyGenData: KeyGenData + } & DanParameters, + "account" +> + +export const REQUIRED_FIELDS = [ + "sender", + "nonce", + "callData", + "callGasLimit", + "verificationGasLimit", + "preVerificationGas", + "maxFeePerGas", + "maxPriorityFeePerGas", + "factoryData" +] + +export const sigGen = async < + TModularSmartAccount extends ModularSmartAccount | undefined, + chain extends Chain | undefined +>( + client: Client, + parameters: SigGenParameters +): Promise> => { + const { + walletProviderUrl = DEFAULT_DAN_URL, + partiesNumber = QUORUM_PARTIES, + threshold = QUORUM_THRESHOLD, + chain: chain_ = client.account?.client?.chain, + keyGenData: { ephSK, ephId, keyId } + } = parameters ?? {} + + if (!chain_) { + throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND) + } + + const ephSKArr = hexToUint8Array(ephSK.slice(2)) + + const wpClient = new WalletProviderServiceClient({ + walletProviderId: "WalletProvider", + walletProviderUrl + }) + const authModule = new EphAuth(ephId, ephSKArr) + + const networkSigner = new NetworkSigner( + wpClient, + threshold, + partiesNumber, + authModule + ) + + const preparedUserOperation = await getAction( + client, + prepareUserOperation, + "prepareUserOperation" + )(parameters as PrepareUserOperationParameters) + + const userOperation = REQUIRED_FIELDS.reduce((acc, field) => { + if (field in preparedUserOperation) { + // @ts-ignore + acc[field] = preparedUserOperation[field] + } + return acc + }, {} as AnyData) + + const userOperationHexed = deepHexlify(userOperation) + + const signMessage = JSON.stringify({ + message: JSON.stringify({ + userOperation: userOperationHexed, + entryPointVersion: "v0.7.0", + entryPointAddress: "0x0000000071727De22E5E9d8BAf0edAc6f37da032", + chainId: chain_.id + }), + requestType: "accountAbstractionTx" + }) + + const { sign, recid } = await networkSigner.signMessage(keyId, signMessage) + + const recid_hex = (27 + recid).toString(16) + const signature = `0x${sign}${recid_hex}` as Hex + return { ...userOperationHexed, signature } +} diff --git a/src/sdk/clients/decorators/dan/index.ts b/src/sdk/clients/decorators/dan/index.ts new file mode 100644 index 000000000..f06563064 --- /dev/null +++ b/src/sdk/clients/decorators/dan/index.ts @@ -0,0 +1,2 @@ +export * from "./decorators" +export * from "./Helpers" diff --git a/src/sdk/clients/decorators/erc7579/accountId.ts b/src/sdk/clients/decorators/erc7579/accountId.ts index f7759e5c6..4c9d240b9 100644 --- a/src/sdk/clients/decorators/erc7579/accountId.ts +++ b/src/sdk/clients/decorators/erc7579/accountId.ts @@ -41,7 +41,7 @@ export async function accountId( if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts b/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts index 6e2f71799..24ee7731f 100644 --- a/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts +++ b/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts @@ -1,10 +1,8 @@ -import { textSpanOverlapsWith } from "typescript" import { http, type Account, type Address, type Chain, - encodeAbiParameters, encodePacked, isHex } from "viem" @@ -19,7 +17,7 @@ import { killNetwork, toTestClient } from "../../../../test/testUtils" -import contracts from "../../../__contracts" +import { k1ValidatorAddress } from "../../../constants" import { type NexusClient, createNexusClient } from "../../createNexusClient" describe("erc7579.decorators", async () => { @@ -79,14 +77,14 @@ describe("erc7579.decorators", async () => { nexusClient.isModuleInstalled({ module: { type: "validator", - module: contracts.k1Validator.address, + address: k1ValidatorAddress, initData: "0x" } }) ]) expect(installedExecutors[0].length).toBeTypeOf("number") - expect(installedValidators[0]).toEqual([contracts.k1Validator.address]) + expect(installedValidators[0]).toEqual([k1ValidatorAddress]) expect(isHex(activeHook)).toBe(true) expect(fallbackSelector.length).toBeTypeOf("number") expect(supportsValidator).toBe(true) @@ -98,7 +96,7 @@ describe("erc7579.decorators", async () => { const hash = await nexusClient.installModule({ module: { type: "validator", - module: mockAddresses.MockValidator, + address: mockAddresses.MockValidator, initData: encodePacked(["address"], [eoaAccount.address]) } }) @@ -111,7 +109,7 @@ describe("erc7579.decorators", async () => { const hash = await nexusClient.uninstallModule({ module: { type: "validator", - module: mockAddresses.MockValidator, + address: mockAddresses.MockValidator, initData: encodePacked(["address"], [eoaAccount.address]) } }) diff --git a/src/sdk/clients/decorators/erc7579/getActiveHook.ts b/src/sdk/clients/decorators/erc7579/getActiveHook.ts index a59d3b656..08766e615 100644 --- a/src/sdk/clients/decorators/erc7579/getActiveHook.ts +++ b/src/sdk/clients/decorators/erc7579/getActiveHook.ts @@ -35,7 +35,7 @@ export async function getActiveHook< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/clients/decorators/erc7579/getFallbackBySelector.ts b/src/sdk/clients/decorators/erc7579/getFallbackBySelector.ts index 56fee541d..659c81382 100644 --- a/src/sdk/clients/decorators/erc7579/getFallbackBySelector.ts +++ b/src/sdk/clients/decorators/erc7579/getFallbackBySelector.ts @@ -44,7 +44,7 @@ export async function getFallbackBySelector< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/clients/decorators/erc7579/getInstalledExecutors.ts b/src/sdk/clients/decorators/erc7579/getInstalledExecutors.ts index 3972f7186..55e5f0d4f 100644 --- a/src/sdk/clients/decorators/erc7579/getInstalledExecutors.ts +++ b/src/sdk/clients/decorators/erc7579/getInstalledExecutors.ts @@ -43,7 +43,7 @@ export async function getInstalledExecutors< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/clients/decorators/erc7579/getInstalledValidators.ts b/src/sdk/clients/decorators/erc7579/getInstalledValidators.ts index cf7c11d5c..86aafb51d 100644 --- a/src/sdk/clients/decorators/erc7579/getInstalledValidators.ts +++ b/src/sdk/clients/decorators/erc7579/getInstalledValidators.ts @@ -43,7 +43,7 @@ export async function getInstalledValidators< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/clients/decorators/erc7579/getPreviousModule.ts b/src/sdk/clients/decorators/erc7579/getPreviousModule.ts index b1d2e834e..ae815fac5 100644 --- a/src/sdk/clients/decorators/erc7579/getPreviousModule.ts +++ b/src/sdk/clients/decorators/erc7579/getPreviousModule.ts @@ -1,4 +1,3 @@ -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" import type { Client, Hex } from "viem" import type { GetSmartAccountParameter, @@ -6,6 +5,7 @@ import type { } from "viem/account-abstraction" import { getAddress } from "viem/utils" import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import type { ModuleMeta } from "../../../modules/utils/Types" const SENTINEL_ADDRESS = "0x0000000000000000000000000000000000000001" as const export type GetPreviousModuleParameters< @@ -46,7 +46,7 @@ export async function getPreviousModule< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } @@ -63,12 +63,12 @@ export async function getPreviousModule< throw new Error(`Unknown module type ${module.type}`) } - const index = installedModules.indexOf(getAddress(module.module)) + const index = installedModules.indexOf(getAddress(module.address)) if (index === 0) { return SENTINEL_ADDRESS } if (index > 0) { return installedModules[index - 1] } - throw new Error(`Module ${module.module} not found in installed modules`) + throw new Error(`Module ${module.address} not found in installed modules`) } diff --git a/src/sdk/clients/decorators/erc7579/index.ts b/src/sdk/clients/decorators/erc7579/index.ts index 94c5069ea..a6320138c 100644 --- a/src/sdk/clients/decorators/erc7579/index.ts +++ b/src/sdk/clients/decorators/erc7579/index.ts @@ -31,6 +31,7 @@ import { type IsModuleInstalledParameters, isModuleInstalled } from "./isModuleInstalled.js" +import { moduleActivator } from "./moduleActivator" import { type SupportsExecutionModeParameters, supportsExecutionMode @@ -113,7 +114,8 @@ export { getInstalledExecutors, getActiveHook, getFallbackBySelector, - getPreviousModule + getPreviousModule, + moduleActivator } export function erc7579Actions() { diff --git a/src/sdk/clients/decorators/erc7579/installModule.ts b/src/sdk/clients/decorators/erc7579/installModule.ts index 36604a81b..1ddc7e494 100644 --- a/src/sdk/clients/decorators/erc7579/installModule.ts +++ b/src/sdk/clients/decorators/erc7579/installModule.ts @@ -1,4 +1,3 @@ -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" import { type Chain, type Client, @@ -14,6 +13,7 @@ import { } from "viem/account-abstraction" import { getAction, parseAccount } from "viem/utils" import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import type { ModuleMeta } from "../../../modules/utils/Types" import { parseModuleTypeId } from "./supportsModule" export type InstallModuleParameters< @@ -56,12 +56,12 @@ export async function installModule< maxFeePerGas, maxPriorityFeePerGas, nonce, - module: { module, initData, type } + module: { address, initData, type } } = parameters if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } @@ -100,7 +100,7 @@ export async function installModule< } ], functionName: "installModule", - args: [parseModuleTypeId(type), getAddress(module), initData ?? "0x"] + args: [parseModuleTypeId(type), getAddress(address), initData ?? "0x"] }) } ], diff --git a/src/sdk/clients/decorators/erc7579/installModules.ts b/src/sdk/clients/decorators/erc7579/installModules.ts index 4513fba94..e415f2180 100644 --- a/src/sdk/clients/decorators/erc7579/installModules.ts +++ b/src/sdk/clients/decorators/erc7579/installModules.ts @@ -65,7 +65,7 @@ export async function installModules< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/clients/decorators/erc7579/isModuleInstalled.ts b/src/sdk/clients/decorators/erc7579/isModuleInstalled.ts index 73a2380b0..3e7f287c4 100644 --- a/src/sdk/clients/decorators/erc7579/isModuleInstalled.ts +++ b/src/sdk/clients/decorators/erc7579/isModuleInstalled.ts @@ -1,4 +1,3 @@ -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" import { type Chain, type Client, @@ -15,6 +14,7 @@ import type { import { call, readContract } from "viem/actions" import { getAction, parseAccount } from "viem/utils" import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import type { ModuleMeta } from "../../../modules/utils/Types" import { parseModuleTypeId } from "./supportsModule" export type IsModuleInstalledParameters< @@ -52,12 +52,12 @@ export async function isModuleInstalled< ): Promise { const { account: account_ = client.account, - module: { module, initData, type } + module: { address, initData, type } } = parameters if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } @@ -100,7 +100,7 @@ export async function isModuleInstalled< )({ abi, functionName: "isModuleInstalled", - args: [parseModuleTypeId(type), getAddress(module), initData ?? "0x"], + args: [parseModuleTypeId(type), getAddress(address), initData ?? "0x"], address: account.address })) as unknown as Promise } catch (error) { @@ -118,7 +118,7 @@ export async function isModuleInstalled< data: encodeFunctionData({ abi, functionName: "isModuleInstalled", - args: [parseModuleTypeId(type), getAddress(module), initData ?? "0x"] + args: [parseModuleTypeId(type), getAddress(address), initData ?? "0x"] }) }) diff --git a/src/sdk/clients/decorators/erc7579/moduleActivator.ts b/src/sdk/clients/decorators/erc7579/moduleActivator.ts new file mode 100644 index 000000000..0b58b1124 --- /dev/null +++ b/src/sdk/clients/decorators/erc7579/moduleActivator.ts @@ -0,0 +1,20 @@ +import type { Chain, Client, Transport } from "viem" +import type { Module } from "../../../modules" +import type { ModularSmartAccount } from "../../../modules/utils/Types" + +export type ModuleActions = Record + +/** + * Creates a decorator function that activates a specific module for a modular smart account client + * @param module - The module to be activated on the smart account + * @returns An empty object + * @remarks This decorator is used to only set the module on the client + */ +export function moduleActivator(module: Module) { + return ( + client: Client + ): ModuleActions => { + client?.account?.setModule(module) + return {} as ModuleActions + } +} diff --git a/src/sdk/clients/decorators/erc7579/supportsExecutionMode.ts b/src/sdk/clients/decorators/erc7579/supportsExecutionMode.ts index bb547ca42..2f25b4661 100644 --- a/src/sdk/clients/decorators/erc7579/supportsExecutionMode.ts +++ b/src/sdk/clients/decorators/erc7579/supportsExecutionMode.ts @@ -103,7 +103,7 @@ export async function supportsExecutionMode< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/clients/decorators/erc7579/supportsModule.ts b/src/sdk/clients/decorators/erc7579/supportsModule.ts index 538362e58..188a4bb8a 100644 --- a/src/sdk/clients/decorators/erc7579/supportsModule.ts +++ b/src/sdk/clients/decorators/erc7579/supportsModule.ts @@ -70,7 +70,7 @@ export async function supportsModule< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/clients/decorators/erc7579/uninstallFallback.ts b/src/sdk/clients/decorators/erc7579/uninstallFallback.ts index 8e1203009..5d499b3b2 100644 --- a/src/sdk/clients/decorators/erc7579/uninstallFallback.ts +++ b/src/sdk/clients/decorators/erc7579/uninstallFallback.ts @@ -1,4 +1,3 @@ -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" import { type Chain, type Client, @@ -15,6 +14,7 @@ import { import { getAction } from "viem/utils" import { parseAccount } from "viem/utils" import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import type { ModuleMeta } from "../../../modules/utils/Types" import { parseModuleTypeId } from "./supportsModule" export type UninstallFallbackParameters< @@ -57,12 +57,12 @@ export async function uninstallFallback< maxFeePerGas, maxPriorityFeePerGas, nonce, - module: { module, initData, type } + module: { address, initData, type } } = parameters if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } @@ -101,7 +101,7 @@ export async function uninstallFallback< } ], functionName: "uninstallFallback", - args: [parseModuleTypeId(type), getAddress(module), initData ?? "0x"] + args: [parseModuleTypeId(type), getAddress(address), initData ?? "0x"] }) } ], diff --git a/src/sdk/clients/decorators/erc7579/uninstallModule.ts b/src/sdk/clients/decorators/erc7579/uninstallModule.ts index bef2e576d..4341936c9 100644 --- a/src/sdk/clients/decorators/erc7579/uninstallModule.ts +++ b/src/sdk/clients/decorators/erc7579/uninstallModule.ts @@ -1,4 +1,3 @@ -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" import { type Chain, type Client, @@ -17,6 +16,7 @@ import { getAction } from "viem/utils" import { parseAccount } from "viem/utils" import { getInstalledValidators, getPreviousModule } from "." import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import type { ModuleMeta } from "../../../modules/utils/Types" import { parseModuleTypeId } from "./supportsModule" export type UninstallModuleParameters< @@ -59,12 +59,12 @@ export async function uninstallModule< maxFeePerGas, maxPriorityFeePerGas, nonce, - module: { module, initData, type } + module: { address, initData, type } } = parameters if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } @@ -73,7 +73,7 @@ export async function uninstallModule< const prevModule = await getPreviousModule(client, { module: { - module, + address, type }, installedValidators, @@ -121,7 +121,7 @@ export async function uninstallModule< } ], functionName: "uninstallModule", - args: [parseModuleTypeId(type), getAddress(module), deInitData] + args: [parseModuleTypeId(type), getAddress(address), deInitData] }) } ], diff --git a/src/sdk/clients/decorators/erc7579/uninstallModules.ts b/src/sdk/clients/decorators/erc7579/uninstallModules.ts index 2b4cb65b4..656b71239 100644 --- a/src/sdk/clients/decorators/erc7579/uninstallModules.ts +++ b/src/sdk/clients/decorators/erc7579/uninstallModules.ts @@ -1,4 +1,3 @@ -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" import { type Chain, type Client, @@ -15,6 +14,7 @@ import { import { getAction } from "viem/utils" import { parseAccount } from "viem/utils" import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import type { ModuleMeta } from "../../../modules/utils/Types" import { parseModuleTypeId } from "./supportsModule" export type UninstallModulesParameters< @@ -61,7 +61,7 @@ export async function uninstallModules< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } @@ -72,7 +72,7 @@ export async function uninstallModules< sendUserOperation, "sendUserOperation" )({ - calls: modules.map(({ type, module, initData }) => ({ + calls: modules.map(({ type, address, initData }) => ({ to: account.address, value: BigInt(0), data: encodeFunctionData({ @@ -99,7 +99,7 @@ export async function uninstallModules< } ], functionName: "uninstallModule", - args: [parseModuleTypeId(type), getAddress(module), initData ?? "0x"] + args: [parseModuleTypeId(type), getAddress(address), initData ?? "0x"] }) })), maxFeePerGas, diff --git a/src/sdk/clients/decorators/index.ts b/src/sdk/clients/decorators/index.ts new file mode 100644 index 000000000..4aff93b4d --- /dev/null +++ b/src/sdk/clients/decorators/index.ts @@ -0,0 +1,4 @@ +export * from "./erc7579" +export * from "./smartAccount" +export * from "./bundler" +export * from "./dan" diff --git a/src/sdk/clients/decorators/smartAccount/index.ts b/src/sdk/clients/decorators/smartAccount/index.ts index f3014ec97..fc6b36dd1 100644 --- a/src/sdk/clients/decorators/smartAccount/index.ts +++ b/src/sdk/clients/decorators/smartAccount/index.ts @@ -13,6 +13,7 @@ import type { WriteContractParameters } from "viem" import type { SmartAccount } from "viem/account-abstraction" +import type { AnyData } from "../../../modules/utils/Types" import { sendTransaction } from "./sendTransaction" import { signMessage } from "./signMessage" import { signTypedData } from "./signTypedData" @@ -27,7 +28,7 @@ export type SmartAccountActions< * 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/nexus/nexus-client/methods#sendtransaction.html + * - Docs: https://viem.sh/nexus-client/methods#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) @@ -81,6 +82,7 @@ export type SmartAccountActions< > >[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))`. * @@ -240,7 +242,7 @@ export type SmartAccountActions< * * 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/nexus/nexus-client/methods#sendtransaction.html) with [ABI-encoded `data`](https://viem.sh/docs/contract/encodeFunctionData.html). + * Internally, uses a [Wallet Client](https://viem.sh/docs/clients/wallet.html) to call the [`sendTransaction` action](https://viem.sh/nexus-client/methods#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.__ * @@ -322,7 +324,7 @@ export function smartAccountActions() { >( client: Client ): SmartAccountActions => ({ - sendTransaction: (args) => sendTransaction(client, args as any), + sendTransaction: (args) => sendTransaction(client, args as AnyData), 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 index d0f137962..5224edfcb 100644 --- a/src/sdk/clients/decorators/smartAccount/sendTransaction.ts +++ b/src/sdk/clients/decorators/smartAccount/sendTransaction.ts @@ -13,8 +13,6 @@ import { } from "viem/account-abstraction" import { getAction, parseAccount } from "viem/utils" import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" -import { bigIntReplacer } from "../../../account/utils/Helpers" -import { Logger } from "../../../account/utils/Logger" /** * Creates, signs, and sends a new transaction to the network using a smart account. @@ -45,8 +43,7 @@ export async function sendTransaction< client: Client, args: | SendTransactionParameters - | SendUserOperationParameters, - signature?: `0x${string}` + | SendUserOperationParameters ): Promise { let userOpHash: Hash @@ -63,7 +60,7 @@ export async function sendTransaction< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } @@ -71,7 +68,11 @@ export async function sendTransaction< if (!to) throw new Error("Missing to address") - const sendUserOperationArgs = { + userOpHash = await getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ calls: [ { to, @@ -82,27 +83,14 @@ export async function sendTransaction< account, maxFeePerGas, maxPriorityFeePerGas, - signature, nonce: nonce ? BigInt(nonce) : undefined - } - - const { account: _, ...logableArgs } = sendUserOperationArgs - Logger.log(JSON.stringify(logableArgs, bigIntReplacer, 2)) - - userOpHash = await getAction( - client, - sendUserOperation, - "sendUserOperation" - )(sendUserOperationArgs) + }) } else { userOpHash = await getAction( client, sendUserOperation, "sendUserOperation" - )({ ...args, signature } as SendUserOperationParameters< - account, - accountOverride - >) + )({ ...args } as SendUserOperationParameters) } const userOperationReceipt = await getAction( diff --git a/src/sdk/clients/index.ts b/src/sdk/clients/index.ts index 46973e1a4..4e9aaddee 100644 --- a/src/sdk/clients/index.ts +++ b/src/sdk/clients/index.ts @@ -1,5 +1,5 @@ export * from "./createBicoBundlerClient" export * from "./createBicoPaymasterClient" export * from "./createNexusClient" -export * from "./decorators/erc7579" -export * from "./decorators/smartAccount" +export * from "./createNexusSessionClient" +export * from "./decorators" diff --git a/src/sdk/__contracts/abi/EIP1271Abi.ts b/src/sdk/constants/abi/EIP1271Abi.ts similarity index 100% rename from src/sdk/__contracts/abi/EIP1271Abi.ts rename to src/sdk/constants/abi/EIP1271Abi.ts diff --git a/src/sdk/constants/abi/ERC7484RegistryAbi.ts b/src/sdk/constants/abi/ERC7484RegistryAbi.ts new file mode 100644 index 000000000..ebac48399 --- /dev/null +++ b/src/sdk/constants/abi/ERC7484RegistryAbi.ts @@ -0,0 +1,13 @@ +export const ERC7484RegistryAbi = [ + { + inputs: [ + { internalType: "address", name: "smartAccount", type: "address" } + ], + name: "findTrustedAttesters", + outputs: [ + { internalType: "address[]", name: "attesters", type: "address[]" } + ], + stateMutability: "view", + type: "function" + } +] as const diff --git a/src/sdk/__contracts/abi/EntryPointABI.ts b/src/sdk/constants/abi/EntryPointABI.ts similarity index 100% rename from src/sdk/__contracts/abi/EntryPointABI.ts rename to src/sdk/constants/abi/EntryPointABI.ts diff --git a/src/sdk/constants/abi/SmartSessionAbi.ts b/src/sdk/constants/abi/SmartSessionAbi.ts new file mode 100644 index 000000000..d8b59e004 --- /dev/null +++ b/src/sdk/constants/abi/SmartSessionAbi.ts @@ -0,0 +1,184 @@ +export const SmartSessionAbi = [ + { + type: "function", + name: "enableSessions", + inputs: [ + { + name: "sessions", + type: "tuple[]", + internalType: "struct Session[]", + components: [ + { + name: "sessionValidator", + type: "address", + internalType: "contract ISessionValidator" + }, + { + name: "sessionValidatorInitData", + type: "bytes", + internalType: "bytes" + }, + { name: "salt", type: "bytes32", internalType: "bytes32" }, + { + name: "userOpPolicies", + type: "tuple[]", + internalType: "struct PolicyData[]", + components: [ + { name: "policy", type: "address", internalType: "address" }, + { name: "initData", type: "bytes", internalType: "bytes" } + ] + }, + { + name: "erc7739Policies", + type: "tuple", + internalType: "struct ERC7739Data", + components: [ + { + name: "allowedERC7739Content", + type: "string[]", + internalType: "string[]" + }, + { + name: "erc1271Policies", + type: "tuple[]", + internalType: "struct PolicyData[]", + components: [ + { name: "policy", type: "address", internalType: "address" }, + { name: "initData", type: "bytes", internalType: "bytes" } + ] + } + ] + }, + { + name: "actions", + type: "tuple[]", + internalType: "struct ActionData[]", + components: [ + { + name: "actionTargetSelector", + type: "bytes4", + internalType: "bytes4" + }, + { + name: "actionTarget", + type: "address", + internalType: "address" + }, + { + name: "actionPolicies", + type: "tuple[]", + internalType: "struct PolicyData[]", + components: [ + { name: "policy", type: "address", internalType: "address" }, + { name: "initData", type: "bytes", internalType: "bytes" } + ] + } + ] + } + ] + } + ], + outputs: [ + { + name: "permissionIds", + type: "bytes32[]", + internalType: "PermissionId[]" + } + ], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "getPermissionId", + inputs: [ + { + name: "session", + type: "tuple", + internalType: "struct Session", + components: [ + { + name: "sessionValidator", + type: "address", + internalType: "contract ISessionValidator" + }, + { + name: "sessionValidatorInitData", + type: "bytes", + internalType: "bytes" + }, + { name: "salt", type: "bytes32", internalType: "bytes32" }, + { + name: "userOpPolicies", + type: "tuple[]", + internalType: "struct PolicyData[]", + components: [ + { name: "policy", type: "address", internalType: "address" }, + { name: "initData", type: "bytes", internalType: "bytes" } + ] + }, + { + name: "erc7739Policies", + type: "tuple", + internalType: "struct ERC7739Data", + components: [ + { + name: "allowedERC7739Content", + type: "string[]", + internalType: "string[]" + }, + { + name: "erc1271Policies", + type: "tuple[]", + internalType: "struct PolicyData[]", + components: [ + { name: "policy", type: "address", internalType: "address" }, + { name: "initData", type: "bytes", internalType: "bytes" } + ] + } + ] + }, + { + name: "actions", + type: "tuple[]", + internalType: "struct ActionData[]", + components: [ + { + name: "actionTargetSelector", + type: "bytes4", + internalType: "bytes4" + }, + { + name: "actionTarget", + type: "address", + internalType: "address" + }, + { + name: "actionPolicies", + type: "tuple[]", + internalType: "struct PolicyData[]", + components: [ + { name: "policy", type: "address", internalType: "address" }, + { name: "initData", type: "bytes", internalType: "bytes" } + ] + } + ] + } + ] + } + ], + outputs: [ + { name: "permissionId", type: "bytes32", internalType: "PermissionId" } + ], + stateMutability: "pure" + }, + { + type: "function", + name: "isPermissionEnabled", + inputs: [ + { name: "permissionId", type: "bytes32", internalType: "PermissionId" }, + { name: "account", type: "address", internalType: "address" } + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view" + } +] diff --git a/src/sdk/__contracts/abi/UniActionPolicyAbi.ts b/src/sdk/constants/abi/UniActionPolicyAbi.ts similarity index 100% rename from src/sdk/__contracts/abi/UniActionPolicyAbi.ts rename to src/sdk/constants/abi/UniActionPolicyAbi.ts diff --git a/src/sdk/constants/abi/index.ts b/src/sdk/constants/abi/index.ts new file mode 100644 index 000000000..daef15777 --- /dev/null +++ b/src/sdk/constants/abi/index.ts @@ -0,0 +1,4 @@ +export * from "./EIP1271Abi" +export * from "./UniActionPolicyAbi" +export * from "./EntryPointABI" +export * from "./ERC7484RegistryAbi" diff --git a/src/sdk/constants/index.ts b/src/sdk/constants/index.ts new file mode 100644 index 000000000..9321413d2 --- /dev/null +++ b/src/sdk/constants/index.ts @@ -0,0 +1,47 @@ +import type { Hex } from "viem" +import { isTesting } from "../account" + +export * from "./abi" +export { + SMART_SESSIONS_ADDRESS, + OWNABLE_VALIDATOR_ADDRESS, + OWNABLE_EXECUTOR_ADDRESS, + MOCK_ATTESTER_ADDRESS, + REGISTRY_ADDRESS +} from "@rhinestone/module-sdk" +export const SIMPLE_SESSION_VALIDATOR_ADDRESS: Hex = + "0x41f143f4B5f19AfCd2602F6ADE18E75e9b5E37d3" +export const ENTRY_POINT_ADDRESS: Hex = + "0x0000000071727De22E5E9d8BAf0edAc6f37da032" +export const ENTRYPOINT_SIMULATIONS_ADDRESS: Hex = + "0x74Cb5e4eE81b86e70f9045036a1C5477de69eE87" +export const TIMEFRAME_POLICY_ADDRESS: Hex = + "0x0B7BB9bD65858593D97f12001FaDa94828307805" +export const NEXUS_BOOTSTRAP_ADDRESS: Hex = + "0x00000008c901d8871b6F6942De0B5D9cCf3873d3" +export const UNIVERSAL_ACTION_POLICY_ADDRESS: Hex = + "0x148CD6c24F4dd23C396E081bBc1aB1D92eeDe2BF" + +export const TEST_ADDRESS_NEXUS_IMPLEMENTATION_ADDRESS: Hex = + "0x3AdEa1898eb7d9FbD49242618782717A1f86DA14" +export const TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS: Hex = + "0xB19db8087aCc0Bcb8Fb559dDF2fD483978EA136F" +export const TEST_ADDRESS_K1_VALIDATOR_ADDRESS: Hex = + "0x5aec3f1c43B920a4dc21d500617fb37B8db1992C" + +export const MAINNET_ADDRESS_NEXUS_IMPLEMENTATION_ADDRESS: Hex = + "0x000000039dfcAd030719B07296710F045F0558f7" +export const MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS: Hex = + "0x00000bb19a3579F4D779215dEf97AFbd0e30DB55" +export const MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS: Hex = + "0x00000004171351c442B202678c48D8AB5B321E8f" + +export const nexusImplementationAddress: Hex = isTesting + ? TEST_ADDRESS_NEXUS_IMPLEMENTATION_ADDRESS + : MAINNET_ADDRESS_NEXUS_IMPLEMENTATION_ADDRESS +export const k1ValidatorFactoryAddress: Hex = isTesting + ? TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + : MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS +export const k1ValidatorAddress: Hex = isTesting + ? TEST_ADDRESS_K1_VALIDATOR_ADDRESS + : MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS diff --git a/src/sdk/index.ts b/src/sdk/index.ts index e5e14f2cb..614a44803 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -1,3 +1,4 @@ export * from "./account" export * from "./modules" export * from "./clients" +export * from "./constants" diff --git a/src/sdk/modules/README.md b/src/sdk/modules/README.md index f53adfcb4..6890f946f 100644 --- a/src/sdk/modules/README.md +++ b/src/sdk/modules/README.md @@ -64,7 +64,7 @@ Here's an example of how to use the Smart Sessions module: ```typescript // 1. Create the module -const sessionsModule = toSmartSessions({ +const sessionsModule = toSmartSessionsValidator({ account: nexusClient.account, signer: eoaAccount }) diff --git a/src/sdk/modules/index.ts b/src/sdk/modules/index.ts index a5bf33fe3..9d036b3b7 100644 --- a/src/sdk/modules/index.ts +++ b/src/sdk/modules/index.ts @@ -1,7 +1,4 @@ -export * from "./utils/Types" -export * from "./utils/Constants" -export * from "./utils/Helpers" - -export * from "./k1/toK1" -export * from "./ownables/toOwnables" -export * from "./smartSessions/toSmartSessions" +export * from "./k1Validator" +export * from "./ownableValidator" +export * from "./smartSessionsValidator" +export * from "./utils" diff --git a/src/sdk/modules/k1Validator/index.ts b/src/sdk/modules/k1Validator/index.ts new file mode 100644 index 000000000..81753484f --- /dev/null +++ b/src/sdk/modules/k1Validator/index.ts @@ -0,0 +1 @@ +export * from "./toK1Validator" diff --git a/src/sdk/modules/k1/toK1.test.ts b/src/sdk/modules/k1Validator/toK1Validator.test.ts similarity index 93% rename from src/sdk/modules/k1/toK1.test.ts rename to src/sdk/modules/k1Validator/toK1Validator.test.ts index 9be791f99..8c62b9440 100644 --- a/src/sdk/modules/k1/toK1.test.ts +++ b/src/sdk/modules/k1Validator/toK1Validator.test.ts @@ -16,12 +16,12 @@ import { toTestClient } from "../../../test/testUtils" import type { MasterClient, NetworkConfig } from "../../../test/testUtils" -import addresses from "../../__contracts/addresses" import { type NexusClient, createNexusClient } from "../../clients/createNexusClient" -import { toK1 } from "./toK1" +import { k1ValidatorAddress } from "../../constants" +import { toK1Validator } from "./toK1Validator" describe("modules.k1Validator", async () => { let network: NetworkConfig @@ -88,7 +88,7 @@ describe("modules.k1Validator", async () => { }, 90000) test("k1Validator properties", async () => { - const k1Validator = toK1({ + const k1Validator = toK1Validator({ signer: nexusClient.account.signer, accountAddress: nexusClient.account.address }) @@ -105,7 +105,7 @@ describe("modules.k1Validator", async () => { const isInstalledBefore = await nexusClient.isModuleInstalled({ module: { type: "validator", - module: addresses.K1Validator, + address: k1ValidatorAddress, initData: encodePacked(["address"], [eoaAccount.address]) } }) @@ -113,7 +113,7 @@ describe("modules.k1Validator", async () => { if (!isInstalledBefore) { const hash = await nexusClient.installModule({ module: { - module: addresses.K1Validator, + address: k1ValidatorAddress, type: "validator", initData: encodePacked(["address"], [eoaAccount.address]) } @@ -127,7 +127,7 @@ describe("modules.k1Validator", async () => { const hashUninstall = nexusClient.uninstallModule({ module: { - module: addresses.K1Validator, + address: k1ValidatorAddress, type: "validator", deInitData } @@ -139,7 +139,7 @@ describe("modules.k1Validator", async () => { const hashUninstall = nexusClient.uninstallModule({ module: { - module: addresses.K1Validator, + address: k1ValidatorAddress, type: "validator", deInitData } diff --git a/src/sdk/modules/k1/toK1.ts b/src/sdk/modules/k1Validator/toK1Validator.ts similarity index 84% rename from src/sdk/modules/k1/toK1.ts rename to src/sdk/modules/k1Validator/toK1Validator.ts index fbf4eddef..ee89d5046 100644 --- a/src/sdk/modules/k1/toK1.ts +++ b/src/sdk/modules/k1Validator/toK1Validator.ts @@ -1,16 +1,16 @@ -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" import { type Address, type Hex, type SignableMessage, encodePacked } from "viem" -import addresses from "../../__contracts/addresses" +import { k1ValidatorAddress } from "../../constants" import { sanitizeSignature } from "../utils/Helpers" -import type { Module } from "../utils/Types" -import { type ToModuleParameters, toModule } from "../utils/toModule" -export type ToK1Parameters = ToModuleParameters & { - address?: Hex +import type { Module, ModuleMeta, ModuleParameters } from "../utils/Types" +import { toModule } from "../utils/toModule" + +export type ToK1ValidatorParameters = Omit & { + address?: ModuleParameters["address"] } export type K1ModuleGetInitDataArgs = { @@ -20,7 +20,7 @@ export type K1ModuleGetInitDataArgs = { export const getK1ModuleInitData = ( _: K1ModuleGetInitDataArgs ): ModuleMeta => ({ - module: addresses.K1Validator, + address: k1ValidatorAddress, type: "validator", initData: "0x" }) @@ -39,7 +39,7 @@ export const getK1InitData = ({ signerAddress }: K1ModuleGetInitDataArgs) => * @returns A promise that resolves to a K1 Validator Module instance. * * @example - * const module = await toK1({ + * const module = await toK1Validator({ * accountAddress: '0x1234...', * client: nexusClient, * initData: '0x...', @@ -51,7 +51,7 @@ export const getK1InitData = ({ signerAddress }: K1ModuleGetInitDataArgs) => * const userOpSignature = await module.signUserOpHash('0x...'); * const messageSignature = await module.signMessage('Hello, world!'); */ -export const toK1 = (parameters: ToK1Parameters): Module => { +export const toK1Validator = (parameters: ToK1ValidatorParameters): Module => { const { signer, initData: initData_, @@ -62,7 +62,7 @@ export const toK1 = (parameters: ToK1Parameters): Module => { moduleInitData: moduleInitData_, deInitData = "0x", accountAddress, - address = addresses.K1Validator + address = k1ValidatorAddress } = parameters const initData = initData_ ?? getK1InitData(initArgs_) diff --git a/src/sdk/modules/ownables/decorators/addOwner.ts b/src/sdk/modules/ownableValidator/decorators/addOwner.ts similarity index 98% rename from src/sdk/modules/ownables/decorators/addOwner.ts rename to src/sdk/modules/ownableValidator/decorators/addOwner.ts index fa1721e18..910d554c7 100644 --- a/src/sdk/modules/ownables/decorators/addOwner.ts +++ b/src/sdk/modules/ownableValidator/decorators/addOwner.ts @@ -54,7 +54,7 @@ export async function addOwner< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/modules/ownables/decorators/getAddOwnerTx.ts b/src/sdk/modules/ownableValidator/decorators/getAddOwnerTx.ts similarity index 97% rename from src/sdk/modules/ownables/decorators/getAddOwnerTx.ts rename to src/sdk/modules/ownableValidator/decorators/getAddOwnerTx.ts index 64244776e..1684b8492 100644 --- a/src/sdk/modules/ownables/decorators/getAddOwnerTx.ts +++ b/src/sdk/modules/ownableValidator/decorators/getAddOwnerTx.ts @@ -45,7 +45,7 @@ export async function getAddOwnerTx< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/modules/ownables/decorators/getOwners.ts b/src/sdk/modules/ownableValidator/decorators/getOwners.ts similarity index 96% rename from src/sdk/modules/ownables/decorators/getOwners.ts rename to src/sdk/modules/ownableValidator/decorators/getOwners.ts index 99c05ec71..dccdd319f 100644 --- a/src/sdk/modules/ownables/decorators/getOwners.ts +++ b/src/sdk/modules/ownableValidator/decorators/getOwners.ts @@ -36,9 +36,10 @@ export async function getOwners< ): Promise { const { account: account_ = client.account } = parameters ?? {} + // Review docspath below. if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/modules/ownables/decorators/getRemoveOwnerTx.ts b/src/sdk/modules/ownableValidator/decorators/getRemoveOwnerTx.ts similarity index 97% rename from src/sdk/modules/ownables/decorators/getRemoveOwnerTx.ts rename to src/sdk/modules/ownableValidator/decorators/getRemoveOwnerTx.ts index 889c08668..d6d1f919c 100644 --- a/src/sdk/modules/ownables/decorators/getRemoveOwnerTx.ts +++ b/src/sdk/modules/ownableValidator/decorators/getRemoveOwnerTx.ts @@ -41,7 +41,7 @@ export async function getRemoveOwnerTx< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/modules/ownables/decorators/getSetThresholdTx.ts b/src/sdk/modules/ownableValidator/decorators/getSetThresholdTx.ts similarity index 97% rename from src/sdk/modules/ownables/decorators/getSetThresholdTx.ts rename to src/sdk/modules/ownableValidator/decorators/getSetThresholdTx.ts index e20052b54..930a51514 100644 --- a/src/sdk/modules/ownables/decorators/getSetThresholdTx.ts +++ b/src/sdk/modules/ownableValidator/decorators/getSetThresholdTx.ts @@ -50,7 +50,7 @@ export async function getSetThresholdTx< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/modules/ownables/decorators/getThreshold.ts b/src/sdk/modules/ownableValidator/decorators/getThreshold.ts similarity index 97% rename from src/sdk/modules/ownables/decorators/getThreshold.ts rename to src/sdk/modules/ownableValidator/decorators/getThreshold.ts index 9a0085d3d..dce90013e 100644 --- a/src/sdk/modules/ownables/decorators/getThreshold.ts +++ b/src/sdk/modules/ownableValidator/decorators/getThreshold.ts @@ -56,7 +56,7 @@ export async function getThreshold< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/modules/ownables/decorators/index.ts b/src/sdk/modules/ownableValidator/decorators/index.ts similarity index 100% rename from src/sdk/modules/ownables/decorators/index.ts rename to src/sdk/modules/ownableValidator/decorators/index.ts diff --git a/src/sdk/modules/ownables/decorators/ownables.decorators.test.ts b/src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts similarity index 96% rename from src/sdk/modules/ownables/decorators/ownables.decorators.test.ts rename to src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts index cb5e4db88..b77c1dbaf 100644 --- a/src/sdk/modules/ownables/decorators/ownables.decorators.test.ts +++ b/src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts @@ -21,7 +21,7 @@ import { type NexusClient, createNexusClient } from "../../../clients/createNexusClient" -import { getOwnablesModuleInitData, toOwnables } from "../toOwnables" +import { toOwnableValidator } from "../toOwnableValidator" describe("modules.ownables.decorators", async () => { let network: NetworkConfig @@ -66,7 +66,7 @@ describe("modules.ownables.decorators", async () => { }) test.concurrent("should batch test ownable decorators", async () => { - const ownableModule = toOwnables({ + const ownableModule = toOwnableValidator({ account: nexusClient.account, signer: eoaAccount, moduleInitArgs: { diff --git a/src/sdk/modules/ownables/decorators/prepareSignatures.ts b/src/sdk/modules/ownableValidator/decorators/prepareSignatures.ts similarity index 94% rename from src/sdk/modules/ownables/decorators/prepareSignatures.ts rename to src/sdk/modules/ownableValidator/decorators/prepareSignatures.ts index 47067e9ad..ea9ddedfa 100644 --- a/src/sdk/modules/ownables/decorators/prepareSignatures.ts +++ b/src/sdk/modules/ownableValidator/decorators/prepareSignatures.ts @@ -20,7 +20,7 @@ export async function prepareSignatures< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/modules/ownables/decorators/removeOwner.ts b/src/sdk/modules/ownableValidator/decorators/removeOwner.ts similarity index 98% rename from src/sdk/modules/ownables/decorators/removeOwner.ts rename to src/sdk/modules/ownableValidator/decorators/removeOwner.ts index c170af3d1..f93d24c8c 100644 --- a/src/sdk/modules/ownables/decorators/removeOwner.ts +++ b/src/sdk/modules/ownableValidator/decorators/removeOwner.ts @@ -65,7 +65,7 @@ export async function removeOwner< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/modules/ownables/decorators/setThreshold.ts b/src/sdk/modules/ownableValidator/decorators/setThreshold.ts similarity index 98% rename from src/sdk/modules/ownables/decorators/setThreshold.ts rename to src/sdk/modules/ownableValidator/decorators/setThreshold.ts index 51853e086..5a5e20aaa 100644 --- a/src/sdk/modules/ownables/decorators/setThreshold.ts +++ b/src/sdk/modules/ownableValidator/decorators/setThreshold.ts @@ -69,7 +69,7 @@ export async function setThreshold< if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } diff --git a/src/sdk/modules/ownableValidator/index.ts b/src/sdk/modules/ownableValidator/index.ts new file mode 100644 index 000000000..b5278c843 --- /dev/null +++ b/src/sdk/modules/ownableValidator/index.ts @@ -0,0 +1,2 @@ +export * from "./decorators" +export * from "./toOwnableValidator" diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.dan.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.dan.test.ts new file mode 100644 index 000000000..d76fbe91d --- /dev/null +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.dan.test.ts @@ -0,0 +1,118 @@ +import { http, type Chain, type LocalAccount } from "viem" +import type { BundlerClient } from "viem/account-abstraction" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import { + fundAndDeployClients, + getTestAccount, + killNetwork, + toTestClient +} from "../../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../../test/testUtils" +import { createNexusClient } from "../../clients/createNexusClient" +import { danActions } from "../../clients/decorators/dan/decorators" +import { keyGen } from "../../clients/decorators/dan/decorators/keyGen" +import { ownableActions } from "./decorators" +import { toOwnableValidator } from "./toOwnableValidator" + +describe("modules.dan.dx", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let eoaAccount: LocalAccount + + const recipient = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" // vitalik.eth + + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = getTestAccount(0) + + testClient = toTestClient(chain, getTestAccount(5)) + }) + + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test("should demonstrate ownables module dx using a dan account", async () => { + const nexusClient = await createNexusClient({ + signer: eoaAccount, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + const danNexusClient = nexusClient.extend(danActions()) + + const keyGenData = await danNexusClient.keyGen() + + // Fund the account and deploy the smart contract wallet + // This is just a reminder to fund the account and deploy the smart contract wallet + await fundAndDeployClients(testClient, [nexusClient]) + + // Create an ownables module with the following configuration: + // - Threshold: 1 (requires 1 signature for approval) + // - Owners: danAccount + const ownableModule = toOwnableValidator({ + account: nexusClient.account, + signer: eoaAccount, + moduleInitArgs: { + threshold: 1n, + owners: [keyGenData.sessionPublicKey] + } + }) + + // Install the ownables module on the Nexus client's smart contract account + const hash = await nexusClient.installModule({ + module: ownableModule.moduleInitData + }) + + // Extend the Nexus client with ownable-specific actions + // This allows the client to use the new module's functionality + const ownableDanClient = nexusClient + .extend(ownableActions(ownableModule)) + .extend(danActions()) + + // Wait for the module installation transaction to be mined and check its success + await ownableDanClient.waitForUserOperationReceipt({ hash }) + + // Prepare a user operation to withdraw 1 wei to userTwo + // This demonstrates a simple transaction that requires multi-sig approval + // @ts-ignore + const withdrawalUserOp = await ownableDanClient.prepareUserOperation({ + calls: [ + { + to: recipient, // vitalik.eth + value: 1n + } + ] + }) + + // Collect signature + const { signature } = await ownableDanClient.sigGen({ + keyGenData, + ...withdrawalUserOp + }) + + if (!signature) throw new Error("Missing signature") + + // Send the user operation with the collected signatures + const userOpHash = await nexusClient.sendUserOperation({ + ...withdrawalUserOp, + signature + }) + + // Wait for the user operation to be mined and check its success + const { success: userOpSuccess } = + await ownableDanClient.waitForUserOperationReceipt({ hash: userOpHash }) + + // Verify that the multi-sig transaction was successful + expect(userOpSuccess).toBe(true) + }) +}) diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts new file mode 100644 index 000000000..60782c902 --- /dev/null +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts @@ -0,0 +1,139 @@ +import { http, type Chain, type LocalAccount } from "viem" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import { + fundAndDeployClients, + getTestAccount, + killNetwork, + toTestClient +} from "../../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../../test/testUtils" +import { createNexusClient } from "../../clients/createNexusClient" +import { ownableActions } from "./decorators" +import { toOwnableValidator } from "./toOwnableValidator" + +describe("modules.ownableValidator.dx", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let eoaAccount: LocalAccount + let userTwo: LocalAccount + let userThree: LocalAccount + + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = getTestAccount(0) + userTwo = getTestAccount(1) + userThree = getTestAccount(2) + + testClient = toTestClient(chain, getTestAccount(5)) + }) + + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test("should demonstrate ownables module dx", async () => { + /** + * This test demonstrates the creation and use of an ownables module for multi-signature functionality: + * + * 1. Setup and Installation: + * - Create a Nexus client for the main account + * - Install the ownables module on the smart contract account + * + * 2. Multi-Signature Transaction: + * - Prepare a user operation (withdrawal) that requires multiple signatures + * - Collect signatures from required owners + * - Execute the multi-sig transaction + * + * This test showcases how the ownables module enables multi-signature functionality + * on a smart contract account, ensuring that certain actions require approval from + * multiple designated owners. + */ + + // Create a Nexus client for the main account (eoaAccount) + // This client will be used to interact with the smart contract account + const nexusClient = await createNexusClient({ + signer: eoaAccount, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + // Fund the account and deploy the smart contract wallet + // This is just a reminder to fund the account and deploy the smart contract wallet + await fundAndDeployClients(testClient, [nexusClient]) + + // Create an ownables module with the following configuration: + // - Threshold: 2 (requires 2 signatures for approval) + // - Owners: userThree and userTwo + const ownableModule = toOwnableValidator({ + account: nexusClient.account, + signer: eoaAccount, + moduleInitArgs: { + threshold: 2n, + owners: [userThree.address, userTwo.address] + } + }) + + // Install the ownables module on the Nexus client's smart contract account + const hash = await nexusClient.installModule({ + module: ownableModule.moduleInitData + }) + + // Extend the Nexus client with ownable-specific actions + // This allows the client to use the new module's functionality + const ownableNexusClient = nexusClient.extend(ownableActions(ownableModule)) + + // Wait for the module installation transaction to be mined and check its success + await ownableNexusClient.waitForUserOperationReceipt({ hash }) + + // Prepare a user operation to withdraw 1 wei to userTwo + // This demonstrates a simple transaction that requires multi-sig approval + // @ts-ignore + const withdrawalUserOp = await ownableNexusClient.prepareUserOperation({ + calls: [ + { + to: userTwo.address, + value: 1n + } + ] + }) + + // Get the hash of the user operation + // This hash will be signed by the required owners + const withdrawalUserOpHash = + // @ts-ignore + await nexusClient.account.getUserOpHash(withdrawalUserOp) + + // Collect signatures from both required owners (userTwo and userThree) + const signatures = await Promise.all( + [userTwo, userThree].map(async (signer) => { + return signer.signMessage({ + message: { raw: withdrawalUserOpHash } + }) + }) + ) + + // Combine the signatures and set them on the user operation + // The order of signatures should match the order of owners in the module configuration + withdrawalUserOp.signature = await ownableNexusClient.prepareSignatures({ + signatures + }) + // Send the user operation with the collected signatures + const userOpHash = await nexusClient.sendUserOperation(withdrawalUserOp) + + // Wait for the user operation to be mined and check its success + const { success: userOpSuccess } = + await ownableNexusClient.waitForUserOperationReceipt({ hash: userOpHash }) + + // Verify that the multi-sig transaction was successful + expect(userOpSuccess).toBe(true) + }) +}) diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts new file mode 100644 index 000000000..48c2c3c41 --- /dev/null +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts @@ -0,0 +1,173 @@ +import { + getAddOwnableExecutorOwnerAction, + getExecuteOnOwnedAccountAction +} from "@rhinestone/module-sdk" +import { + http, + type Account, + type Address, + type Chain, + type Hex, + type PublicClient, + type WalletClient, + encodePacked, + parseAbi, + toHex, + zeroAddress +} from "viem" +import { waitForTransactionReceipt } from "viem/actions" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { testAddresses } from "../../../test/callDatas" +import { toNetwork } from "../../../test/testSetup" +import { + fundAndDeployClients, + getTestAccount, + killNetwork, + toTestClient +} from "../../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../../test/testUtils" +import { + type NexusClient, + createNexusClient +} from "../../clients/createNexusClient" +import { moduleActivator } from "../../clients/decorators/erc7579/moduleActivator" +import { toK1Validator } from "../k1Validator/toK1Validator" +import type { Module } from "../utils/Types" + +describe("modules.ownableExecutor", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let eoaAccount: Account + let nexusClient: NexusClient + let nexusAccountAddress: Address + let recipient: Account + let recipientAddress: Address + let k1Module: Module + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = getTestAccount(0) + recipient = getTestAccount(1) + recipientAddress = recipient.address + + testClient = toTestClient(chain, getTestAccount(5)) + + nexusClient = await createNexusClient({ + signer: eoaAccount, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() + await fundAndDeployClients(testClient, [nexusClient]) + + const k1Module = toK1Validator({ + signer: eoaAccount, + accountAddress: nexusClient.account.address + }) + + nexusClient.extend(moduleActivator(k1Module)) + }) + + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test("should install OwnableExecutor module", async () => { + const isInstalled = await nexusClient.isModuleInstalled({ + module: { + type: "executor", + address: testAddresses.OwnableExecutor + } + }) + expect(isInstalled).toBe(false) + + const userOpHash = await nexusClient.installModule({ + module: { + type: "executor", + address: testAddresses.OwnableExecutor, + initData: encodePacked(["address"], [eoaAccount.address]) + } + }) + expect(userOpHash).toBeDefined() + const receipt = await nexusClient.waitForUserOperationReceipt({ + hash: userOpHash + }) + expect(receipt.success).toBe(true) + + const isInstalledAfter = await nexusClient.isModuleInstalled({ + module: { + type: "executor", + address: testAddresses.OwnableExecutor + } + }) + expect(isInstalledAfter).toBe(true) + }) + + test("should add another EOA as executor", async () => { + const execution = await getAddOwnableExecutorOwnerAction({ + owner: recipientAddress, + client: nexusClient.account.client as PublicClient, + account: { + address: nexusClient.account.address, + type: "nexus", + deployedOnChains: [] + } + }) + const userOpHash = await nexusClient.sendTransaction({ + calls: [ + { + to: testAddresses.OwnableExecutor, + data: execution.callData, + value: 0n + } + ] + }) + expect(userOpHash).toBeDefined() + const masterClient = nexusClient.account.client as MasterClient + const owners = await masterClient.readContract({ + address: testAddresses.OwnableExecutor, + abi: parseAbi([ + "function getOwners(address account) external view returns (address[])" + ]), + functionName: "getOwners", + args: [nexusClient.account.address] + }) + expect(owners).toContain(recipientAddress) + }) + + test("added executor EOA should execute user operation on smart account", async () => { + const executeOnOwnedAccountExecution = getExecuteOnOwnedAccountAction({ + execution: { + target: zeroAddress, + callData: toHex("0x"), + value: 0n, + to: zeroAddress, + data: "0x" + }, + ownedAccount: nexusClient.account.address + }) + + const client = nexusClient.account.client as WalletClient + const hash = await client.sendTransaction({ + account: recipient, + to: testAddresses.OwnableExecutor, + data: executeOnOwnedAccountExecution.callData, + chain, + value: 0n + }) + + const receipt = await waitForTransactionReceipt( + nexusClient.account.client as PublicClient, + { hash } + ) + expect(receipt.status).toBe("success") + }) +}) diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts new file mode 100644 index 000000000..8724302d9 --- /dev/null +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts @@ -0,0 +1,304 @@ +import { getOwnableValidatorSignature } from "@rhinestone/module-sdk" +import { + http, + type Account, + type Address, + type Chain, + type Hex, + type LocalAccount, + encodeAbiParameters, + encodeFunctionData, + encodePacked, + getAddress, + zeroAddress +} from "viem" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import { + fundAndDeployClients, + getTestAccount, + killNetwork, + toTestClient +} from "../../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../../test/testUtils" +import type { NexusAccount } from "../../account" +import { + type NexusClient, + createNexusClient +} from "../../clients/createNexusClient" +import { parseModuleTypeId } from "../../clients/decorators/erc7579/supportsModule" +import { k1ValidatorAddress } from "../../constants" +import type { Module } from "../utils/Types" +import { type OwnableActions, ownableActions } from "./decorators" +import { toOwnableValidator } from "./toOwnableValidator" + +describe("modules.ownables", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let eoaAccount: Account + let nexusClient: NexusClient + let ownableNexusClient: NexusClient & OwnableActions + let recipient: LocalAccount + let recipientAddress: Address + let userThree: LocalAccount + let userThreeAddress: Address + let ownableModule: Module + + beforeAll(async () => { + network = await toNetwork() + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = getTestAccount(0) + recipient = getTestAccount(1) + userThree = getTestAccount(2) + + recipientAddress = recipient.address + userThreeAddress = userThree.address + testClient = toTestClient(chain, getTestAccount(5)) + + nexusClient = await createNexusClient({ + signer: eoaAccount, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + await fundAndDeployClients(testClient, [nexusClient]) + + ownableModule = toOwnableValidator({ + account: nexusClient.account, + signer: eoaAccount, + moduleInitArgs: { + threshold: 1n, + owners: [eoaAccount.address] + } + }) + }) + + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test("should install ownable validator and perform operations", async () => { + const installHash = await nexusClient.installModule({ + module: ownableModule.moduleInitData + }) + + // @ts-ignore + ownableNexusClient = nexusClient.extend(ownableActions(ownableModule)) + + const { success: installSuccess } = + await ownableNexusClient.waitForUserOperationReceipt({ + hash: installHash + }) + + expect(installSuccess).toBe(true) + }) + + test("should add accountTwo as owner", async () => { + const hash = await ownableNexusClient.addOwner({ + owner: userThreeAddress + }) + expect(hash).toBeDefined() + const receipt = await ownableNexusClient.waitForUserOperationReceipt({ + hash + }) + expect(receipt.success).toBe(true) + + const owners = await ownableNexusClient.getOwners() + expect(owners).toContain(userThreeAddress) + }) + + test("should remove an owner", async () => { + const removeOwnerTx = await ownableNexusClient.getRemoveOwnerTx({ + owner: userThreeAddress + }) + + const userOp = await nexusClient.prepareUserOperation({ + calls: [removeOwnerTx] + }) + const dummyUserOpHash = await nexusClient.account.getUserOpHash(userOp) + + const signature1 = await eoaAccount?.signMessage?.({ + message: { raw: dummyUserOpHash } + }) + const signature2 = await recipient?.signMessage?.({ + message: { raw: dummyUserOpHash } + }) + const multiSignature = encodePacked( + ["bytes", "bytes"], + [signature1 ?? "0x", signature2 ?? "0x"] + ) + userOp.signature = multiSignature + const userOpHash = await nexusClient.sendUserOperation(userOp) + // @note Can also use the removeOwner decorator but it requires a signature override and the user op nonce, + // otherwise it will try to use a different nonce and siganture will be invalid + // const userOpHash = await ownableNexusClient.removeOwner({ + // account: nexusClient.account, + // owner: recipientAddress, + // signatureOverride: multiSignature, + // nonce: userOp.nonce + // }) + expect(userOpHash).toBeDefined() + const { success: userOpSuccess } = + await nexusClient.waitForUserOperationReceipt({ hash: userOpHash }) + expect(userOpSuccess).toBe(true) + }) + + test("should add owner and set threshold to 2", async () => { + const isInstalled = await nexusClient.isModuleInstalled({ + module: { + address: ownableModule.address, + type: "validator" + } + }) + expect(isInstalled).toBe(true) + + // Add owner + const userOpHash1 = await ownableNexusClient.addOwner({ + owner: recipientAddress + }) + expect(userOpHash1).toBeDefined() + const receipt = await ownableNexusClient.waitForUserOperationReceipt({ + hash: userOpHash1 + }) + expect(receipt.success).toBe(true) + + // Set threshold + const userOpHash2 = await ownableNexusClient.setThreshold({ + threshold: 2 + }) + expect(userOpHash2).toBeDefined() + const { success: userOpSuccess } = + await nexusClient.waitForUserOperationReceipt({ hash: userOpHash2 }) + expect(userOpSuccess).toBe(true) + const newThreshold = await ownableNexusClient.getThreshold() + expect(newThreshold).toBe(2) + }, 90000) + + test("should require 2 signatures to send user operation", async () => { + expect(nexusClient.account.getModule().address).toBe(ownableModule.address) + + const userOp = await nexusClient.prepareUserOperation({ + calls: [ + { + to: zeroAddress, + data: "0x" + } + ] + }) + + const userOpHash = await nexusClient.account.getUserOpHash(userOp) + const signature1 = await eoaAccount?.signMessage?.({ + message: { raw: userOpHash } + }) + const signature2 = await recipient?.signMessage?.({ + message: { raw: userOpHash } + }) + const multiSignature = encodePacked( + ["bytes", "bytes"], + [signature1 ?? "0x", signature2 ?? "0x"] + ) + userOp.signature = multiSignature + const userOperationHashResponse = + await nexusClient.sendUserOperation(userOp) + expect(userOpHash).toBeDefined() + const { success: userOpSuccess } = + await nexusClient.waitForUserOperationReceipt({ + hash: userOperationHashResponse + }) + expect(userOpSuccess).toBe(true) + }) + + test("should uninstall ownable validator with 2 signatures", async () => { + const [installedValidators] = await nexusClient.getInstalledValidators() + const prevModule = await nexusClient.getPreviousModule({ + module: { + address: ownableModule.address, + type: "validator" + }, + installedValidators + }) + const deInitData = encodeAbiParameters( + [ + { name: "prev", type: "address" }, + { name: "disableModuleData", type: "bytes" } + ], + [prevModule, "0x"] + ) + const uninstallCallData = 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("validator"), + getAddress(ownableModule.address), + deInitData + ] + }) + + const userOp = await nexusClient.prepareUserOperation({ + calls: [ + { + to: nexusClient.account.address, + data: uninstallCallData + } + ] + }) + const userOpHash = await nexusClient.account.getUserOpHash(userOp) + expect(userOpHash).toBeDefined() + + const signature1 = await eoaAccount?.signMessage?.({ + message: { raw: userOpHash } + }) + const signature2 = (await recipient?.signMessage?.({ + message: { raw: userOpHash } + })) as Hex + const multiSignature = getOwnableValidatorSignature({ + signatures: [signature1 ?? "0x", signature2 ?? "0x"] + }) + userOp.signature = multiSignature + const uninstallHash = await nexusClient.sendUserOperation(userOp) + // const uninstallHash = await nexusClient.uninstallModule({ + // module: { + // address: ownableModule.address, + // type: "validator", + // data: "0x" + // }, + // signatureOverride: multiSignature, + // nonce: userOp.nonce + // }) + expect(uninstallHash).toBeDefined() + const { success: userOpSuccess } = + await nexusClient.waitForUserOperationReceipt({ hash: uninstallHash }) + expect(userOpSuccess).toBe(true) + const [installedValidatorsAfter] = + await nexusClient.getInstalledValidators() + expect(installedValidatorsAfter).toEqual([k1ValidatorAddress]) + }) +}) diff --git a/src/sdk/modules/ownables/toOwnables.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.ts similarity index 84% rename from src/sdk/modules/ownables/toOwnables.ts rename to src/sdk/modules/ownableValidator/toOwnableValidator.ts index d2cf1bde5..804766a88 100644 --- a/src/sdk/modules/ownables/toOwnables.ts +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.ts @@ -5,7 +5,6 @@ import { getOwnableValidatorThreshold, isModuleInstalled } from "@rhinestone/module-sdk" -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" import { type Address, type Hex, @@ -16,15 +15,21 @@ import { import type { ModularSmartAccount, Module, + ModuleMeta, ModuleParameters } from "../utils/Types" -import { type ToModuleParameters, toModule } from "../utils/toModule" +import { toModule } from "../utils/toModule" /** * Parameters for creating an Ownable module. - * Extends ToModuleParameters but replaces 'accountAddress' with 'account'. + * Extends ModuleParameters but replaces 'accountAddress' with 'account'. */ -type ToOwnableModuleParameters = Omit & { +type ToOwnableValidatorModuleParameters = Omit< + ModuleParameters, + "accountAddress" | "address" +> & { + /** The address of the modular smart account to associate with this module. */ + address?: Hex /** The modular smart account to associate with this module. */ account: ModularSmartAccount /** Optional initialization arguments for the module. */ @@ -57,7 +62,7 @@ export type OwnableModuleParameters = ModuleParameters export const getOwnablesModuleInitData = ( parameters: GetOwnablesModuleInitDataParams ): ModuleMeta => ({ - module: OWNABLE_VALIDATOR_ADDRESS, + address: OWNABLE_VALIDATOR_ADDRESS, type: "validator", initData: encodeAbiParameters( [ @@ -89,7 +94,7 @@ export const getOwnablesInitData = (_?: GetOwnablesModuleInitDataParams): Hex => * * @example * ```typescript - * const ownableModule = toOwnables({ + * const ownableModule = toOwnableValidator({ * account: mySmartAccount, * signer: mySigner, * moduleInitArgs: { @@ -104,7 +109,9 @@ export const getOwnablesInitData = (_?: GetOwnablesModuleInitDataParams): Hex => * - If not installed, it will use the threshold from the initialization parameters. * - The function generates a mock signature based on the threshold. */ -export const toOwnables = (parameters: ToOwnableModuleParameters): Module => { +export const toOwnableValidator = ( + parameters: ToOwnableValidatorModuleParameters +): Module => { const { account, signer, @@ -137,7 +144,14 @@ export const toOwnables = (parameters: ToOwnableModuleParameters): Module => { const isInstalled = await isModuleInstalled({ account: nexusAccount, client: client as PublicClient, - module: { module: OWNABLE_VALIDATOR_ADDRESS, type: "validator" } + module: { + address: OWNABLE_VALIDATOR_ADDRESS, + type: "validator", + module: OWNABLE_VALIDATOR_ADDRESS, + initData: "0x", + deInitData: "0x", + additionalContext: "0x" + } }) let threshold: number if (isInstalled) { diff --git a/src/sdk/modules/ownables/toOwnables.test.ts b/src/sdk/modules/ownables/toOwnables.test.ts deleted file mode 100644 index eff9c6160..000000000 --- a/src/sdk/modules/ownables/toOwnables.test.ts +++ /dev/null @@ -1,581 +0,0 @@ -import { - getAddOwnableExecutorOwnerAction, - getExecuteOnOwnedAccountAction, - getOwnableValidatorSignature -} from "@rhinestone/module-sdk" -import { - http, - type Account, - type Address, - type Chain, - type Hex, - type LocalAccount, - type PublicClient, - type WalletClient, - encodeAbiParameters, - encodeFunctionData, - encodePacked, - getAddress, - parseAbi, - toHex, - zeroAddress -} from "viem" -import { waitForTransactionReceipt } from "viem/actions" -import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { testAddresses } from "../../../test/callDatas" -import { toNetwork } from "../../../test/testSetup" -import { - fundAndDeployClients, - getTestAccount, - killNetwork, - toTestClient -} from "../../../test/testUtils" -import type { MasterClient, NetworkConfig } from "../../../test/testUtils" -import addresses from "../../__contracts/addresses" -import type { NexusAccount } from "../../account" -import { - type NexusClient, - createNexusClient -} from "../../clients/createNexusClient" -import { parseModuleTypeId } from "../../clients/decorators/erc7579/supportsModule" -import { toK1 } from "../k1/toK1" -import type { Module } from "../utils/Types" -import { type OwnableActions, ownableActions } from "./decorators" -import { toOwnables } from "./toOwnables" - -describe("modules.ownableValidator.dx", async () => { - let network: NetworkConfig - let chain: Chain - let bundlerUrl: string - - // Test utils - let testClient: MasterClient - let eoaAccount: LocalAccount - let userTwo: LocalAccount - let userThree: LocalAccount - - beforeAll(async () => { - network = await toNetwork() - - chain = network.chain - bundlerUrl = network.bundlerUrl - eoaAccount = getTestAccount(0) - userTwo = getTestAccount(1) - userThree = getTestAccount(2) - - testClient = toTestClient(chain, getTestAccount(5)) - }) - - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should demonstrate ownables module dx", async () => { - /** - * This test demonstrates the creation and use of an ownables module for multi-signature functionality: - * - * 1. Setup and Installation: - * - Create a Nexus client for the main account - * - Install the ownables module on the smart contract account - * - * 2. Multi-Signature Transaction: - * - Prepare a user operation (withdrawal) that requires multiple signatures - * - Collect signatures from required owners - * - Execute the multi-sig transaction - * - * This test showcases how the ownables module enables multi-signature functionality - * on a smart contract account, ensuring that certain actions require approval from - * multiple designated owners. - */ - - // Create a Nexus client for the main account (eoaAccount) - // This client will be used to interact with the smart contract account - const nexusClient = await createNexusClient({ - signer: eoaAccount, - chain, - transport: http(), - bundlerTransport: http(bundlerUrl) - }) - - // Fund the account and deploy the smart contract wallet - // This is just a reminder to fund the account and deploy the smart contract wallet - await fundAndDeployClients(testClient, [nexusClient]) - - // Create an ownables module with the following configuration: - // - Threshold: 2 (requires 2 signatures for approval) - // - Owners: userThree and userTwo - const ownableModule = toOwnables({ - account: nexusClient.account, - signer: eoaAccount, - moduleInitArgs: { - threshold: 2n, - owners: [userThree.address, userTwo.address] - } - }) - - // Install the ownables module on the Nexus client's smart contract account - const hash = await nexusClient.installModule({ - module: ownableModule.moduleInitData - }) - - // Extend the Nexus client with ownable-specific actions - // This allows the client to use the new module's functionality - const ownableNexusClient = nexusClient.extend(ownableActions(ownableModule)) - - // Wait for the module installation transaction to be mined and check its success - const { success } = await ownableNexusClient.waitForUserOperationReceipt({ - hash - }) - - // Prepare a user operation to withdraw 1 wei to userTwo - // This demonstrates a simple transaction that requires multi-sig approval - // @ts-ignore - const withdrawalUserOp = await ownableNexusClient.prepareUserOperation({ - calls: [ - { - to: userTwo.address, - value: 1n - } - ] - }) - - // Get the hash of the user operation - // This hash will be signed by the required owners - const withdrawalUserOpHash = - // @ts-ignore - await nexusClient.account.getUserOpHash(withdrawalUserOp) - - // Collect signatures from both required owners (userTwo and userThree) - const signatures = await Promise.all( - [userTwo, userThree].map(async (signer) => { - return signer.signMessage({ - message: { raw: withdrawalUserOpHash } - }) - }) - ) - - // Combine the signatures and set them on the user operation - // The order of signatures should match the order of owners in the module configuration - withdrawalUserOp.signature = await ownableNexusClient.prepareSignatures({ - signatures - }) - // Send the user operation with the collected signatures - const userOpHash = await nexusClient.sendUserOperation(withdrawalUserOp) - - // Wait for the user operation to be mined and check its success - const { success: userOpSuccess } = - await ownableNexusClient.waitForUserOperationReceipt({ hash: userOpHash }) - - // Verify that the multi-sig transaction was successful - expect(userOpSuccess).toBe(true) - }) -}) - -describe("modules.ownables", async () => { - let network: NetworkConfig - let chain: Chain - let bundlerUrl: string - - // Test utils - let testClient: MasterClient - let eoaAccount: Account - let nexusClient: NexusClient - let ownableNexusClient: NexusClient & OwnableActions - let recipient: LocalAccount - let recipientAddress: Address - let userThree: LocalAccount - let userThreeAddress: Address - let ownableModule: Module - - beforeAll(async () => { - network = await toNetwork() - - chain = network.chain - bundlerUrl = network.bundlerUrl - eoaAccount = getTestAccount(0) - recipient = getTestAccount(1) - userThree = getTestAccount(2) - - recipientAddress = recipient.address - userThreeAddress = userThree.address - testClient = toTestClient(chain, getTestAccount(5)) - - nexusClient = await createNexusClient({ - signer: eoaAccount, - chain, - transport: http(), - bundlerTransport: http(bundlerUrl) - }) - - await fundAndDeployClients(testClient, [nexusClient]) - - ownableModule = toOwnables({ - account: nexusClient.account, - signer: eoaAccount, - moduleInitArgs: { - threshold: 1n, - owners: [eoaAccount.address] - } - }) - }) - - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should install ownable validator and perform operations", async () => { - const installHash = await nexusClient.installModule({ - module: ownableModule.moduleInitData - }) - - // @ts-ignore - ownableNexusClient = nexusClient.extend(ownableActions(ownableModule)) - - const { success: installSuccess } = - await ownableNexusClient.waitForUserOperationReceipt({ - hash: installHash - }) - - expect(installSuccess).toBe(true) - }) - - test("should add accountTwo as owner", async () => { - const hash = await ownableNexusClient.addOwner({ - owner: userThreeAddress - }) - expect(hash).toBeDefined() - const receipt = await ownableNexusClient.waitForUserOperationReceipt({ - hash - }) - expect(receipt.success).toBe(true) - - const owners = await ownableNexusClient.getOwners() - expect(owners).toContain(userThreeAddress) - }) - - test("should remove an owner", async () => { - const removeOwnerTx = await ownableNexusClient.getRemoveOwnerTx({ - owner: userThreeAddress - }) - - const userOp = await nexusClient.prepareUserOperation({ - calls: [removeOwnerTx] - }) - const dummyUserOpHash = await nexusClient.account.getUserOpHash(userOp) - - const signature1 = await eoaAccount?.signMessage?.({ - message: { raw: dummyUserOpHash } - }) - const signature2 = await recipient?.signMessage?.({ - message: { raw: dummyUserOpHash } - }) - const multiSignature = encodePacked( - ["bytes", "bytes"], - [signature1 ?? "0x", signature2 ?? "0x"] - ) - userOp.signature = multiSignature - const userOpHash = await nexusClient.sendUserOperation(userOp) - // @note Can also use the removeOwner decorator but it requires a signature override and the user op nonce, - // otherwise it will try to use a different nonce and siganture will be invalid - // const userOpHash = await ownableNexusClient.removeOwner({ - // account: nexusClient.account, - // owner: recipientAddress, - // signatureOverride: multiSignature, - // nonce: userOp.nonce - // }) - expect(userOpHash).toBeDefined() - const { success: userOpSuccess } = - await nexusClient.waitForUserOperationReceipt({ hash: userOpHash }) - expect(userOpSuccess).toBe(true) - }) - - test("should add owner and set threshold to 2", async () => { - const isInstalled = await nexusClient.isModuleInstalled({ - module: { - module: ownableModule.address, - type: "validator" - } - }) - expect(isInstalled).toBe(true) - - // Add owner - const userOpHash1 = await ownableNexusClient.addOwner({ - owner: recipientAddress - }) - expect(userOpHash1).toBeDefined() - const receipt = await ownableNexusClient.waitForUserOperationReceipt({ - hash: userOpHash1 - }) - expect(receipt.success).toBe(true) - - // Set threshold - const userOpHash2 = await ownableNexusClient.setThreshold({ - threshold: 2 - }) - expect(userOpHash2).toBeDefined() - const { success: userOpSuccess } = - await nexusClient.waitForUserOperationReceipt({ hash: userOpHash2 }) - expect(userOpSuccess).toBe(true) - const newThreshold = await ownableNexusClient.getThreshold() - expect(newThreshold).toBe(2) - }, 90000) - - test("should require 2 signatures to send user operation", async () => { - expect(nexusClient.account.getModule().address).toBe(ownableModule.address) - - const userOp = await nexusClient.prepareUserOperation({ - calls: [ - { - to: zeroAddress, - data: "0x" - } - ] - }) - - const userOpHash = await nexusClient.account.getUserOpHash(userOp) - const signature1 = await eoaAccount?.signMessage?.({ - message: { raw: userOpHash } - }) - const signature2 = await recipient?.signMessage?.({ - message: { raw: userOpHash } - }) - const multiSignature = encodePacked( - ["bytes", "bytes"], - [signature1 ?? "0x", signature2 ?? "0x"] - ) - userOp.signature = multiSignature - const userOperationHashResponse = - await nexusClient.sendUserOperation(userOp) - expect(userOpHash).toBeDefined() - const { success: userOpSuccess } = - await nexusClient.waitForUserOperationReceipt({ - hash: userOperationHashResponse - }) - expect(userOpSuccess).toBe(true) - }) - - test("should uninstall ownable validator with 2 signatures", async () => { - const [installedValidators] = await nexusClient.getInstalledValidators() - const prevModule = await nexusClient.getPreviousModule({ - module: { - module: ownableModule.address, - type: "validator" - }, - installedValidators - }) - const deInitData = encodeAbiParameters( - [ - { name: "prev", type: "address" }, - { name: "disableModuleData", type: "bytes" } - ], - [prevModule, "0x"] - ) - const uninstallCallData = 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("validator"), - getAddress(ownableModule.address), - deInitData - ] - }) - - const userOp = await nexusClient.prepareUserOperation({ - calls: [ - { - to: nexusClient.account.address, - data: uninstallCallData - } - ] - }) - const userOpHash = await nexusClient.account.getUserOpHash(userOp) - expect(userOpHash).toBeDefined() - - const signature1 = await eoaAccount?.signMessage?.({ - message: { raw: userOpHash } - }) - const signature2 = (await recipient?.signMessage?.({ - message: { raw: userOpHash } - })) as Hex - const multiSignature = getOwnableValidatorSignature({ - signatures: [signature1 ?? "0x", signature2 ?? "0x"] - }) - userOp.signature = multiSignature - const uninstallHash = await nexusClient.sendUserOperation(userOp) - // const uninstallHash = await nexusClient.uninstallModule({ - // module: { - // address: ownableModule.address, - // type: "validator", - // data: "0x" - // }, - // signatureOverride: multiSignature, - // nonce: userOp.nonce - // }) - expect(uninstallHash).toBeDefined() - const { success: userOpSuccess } = - await nexusClient.waitForUserOperationReceipt({ hash: uninstallHash }) - expect(userOpSuccess).toBe(true) - const [installedValidatorsAfter] = - await nexusClient.getInstalledValidators() - expect(installedValidatorsAfter).toEqual([addresses.K1Validator]) - }) -}) - -describe("modules.ownableExecutor", async () => { - let network: NetworkConfig - let chain: Chain - let bundlerUrl: string - - // Test utils - let testClient: MasterClient - let eoaAccount: Account - let nexusClient: NexusClient - let nexusAccountAddress: Address - let recipient: Account - let recipientAddress: Address - let k1Module: Module - beforeAll(async () => { - network = await toNetwork() - - chain = network.chain - bundlerUrl = network.bundlerUrl - eoaAccount = getTestAccount(0) - recipient = getTestAccount(1) - recipientAddress = recipient.address - - testClient = toTestClient(chain, getTestAccount(5)) - - nexusClient = await createNexusClient({ - signer: eoaAccount, - chain, - transport: http(), - bundlerTransport: http(bundlerUrl) - }) - - nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() - await fundAndDeployClients(testClient, [nexusClient]) - - const k1Module = toK1({ - signer: eoaAccount, - accountAddress: nexusClient.account.address - }) - - nexusClient.account.setModule(k1Module) - }) - - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should install OwnableExecutor module", async () => { - const isInstalled = await nexusClient.isModuleInstalled({ - module: { - type: "executor", - module: testAddresses.OwnableExecutor - } - }) - expect(isInstalled).toBe(false) - - const userOpHash = await nexusClient.installModule({ - module: { - type: "executor", - module: testAddresses.OwnableExecutor, - initData: encodePacked(["address"], [eoaAccount.address]) - } - }) - expect(userOpHash).toBeDefined() - const receipt = await nexusClient.waitForUserOperationReceipt({ - hash: userOpHash - }) - expect(receipt.success).toBe(true) - - const isInstalledAfter = await nexusClient.isModuleInstalled({ - module: { - type: "executor", - module: testAddresses.OwnableExecutor - } - }) - expect(isInstalledAfter).toBe(true) - }) - - test("should add another EOA as executor", async () => { - const execution = await getAddOwnableExecutorOwnerAction({ - owner: recipientAddress, - client: nexusClient.account.client as PublicClient, - account: { - address: nexusClient.account.address, - type: "nexus", - deployedOnChains: [] - } - }) - const userOpHash = await nexusClient.sendTransaction({ - calls: [ - { - to: testAddresses.OwnableExecutor, - data: execution.callData, - value: 0n - } - ] - }) - expect(userOpHash).toBeDefined() - const masterClient = nexusClient.account.client as MasterClient - const owners = await masterClient.readContract({ - address: testAddresses.OwnableExecutor, - abi: parseAbi([ - "function getOwners(address account) external view returns (address[])" - ]), - functionName: "getOwners", - args: [nexusClient.account.address] - }) - expect(owners).toContain(recipientAddress) - }) - - test("added executor EOA should execute user operation on smart account", async () => { - const execution = { - target: zeroAddress, - callData: toHex("0x"), - value: 0n - } - - const executeOnOwnedAccountExecution = getExecuteOnOwnedAccountAction({ - execution, - ownedAccount: nexusClient.account.address - }) - - const client = nexusClient.account.client as WalletClient - const hash = await client.sendTransaction({ - account: recipient, - to: testAddresses.OwnableExecutor, - data: executeOnOwnedAccountExecution.callData, - chain, - value: 0n - }) - - const receipt = await waitForTransactionReceipt( - nexusClient.account.client as PublicClient, - { hash } - ) - expect(receipt.status).toBe("success") - }) -}) diff --git a/src/sdk/modules/smartSessions/smartSessions.test.ts b/src/sdk/modules/smartSessions/smartSessions.test.ts deleted file mode 100644 index 9073699f3..000000000 --- a/src/sdk/modules/smartSessions/smartSessions.test.ts +++ /dev/null @@ -1,780 +0,0 @@ -import { SmartSessionMode } from "@rhinestone/module-sdk/module" -import { - http, - type AbiFunction, - type Address, - type Chain, - type Hex, - type LocalAccount, - type PublicClient, - encodeFunctionData, - getContract, - pad, - slice, - toBytes, - toFunctionSelector, - toHex -} from "viem" -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" -import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { CounterAbi } from "../../../test/__contracts/abi/CounterAbi" -import { MockCalleeAbi } from "../../../test/__contracts/abi/MockCalleeAbi" -import { TEST_CONTRACTS } from "../../../test/callDatas" -import { testAddresses } from "../../../test/callDatas" -import { toNetwork } from "../../../test/testSetup" -import { - fundAndDeployClients, - 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" -import { createNexusSessionClient } from "../../clients/createNexusSessionClient" -import { parseReferenceValue } from "../utils/Helpers" -import type { ModularSmartAccount, Module } from "../utils/Types" -import policies, { - isSessionEnabled, - unzipSessionData, - zipSessionData -} from "./Helpers" -import type { CreateSessionDataParams, Rule, SessionData } from "./Types" -import { ParamCondition } from "./Types" -import { smartSessionCreateActions, smartSessionUseActions } from "./decorators" -import { toSmartSessions } from "./toSmartSessions" - -describe("modules.smartSessions.dx", async () => { - let network: NetworkConfig - let chain: Chain - let bundlerUrl: string - - // Test utils - let testClient: MasterClient - let eoaAccount: LocalAccount - let usersNexusClient: NexusClient - let cachedPermissionId: Hex - let sessionKeyAccount: LocalAccount - let sessionPublicKey: Address - - let zippedSessionDatum: string - let sessionsModule: Module - - beforeAll(async () => { - network = await toNetwork() - - chain = network.chain - bundlerUrl = network.bundlerUrl - eoaAccount = getTestAccount(0) - sessionKeyAccount = privateKeyToAccount(generatePrivateKey()) // Generally belongs to the dapp - sessionPublicKey = sessionKeyAccount.address - testClient = toTestClient(chain, getTestAccount(5)) - }) - - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - /** - * This test demonstrates the creation and use of a smart session from two perspectives: - * - * 1. User Perspective (first test): - * - Create a Nexus client for the user's account - * - Install the smart sessions module on the user's account - * - Create a smart session with specific permissions - * - * 2. Dapp Perspective (second test): - * - Simulate a scenario where the user has left the dapp - * - Create a new Nexus client using the session key - * - Use the session to perform actions on behalf of the user - * - * This test showcases how smart sessions enable controlled, delegated actions - * on a user's smart account, even after the user is no longer actively engaged. - */ - test("should demonstrate creating a smart session from user's perspective", async () => { - // User Perspective: Creating and setting up the smart session - - // Create a Nexus client for the main account (eoaAccount) - // This client will be used to interact with the smart contract account - usersNexusClient = await createNexusClient({ - signer: eoaAccount, - chain, - transport: http(), - bundlerTransport: http(bundlerUrl) - }) - - // Fund the account and deploy the smart contract wallet - await fundAndDeployClients(testClient, [usersNexusClient]) - - // Create a smart sessions module for the user's account - sessionsModule = toSmartSessions({ - account: usersNexusClient.account, - signer: eoaAccount - }) - - // Install the smart sessions module on the Nexus client's smart contract account - const hash = await usersNexusClient.installModule({ - module: sessionsModule.moduleInitData - }) - - // Wait for the module installation transaction to be mined and check its success - const { success: installSuccess } = - await usersNexusClient.waitForUserOperationReceipt({ hash }) - - expect(installSuccess).toBe(true) - - // Define the session parameters - // This includes the session key, validator, and action policies - const sessionRequestedInfo: CreateSessionDataParams[] = [ - { - sessionPublicKey, // Public key of the session - sessionValidatorAddress: TEST_CONTRACTS.SimpleSessionValidator.address, - sessionKeyData: toHex(toBytes(sessionPublicKey)), - sessionValidAfter: 0, // Session valid immediately - sessionValidUntil: 0, // Session valid indefinitely - actionPoliciesInfo: [ - { - contractAddress: TEST_CONTRACTS.Counter.address, - functionSelector: "0x273ea3e3" as Hex, // Selector for 'incrementNumber' - validUntil: 0, // Policy valid indefinitely - validAfter: 0, // Policy valid immediately - rules: [], // No additional rules - valueLimit: BigInt(0) // No value limit - } - ] - } - ] - - // Extend the Nexus client with smart session creation actions - const nexusSessionClient = usersNexusClient.extend( - smartSessionCreateActions(sessionsModule) - ) - - // Create the smart session - const createSessionsResponse = await nexusSessionClient.createSessions({ - sessionRequestedInfo - }) - ;[cachedPermissionId] = createSessionsResponse.permissionIds - - // Wait for the session creation transaction to be mined and check its success - const { success: sessionCreateSuccess } = - await usersNexusClient.waitForUserOperationReceipt({ - hash: createSessionsResponse.userOpHash - }) - - expect(installSuccess).toBe(sessionCreateSuccess) - - const sessionData: SessionData = { - granter: usersNexusClient.account.address, - sessionPublicKey, - moduleData: { - permissionId: cachedPermissionId, - mode: SmartSessionMode.USE - } - } - - // Zip the session data, and store it for later use by a dapp - zippedSessionDatum = zipSessionData(sessionData) - }, 60000) - - test("should demonstrate using a smart session from dapp's perspective", async () => { - // Now assume the user has left the dapp and the usersNexusClient signer is no longer available - // The following code demonstrates how a dapp can use the session to act on behalf of the user - - // Unzip the session data - const usersSessionData = unzipSessionData(zippedSessionDatum) - - // Create a new Nexus client for the session - // This client will be used to interact with the smart contract account using the session key - const smartSessionNexusClient = await createNexusSessionClient({ - chain, - accountAddress: usersSessionData.granter, - signer: sessionKeyAccount, - transport: http(), - bundlerTransport: http(bundlerUrl) - }) - - // Create a new smart sessions module with the session key - const useSessionsModule = toSmartSessions({ - account: smartSessionNexusClient.account, - signer: sessionKeyAccount, - moduleData: usersSessionData.moduleData - }) - - // Extend the session client with smart session use actions - const useSmartSessionNexusClient = smartSessionNexusClient.extend( - smartSessionUseActions(useSessionsModule) - ) - - // Use the session to perform an action (increment the counter) - const userOpHash = await useSmartSessionNexusClient.useSession({ - actions: [ - { - target: TEST_CONTRACTS.Counter.address, - value: 0n, - callData: encodeFunctionData({ - abi: CounterAbi, - functionName: "incrementNumber" - }) - } - ] - }) - - // Wait for the action to be mined and check its success - const { success: sessionUseSuccess } = - await useSmartSessionNexusClient.waitForUserOperationReceipt({ - hash: userOpHash - }) - - expect(sessionUseSuccess).toBe(true) - }, 60000) // Test timeout set to 60 seconds -}) - -describe("modules.smartSessions", async () => { - let network: NetworkConfig - let chain: Chain - let bundlerUrl: string - - // Test utils - let testClient: MasterClient - let eoaAccount: LocalAccount - let nexusClient: NexusClient - let cachedPermissionId: Hex - let sessionKeyAccount: LocalAccount - let sessionPublicKey: Address - - let sessionsModule: Module - - beforeAll(async () => { - network = await toNetwork() - - chain = network.chain - bundlerUrl = network.bundlerUrl - eoaAccount = getTestAccount(0) - sessionKeyAccount = privateKeyToAccount(generatePrivateKey()) // Generally belongs to the dapp - sessionPublicKey = sessionKeyAccount.address - testClient = toTestClient(chain, getTestAccount(5)) - - nexusClient = await createNexusClient({ - signer: eoaAccount, - chain, - transport: http(), - bundlerTransport: http(bundlerUrl) - }) - - sessionsModule = toSmartSessions({ - account: nexusClient.account, - signer: eoaAccount - }) - await fundAndDeployClients(testClient, [nexusClient]) - }) - - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test.concurrent("should have smart account bytecode", async () => { - const bytecodes = await Promise.all( - [testAddresses.SmartSession, testAddresses.UniActionPolicy].map( - (address) => testClient.getCode({ address }) - ) - ) - expect(bytecodes.every((bytecode) => !!bytecode?.length)).toBeTruthy() - }) - - test.concurrent( - "should parse a human friendly policy reference value to the hex version expected by the contracts", - async () => { - const TWO_THOUSAND_AS_HEX = - "0x00000000000000000000000000000000000000000000000000000000000007d0" - - expect(parseReferenceValue(BigInt(2000))).toBe(TWO_THOUSAND_AS_HEX) - expect(parseReferenceValue(2000)).toBe(TWO_THOUSAND_AS_HEX) - expect(parseReferenceValue("7d0")).toBe(TWO_THOUSAND_AS_HEX) - expect( - parseReferenceValue( - parseReferenceValue(pad(toHex(BigInt(2000)), { size: 32 })) - ) - ).toBe(TWO_THOUSAND_AS_HEX) - } - ) - - test.concurrent("should get a universal action policy", async () => { - const actionConfigData = { - valueLimitPerUse: BigInt(1000), - paramRules: { - length: 2, - rules: [ - { - condition: ParamCondition.EQUAL, - offsetIndex: 0, - isLimited: true, - ref: 1000, - usage: { - limit: BigInt(1000), - used: BigInt(10) - } - }, - { - condition: ParamCondition.LESS_THAN, - offsetIndex: 1, - isLimited: false, - ref: 2000, - usage: { - limit: BigInt(2000), - used: BigInt(100) - } - } - ] - } - } - const installUniversalPolicy = policies.to.universalAction(actionConfigData) - - expect(installUniversalPolicy.policy).toEqual(testAddresses.UniActionPolicy) - expect(installUniversalPolicy.initData).toBeDefined() - }) - - test.concurrent("should get a sudo action policy", async () => { - const installSudoActionPolicy = policies.sudo - expect(installSudoActionPolicy.policy).toBeDefined() - expect(installSudoActionPolicy.initData).toEqual("0x") - }) - - test.concurrent("should get a spending limit policy", async () => { - const installSpendingLimitPolicy = policies.to.spendingLimits([ - { - limit: BigInt(1000), - token: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - } - ]) - - expect(installSpendingLimitPolicy.policy).toBeDefined() - expect(installSpendingLimitPolicy.initData).toBeDefined() - }) - - test.concurrent( - "should have valid smartSessionValidator properties", - async () => { - const smartSessionValidator = toSmartSessions({ - account: nexusClient.account, - signer: eoaAccount - }) - expect(smartSessionValidator.signMessage).toBeDefined() - expect(smartSessionValidator.signUserOpHash).toBeDefined() - expect(smartSessionValidator.address).toBeDefined() - expect(smartSessionValidator.initData).toBeDefined() - expect(smartSessionValidator.deInitData).toBeDefined() - expect(smartSessionValidator.signer).toBeDefined() - expect(smartSessionValidator.type).toBeDefined() - } - ) - - test.concurrent( - "should install sessions module with no init data", - async () => { - const isInstalledBefore = await nexusClient.isModuleInstalled({ - module: sessionsModule.moduleInitData - }) - - if (!isInstalledBefore) { - const hash = await nexusClient.installModule({ - module: sessionsModule.moduleInitData - }) - - const { success: installSuccess } = - await nexusClient.waitForUserOperationReceipt({ hash }) - expect(installSuccess).toBe(true) - } - - const isInstalledAfter = await nexusClient.isModuleInstalled({ - module: sessionsModule - }) - expect(isInstalledAfter).toBe(true) - } - ) - - test("should create Counter increment session (USE mode) on installed smart session validator", async () => { - const isInstalledBefore = await nexusClient.isModuleInstalled({ - module: sessionsModule - }) - - expect(isInstalledBefore).toBe(true) - - // session key signer address is declared here - const sessionRequestedInfo: CreateSessionDataParams[] = [ - { - sessionPublicKey, // session key signer - sessionValidatorAddress: TEST_CONTRACTS.SimpleSessionValidator.address, - sessionKeyData: toHex(toBytes(sessionPublicKey)), - sessionValidAfter: 0, - sessionValidUntil: 0, - actionPoliciesInfo: [ - { - contractAddress: TEST_CONTRACTS.Counter.address, // counter address - functionSelector: "0x273ea3e3" as Hex, // function selector for increment count - validUntil: 0, - validAfter: 0, - rules: [], // no other rules and conditions applied - valueLimit: BigInt(0) - } - ] - } - ] - - const nexusSessionClient = nexusClient.extend( - smartSessionCreateActions(sessionsModule) - ) - - const createSessionsResponse = await nexusSessionClient.createSessions({ - sessionRequestedInfo - }) - - expect(createSessionsResponse.userOpHash).toBeDefined() - expect(createSessionsResponse.permissionIds).toBeDefined() - ;[cachedPermissionId] = createSessionsResponse.permissionIds - - const receipt = await nexusClient.waitForUserOperationReceipt({ - hash: createSessionsResponse.userOpHash - }) - - expect(receipt.success).toBe(true) - }, 60000) - - test("should make use of already enabled session (USE mode) to increment a counter using a session key", async () => { - const counterBefore = await testClient.readContract({ - address: TEST_CONTRACTS.Counter.address, - abi: CounterAbi, - functionName: "getNumber" - }) - - const smartSessionNexusClient = await createNexusSessionClient({ - chain, - accountAddress: nexusClient.account.address, - signer: sessionKeyAccount, - transport: http(), - bundlerTransport: http(bundlerUrl) - }) - - const useSessionsModule = toSmartSessions({ - account: smartSessionNexusClient.account, - signer: sessionKeyAccount, - moduleData: { - permissionId: cachedPermissionId - } - }) - - const useSmartSessionNexusClient = smartSessionNexusClient.extend( - smartSessionUseActions(useSessionsModule) - ) - - const userOpHash = await useSmartSessionNexusClient.useSession({ - actions: [ - { - target: TEST_CONTRACTS.Counter.address, - value: 0n, - callData: encodeFunctionData({ - abi: CounterAbi, - functionName: "incrementNumber" - }) - } - ] - }) - - expect(userOpHash).toBeDefined() - const receipt = - await useSmartSessionNexusClient.waitForUserOperationReceipt({ - hash: userOpHash - }) - expect(receipt.success).toBe(true) - - const counterAfter = await testClient.readContract({ - address: TEST_CONTRACTS.Counter.address, - abi: CounterAbi, - functionName: "getNumber" - }) - - expect(counterAfter).toBe(counterBefore + BigInt(1)) - }, 60000) -}) - -describe("modules.smartSessions.uniPolicy", async () => { - let network: NetworkConfig - let chain: Chain - let bundlerUrl: string - - // Test utils - let testClient: MasterClient - let eoaAccount: LocalAccount - let nexusClient: NexusClient - let nexusAccountAddress: Address - let sessionKeyAccount: LocalAccount - let sessionPublicKey: Address - let cachedPermissionId: Hex - - let sessionsModule: Module - - beforeAll(async () => { - network = await toNetwork() - - chain = network.chain - bundlerUrl = network.bundlerUrl - eoaAccount = getTestAccount(0) - sessionKeyAccount = privateKeyToAccount(generatePrivateKey()) // Generally belongs to the dapp - sessionPublicKey = sessionKeyAccount.address - - testClient = toTestClient(chain, getTestAccount(5)) - - nexusClient = await createNexusClient({ - signer: eoaAccount, - chain, - transport: http(), - bundlerTransport: http(bundlerUrl) - }) - - nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() - - sessionsModule = toSmartSessions({ - account: nexusClient.account, - signer: eoaAccount - }) - - await fundAndDeployClients(testClient, [nexusClient]) - }) - - afterAll(async () => { - await killNetwork([network?.rpcPort, network?.bundlerPort]) - }) - - test("should add balance to mock callee", async () => { - const mockContract = getContract({ - address: TEST_CONTRACTS.MockCallee.address, - abi: MockCalleeAbi, - client: testClient - }) - const balUint = 123n - const balBytes32 = `0x${balUint.toString(16).padStart(64, "0")}` - const balancesBefore = await mockContract.read.bals([nexusAccountAddress]) - const hash = await nexusClient.sendTransaction({ - calls: [ - { - to: TEST_CONTRACTS.MockCallee.address, - data: encodeFunctionData({ - abi: MockCalleeAbi, - functionName: "addBalance", - args: [nexusAccountAddress, balUint, balBytes32 as Hex] - }) - } - ] - }) - const { status } = await nexusClient.waitForTransactionReceipt({ hash }) - expect(status).toBe("success") - const balanceAfter = await mockContract.read.bals([nexusAccountAddress]) - expect(balanceAfter[0]).toBeGreaterThan(balancesBefore[0]) - }, 90000) - - test("should install smartSessionValidator with no init data", async () => { - const isInstalledBefore = await nexusClient.isModuleInstalled({ - module: sessionsModule.moduleInitData - }) - - if (!isInstalledBefore) { - const hash = await nexusClient.installModule({ - module: sessionsModule.moduleInitData - }) - - const { success: installSuccess } = - await nexusClient.waitForUserOperationReceipt({ hash }) - expect(installSuccess).toBe(true) - } - - const isInstalledAfter = await nexusClient.isModuleInstalled({ - module: { - type: "validator", - module: addresses.SmartSession - } - }) - expect(isInstalledAfter).toBe(true) - }) - - test("should create MockCallee add balance session (USE mode) on installed smart session validator", async () => { - const isInstalledBefore = await nexusClient.isModuleInstalled({ - module: { - type: "validator", - module: addresses.SmartSession - } - }) - - expect(isInstalledBefore).toBe(true) - - const functionSelector = "addBalance(address,uint256,bytes32)" - - const unparsedFunctionSelector = functionSelector as AbiFunction | string - const parsedFunctionSelector = slice( - toFunctionSelector(unparsedFunctionSelector), - 0, - 4 - ) - - const maxUintDeposit = 123456n - const minBytes32Deposit = `0x${maxUintDeposit - .toString(16) - .padStart(64, "0")}` - - const rules: Rule[] = [ - { - condition: ParamCondition.EQUAL, - offsetIndex: 0, - isLimited: false, - ref: nexusAccountAddress, - usage: { - limit: BigInt(0), - used: BigInt(0) - } - }, - { - condition: ParamCondition.LESS_THAN, - offsetIndex: 1, - isLimited: true, - ref: maxUintDeposit, - usage: { - limit: BigInt(maxUintDeposit), - used: BigInt(0) - } - }, - { - condition: ParamCondition.GREATER_THAN, - offsetIndex: 2, - isLimited: false, - ref: minBytes32Deposit, - usage: { - limit: BigInt(0), - used: BigInt(0) - } - } - ] - - const sessionRequestedInfo: CreateSessionDataParams[] = [ - { - sessionPublicKey, - sessionValidatorAddress: TEST_CONTRACTS.SimpleSessionValidator.address, - sessionKeyData: toHex(toBytes(sessionPublicKey)), - sessionValidAfter: 0, - sessionValidUntil: 0, - actionPoliciesInfo: [ - { - contractAddress: TEST_CONTRACTS.MockCallee.address, // mock callee address - functionSelector: parsedFunctionSelector, // addBalance function selector - validUntil: 0, // 1717001666 - validAfter: 0, - rules: rules, - valueLimit: BigInt(0) - } - ] - } - ] - - const smartSessionNexusClient = nexusClient.extend( - smartSessionCreateActions(sessionsModule) - ) - - const createSessionsResponse = await smartSessionNexusClient.createSessions( - { sessionRequestedInfo } - ) - - expect(createSessionsResponse.userOpHash).toBeDefined() - expect(createSessionsResponse.permissionIds).toBeDefined() - ;[cachedPermissionId] = createSessionsResponse.permissionIds - - const receipt = await nexusClient.waitForUserOperationReceipt({ - hash: createSessionsResponse.userOpHash - }) - - expect(receipt.success).toBe(true) - - const isEnabled = await isSessionEnabled({ - client: nexusClient.account.client as PublicClient, - accountAddress: nexusClient.account.address, - permissionId: cachedPermissionId - }) - expect(isEnabled).toBe(true) - }, 60000) - - test("should make use of already enabled session (USE mode) to add balance to MockCallee using a session key", async () => { - const isEnabled = await isSessionEnabled({ - client: nexusClient.account.client as PublicClient, - accountAddress: nexusClient.account.address, - permissionId: cachedPermissionId - }) - expect(isEnabled).toBe(true) - - const mockContract = getContract({ - address: TEST_CONTRACTS.MockCallee.address, - abi: MockCalleeAbi, - client: testClient - }) - - // Note: if you try to add more than maxUintDeposit then you would get this below error. - // Error: https://openchain.xyz/signatures?query=0x3b577361 - const balToAddUint = 1234n - - // Note: if you try to add less than minBytes32Deposit then you would get this below error. - // Error: https://openchain.xyz/signatures?query=0x3b577361 - const balToAddBytes32 = `0x${BigInt(1234567) - .toString(16) - .padStart(64, "0")}` - - const balancesBefore = await mockContract.read.bals([nexusAccountAddress]) - - // helpful for out of range test. If time range limit has been provided in the policy. - // await testClient.setNextBlockTimestamp({ - // timestamp: 9727001666n - // }) - - const smartSessionNexusClient = await createNexusSessionClient({ - chain, - accountAddress: nexusClient.account.address, - signer: sessionKeyAccount, - transport: http(), - bundlerTransport: http(bundlerUrl) - }) - - const useSessionsModule = toSmartSessions({ - account: smartSessionNexusClient.account, - signer: sessionKeyAccount, - moduleData: { - permissionId: cachedPermissionId - } - }) - - const useSmartSessionNexusClient = smartSessionNexusClient.extend( - smartSessionUseActions(useSessionsModule) - ) - - const userOpHash = await useSmartSessionNexusClient.useSession({ - account: nexusClient.account, - actions: [ - { - target: TEST_CONTRACTS.MockCallee.address, - value: 0n, - callData: encodeFunctionData({ - abi: MockCalleeAbi, - functionName: "addBalance", - args: [nexusAccountAddress, balToAddUint, balToAddBytes32 as Hex] - }) - } - ] - }) - - expect(userOpHash).toBeDefined() - const receipt = await nexusClient.waitForUserOperationReceipt({ - hash: userOpHash - }) - expect(receipt.success).toBe(true) - - const balanceAfter = await mockContract.read.bals([nexusAccountAddress]) - expect(balanceAfter[0]).toBeGreaterThan(balancesBefore[0]) - }, 60000) -}) diff --git a/src/sdk/modules/smartSessions/Helpers.ts b/src/sdk/modules/smartSessionsValidator/Helpers.ts similarity index 63% rename from src/sdk/modules/smartSessions/Helpers.ts rename to src/sdk/modules/smartSessionsValidator/Helpers.ts index 587f5f235..52f9e7567 100644 --- a/src/sdk/modules/smartSessions/Helpers.ts +++ b/src/sdk/modules/smartSessionsValidator/Helpers.ts @@ -10,20 +10,26 @@ import { toBytes, toHex } from "viem" -import { UniActionPolicyAbi } from "../../__contracts/abi" -import { SmartSessionAbi } from "../../__contracts/abi/SmartSessionAbi" -import addresses from "../../__contracts/addresses" +import { + REGISTRY_ADDRESS, + SIMPLE_SESSION_VALIDATOR_ADDRESS, + SMART_SESSIONS_ADDRESS, + TIMEFRAME_POLICY_ADDRESS, + UNIVERSAL_ACTION_POLICY_ADDRESS +} from "../../constants" +import { ERC7484RegistryAbi, UniActionPolicyAbi } from "../../constants/abi" +import { SmartSessionAbi } from "../../constants/abi/SmartSessionAbi" import { parseReferenceValue } from "../utils/Helpers" +import type { AnyData } from "../utils/Types" import type { ActionConfig, + CreateSessionDataParams, + FullCreateSessionDataParams, RawActionConfig, Rule, - SessionData, SpendingLimitsParams } from "./Types" -const TIMEFRAME_POLICY_ADDRESS = addresses.TimeframePolicy - export const MAX_RULES = 16 /** @@ -48,7 +54,7 @@ export const generateSalt = (): Hex => { */ export const createActionConfig = ( rules: Rule[], - valueLimit: bigint + valueLimit = 0n ): ActionConfig => ({ paramRules: { length: rules.length, @@ -57,6 +63,29 @@ export const createActionConfig = ( valueLimitPerUse: valueLimit }) +/** + * Applies default values to a CreateSessionDataParams object. + * + * @param sessionInfo - The CreateSessionDataParams object to apply defaults to. + * @returns A FullCreateSessionDataParams object with default values applied. + */ +export const applyDefaults = ( + sessionInfo: CreateSessionDataParams +): FullCreateSessionDataParams => { + const sessionKeyData = + sessionInfo.sessionKeyData ?? toHex(toBytes(sessionInfo.sessionPublicKey)) + const sessionPublicKey = sessionInfo.sessionPublicKey ?? sessionKeyData + return { + ...sessionInfo, + sessionKeyData, + sessionPublicKey, + sessionValidUntil: sessionInfo.sessionValidUntil ?? 0, + sessionValidAfter: sessionInfo.sessionValidAfter ?? 0, + sessionValidatorAddress: + sessionInfo.sessionValidatorAddress ?? SIMPLE_SESSION_VALIDATOR_ADDRESS + } +} + /** * Creates an ActionData object. * @@ -136,22 +165,14 @@ export const getPermissionId = async ({ session: Session }) => { return (await client.readContract({ - address: addresses.SmartSession, + address: SMART_SESSIONS_ADDRESS, abi: SmartSessionAbi, functionName: "getPermissionId", args: [session] })) as Hex } -/** - * Checks if a session is enabled for a given account. - * - * @param client - The PublicClient to use for the contract call. - * @param accountAddress - The address of the account. - * @param permissionId - The permission ID to check. - * @returns A promise that resolves to a boolean indicating if the session is enabled. - */ -export const isSessionEnabled = ({ +export const isPermissionEnabled = async ({ client, accountAddress, permissionId @@ -161,9 +182,9 @@ export const isSessionEnabled = ({ permissionId: Hex }) => client.readContract({ - address: addresses.SmartSession, + address: SMART_SESSIONS_ADDRESS, abi: SmartSessionAbi, - functionName: "isSessionEnabled", + functionName: "isPermissionEnabled", args: [permissionId, accountAddress] }) @@ -176,7 +197,7 @@ export const isSessionEnabled = ({ export const toUniversalActionPolicy = ( actionConfig: ActionConfig ): PolicyData => ({ - policy: "0x28120dC008C36d95DE5fa0603526f219c1Ba80f6", + policy: UNIVERSAL_ACTION_POLICY_ADDRESS, initData: encodeAbiParameters(UniActionPolicyAbi, [ toActionConfig(actionConfig) ]) @@ -251,23 +272,71 @@ export const policies = { } as const /** - * Zips SessionData into a compact string representation. + * Stringifies an object, explicitly tagging BigInt values. + * + * @param obj - The object to be stringified. + * @returns A string representing the stringified object with tagged BigInts. + */ +export function stringify(obj: Record): string { + return JSON.stringify(obj, (_, value) => + typeof value === "bigint" + ? { __type: "bigint", value: value.toString() } + : value + ) +} + +/** + * Parses a string representation back into an object, correctly handling tagged BigInt values. * - * @param sessionData - The SessionData object to be zipped. - * @returns A string representing the zipped SessionData. + * @param data - The string representing the stringified object. + * @returns The parsed object with BigInt values restored. */ -export function zipSessionData(sessionData: SessionData): string { - return JSON.stringify(sessionData) +export function parse(data: string): Record { + return JSON.parse(data, (_, value) => { + if (value && typeof value === "object" && value.__type === "bigint") { + return BigInt(value.value) + } + return value + }) } +// Todo +// 1. find trusted attesters. why not just here instead of part of read decorators? +// 2. get trusteAttesters calldata. or returning the whole "Action"/Execution + /** - * Unzips a string representation back into a SessionData object. + * Retrieves the list of trusted attesters for a given account from the registry. + * + * This function queries the registry contract to find all attesters that are trusted + * by the specified account. * - * @param zippedData - The string representing the zipped SessionData. - * @returns The unzipped SessionData object. + * @param params - The parameters object + * @param params.account - The account to check trusted attesters for + * @param params.client - The public client used to interact with the blockchain + * @returns A promise that resolves to an array of addresses representing the trusted attesters + * @throws Will log error and return empty array if registry query fails */ -export function unzipSessionData(zippedData: string): SessionData { - return JSON.parse(zippedData) as SessionData + +export const getTrustedAttesters = async ({ + accountAddress, + client +}: { + accountAddress: Address + client: PublicClient +}): Promise => { + try { + const attesters = (await client.readContract({ + address: REGISTRY_ADDRESS, + abi: ERC7484RegistryAbi, + functionName: "findTrustedAttesters", + args: [accountAddress] + })) as Address[] + + return attesters + } catch (err) { + console.error(err) + return [] + } } export default policies diff --git a/src/sdk/modules/smartSessions/Types.ts b/src/sdk/modules/smartSessionsValidator/Types.ts similarity index 74% rename from src/sdk/modules/smartSessions/Types.ts rename to src/sdk/modules/smartSessionsValidator/Types.ts index 04d3c16c2..c650ccf06 100644 --- a/src/sdk/modules/smartSessions/Types.ts +++ b/src/sdk/modules/smartSessionsValidator/Types.ts @@ -2,7 +2,8 @@ import type { EnableSessionData, SmartSessionMode } from "@rhinestone/module-sdk" -import type { AbiFunction, Address, Hex } from "viem" +import type { AbiFunction, Address, Hex, OneOf } from "viem" +import type { KeyGenData } from "../../clients/decorators/dan/decorators/keyGen" import type { AnyReferenceValue } from "../utils/Helpers" import type { Execution } from "../utils/Types" @@ -23,10 +24,10 @@ export type SessionData = { sessionPublicKey: Hex /** Module-specific data containing session configuration and permissions. */ - moduleData: UseSessionModuleData + moduleData: UsePermissionModuleData } -export type CreateSessionsActionReturnParams = { +export type GrantPermissionActionReturnParams = { /** Array of permission IDs for the created sessions. */ permissionIds: Hex[] /** The execution object for the action. */ @@ -36,7 +37,7 @@ export type CreateSessionsActionReturnParams = { /** * Represents the response for creating sessions. */ -export type CreateSessionsResponse = { +export type GrantPermissionResponse = { /** The hash of the user operation. */ userOpHash: Hex /** Array of permission IDs for the created sessions. */ @@ -52,22 +53,56 @@ export type SmartSessionModeType = /** * Represents the data structure for using a session module. */ -export type UseSessionModuleData = { +export type UsePermissionModuleData = { /** The permission ID for the session. */ - permissionId: Hex + permissionIds: Hex[] /** The mode of the smart session. */ mode?: SmartSessionModeType /** Data for enabling the session. */ enableSessionData?: EnableSessionData + /** Key generation data for the session. */ + keyGenData?: KeyGenData + /** The index of the permission ID to use for the session. Defaults to 0. */ + permissionIdIndex?: number } +type OptionalSessionKeyData = OneOf< + | { + /** Public key for the session. Required for K1 algorithm validators. */ + sessionPublicKey: Hex + } + | { + /** Data for the session key. */ + sessionKeyData: Hex + } +> + /** * Parameters for creating a session. */ -export type CreateSessionDataParams = { +export type CreateSessionDataParams = OptionalSessionKeyData & { /** Public key for the session. Required for K1 algorithm validators. */ sessionPublicKey?: Hex /** Address of the session validator. */ + sessionValidatorAddress?: Address + /** Type of the session validator. Usually "simple K1 validator". */ + sessionValidatorType?: string + /** Optional salt for the session. */ + salt?: Hex + /** Timestamp until which the session is valid. */ + sessionValidUntil?: number + /** Timestamp after which the session becomes valid. */ + sessionValidAfter?: number + /** Array of action policy data for the session. */ + actionPoliciesInfo: ActionPolicyData[] + /** Chain IDs where the session should be enabled. Useful for enable mode. */ + chainIds?: bigint[] +} + +export type FullCreateSessionDataParams = { + /** Public key for the session. Required for K1 algorithm validators. */ + sessionPublicKey: Hex + /** Address of the session validator. */ sessionValidatorAddress: Address /** Type of the session validator. Usually "simple K1 validator". */ sessionValidatorType?: string @@ -76,9 +111,9 @@ export type CreateSessionDataParams = { /** Optional salt for the session. */ salt?: Hex /** Timestamp until which the session is valid. */ - sessionValidUntil?: number + sessionValidUntil: number /** Timestamp after which the session becomes valid. */ - sessionValidAfter?: number + sessionValidAfter: number /** Array of action policy data for the session. */ actionPoliciesInfo: ActionPolicyData[] /** Chain IDs where the session should be enabled. Useful for enable mode. */ @@ -94,13 +129,13 @@ export type ActionPolicyData = { /** The specific function selector from the contract to be included in the policy */ functionSelector: string | AbiFunction /** Timestamp until which the policy is valid */ - validUntil: number + validUntil?: number /** Timestamp after which the policy becomes valid */ - validAfter: number + validAfter?: number /** Array of rules for the policy */ - rules: Rule[] + rules?: Rule[] /** The maximum value that can be transferred in a single transaction */ - valueLimit: bigint + valueLimit?: bigint } /** diff --git a/src/sdk/modules/smartSessions/decorators/createSessions.ts b/src/sdk/modules/smartSessionsValidator/decorators/grantPermission.ts similarity index 67% rename from src/sdk/modules/smartSessions/decorators/createSessions.ts rename to src/sdk/modules/smartSessionsValidator/decorators/grantPermission.ts index 78bc7214a..21b26de59 100644 --- a/src/sdk/modules/smartSessions/decorators/createSessions.ts +++ b/src/sdk/modules/smartSessionsValidator/decorators/grantPermission.ts @@ -1,12 +1,24 @@ -import type { ActionData, PolicyData, Session } from "@rhinestone/module-sdk" +import { + type ActionData, + type PolicyData, + type Session, + findTrustedAttesters, + getTrustAttestersAction +} from "@rhinestone/module-sdk" import type { Chain, Client, Hex, PublicClient, Transport } from "viem" import { sendUserOperation } from "viem/account-abstraction" import { encodeFunctionData, getAction, parseAccount } from "viem/utils" -import { SmartSessionAbi } from "../../../__contracts/abi/SmartSessionAbi" -import addresses from "../../../__contracts/addresses" +import { ERROR_MESSAGES, Logger } from "../../../account" import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import { MOCK_ATTESTER_ADDRESS } from "../../../constants" +import { + SIMPLE_SESSION_VALIDATOR_ADDRESS, + SMART_SESSIONS_ADDRESS +} from "../../../constants" +import { SmartSessionAbi } from "../../../constants/abi/SmartSessionAbi" import type { ModularSmartAccount } from "../../utils/Types" import { + applyDefaults, createActionConfig, createActionData, generateSalt, @@ -14,20 +26,21 @@ import { toTimeRangePolicy, toUniversalActionPolicy } from "../Helpers" -import type { CreateSessionDataParams } from "../Types" import type { - CreateSessionsActionReturnParams, - CreateSessionsResponse + CreateSessionDataParams, + FullCreateSessionDataParams +} from "../Types" +import type { + GrantPermissionActionReturnParams, + GrantPermissionResponse } from "../Types" - -const SIMPLE_SESSION_VALIDATOR_ADDRESS = addresses.SimpleSessionValidator /** * Parameters for creating sessions in a modular smart account. * * @template TModularSmartAccount - Type of the modular smart account, extending ModularSmartAccount or undefined. */ -export type CreateSessionsParameters< +export type GrantPermissionParameters< TModularSmartAccount extends ModularSmartAccount | undefined > = { /** Array of session data parameters for creating multiple sessions. */ @@ -42,6 +55,8 @@ export type CreateSessionsParameters< publicClient?: PublicClient /** The modular smart account to create sessions for. If not provided, the client's account will be used. */ account?: TModularSmartAccount + /** Optional attesters to trust. */ + attesters?: Hex[] } /** @@ -51,13 +66,15 @@ export type CreateSessionsParameters< * @param client - The public client for blockchain interactions. * @returns A promise that resolves to the action data and permission IDs, or an Error. */ -export const getSmartSessionValidatorCreateSessionsAction = async ({ +export const getPermissionAction = async ({ + chainId, sessionRequestedInfo, client }: { - sessionRequestedInfo: CreateSessionDataParams[] + chainId: number + sessionRequestedInfo: FullCreateSessionDataParams[] client: PublicClient -}): Promise => { +}): Promise => { const sessions: Session[] = [] const permissionIds: Hex[] = [] @@ -67,7 +84,7 @@ export const getSmartSessionValidatorCreateSessionsAction = async ({ for (const actionPolicyInfo of sessionInfo.actionPoliciesInfo) { // TODO: make it easy to generate rules for particular contract and selectors. const actionConfig = createActionConfig( - actionPolicyInfo.rules, + actionPolicyInfo.rules ?? [], actionPolicyInfo.valueLimit ) @@ -77,8 +94,8 @@ export const getSmartSessionValidatorCreateSessionsAction = async ({ const uniActionPolicyData = toUniversalActionPolicy(actionConfig) // create time range policy here.. const timeFramePolicyData: PolicyData = toTimeRangePolicy( - actionPolicyInfo.validUntil, - actionPolicyInfo.validAfter + actionPolicyInfo.validUntil ?? 0, + actionPolicyInfo.validAfter ?? 0 ) // Create ActionData @@ -97,6 +114,7 @@ export const getSmartSessionValidatorCreateSessionsAction = async ({ ) const session: Session = { + chainId: BigInt(chainId), sessionValidator: sessionInfo.sessionValidatorAddress ?? SIMPLE_SESSION_VALIDATOR_ADDRESS, sessionValidatorInitData: sessionInfo.sessionKeyData, // sessionValidatorInitData: abi.encodePacked(sessionSigner.addr), @@ -120,7 +138,7 @@ export const getSmartSessionValidatorCreateSessionsAction = async ({ sessions.push(session) } - const createSessionsData = encodeFunctionData({ + const grantPermissionData = encodeFunctionData({ abi: SmartSessionAbi, functionName: "enableSessions", args: [sessions] @@ -128,9 +146,9 @@ export const getSmartSessionValidatorCreateSessionsAction = async ({ return { action: { - target: addresses.SmartSession, + target: SMART_SESSIONS_ADDRESS, value: BigInt(0), - callData: createSessionsData + callData: grantPermissionData }, permissionIds: permissionIds } @@ -153,9 +171,9 @@ export const getSmartSessionValidatorCreateSessionsAction = async ({ * * @example * ```typescript - * import { createSessions } from '@biconomy/sdk' + * import { grantPermission } from '@biconomy/sdk' * - * const result = await createSessions(nexusClient, { + * const result = await grantPermission(nexusClient, { * sessionRequestedInfo: [ * { * sessionKeyData: '0x...', @@ -180,57 +198,99 @@ export const getSmartSessionValidatorCreateSessionsAction = async ({ * - The number of sessions created is determined by the length of the `sessionRequestedInfo` array. * - Each session's policies and permissions are determined by the `actionPoliciesInfo` provided. */ -export async function createSessions< +export async function grantPermission< TModularSmartAccount extends ModularSmartAccount | undefined >( client: Client, - parameters: CreateSessionsParameters -): Promise { + parameters: GrantPermissionParameters +): Promise { const { publicClient: publicClient_ = client.account?.client as PublicClient, account: account_ = client.account, maxFeePerGas, maxPriorityFeePerGas, nonce, - sessionRequestedInfo + sessionRequestedInfo, + attesters } = parameters if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } const account = parseAccount(account_) as ModularSmartAccount - const actionResponse = await getSmartSessionValidatorCreateSessionsAction({ + const chainId = publicClient_?.chain?.id + + if (!chainId) { + throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND) + } + + const defaultedSessionRequestedInfo = sessionRequestedInfo.map(applyDefaults) + + const attestersToTrust = attesters ?? [MOCK_ATTESTER_ADDRESS] + const actionResponse = await getPermissionAction({ + chainId, client: publicClient_, - sessionRequestedInfo + sessionRequestedInfo: defaultedSessionRequestedInfo }) - if ("action" in actionResponse) { - const { action } = actionResponse - if (!("callData" in action)) { - throw new Error("Error getting enable sessions action") - } + const trustAttestersAction = getTrustAttestersAction({ + attesters: attestersToTrust, + threshold: attestersToTrust.length + }) - const userOpHash = (await getAction( - client, - sendUserOperation, - "sendUserOperation" - )({ - calls: [ + const trustedAttesters = await findTrustedAttesters({ + client: publicClient_, + accountAddress: account.address + }) + + const needToAddTrustAttesters = trustedAttesters.length === 0 + Logger.log("needToAddTrustAttesters", needToAddTrustAttesters) + + if (!("action" in actionResponse)) { + throw new Error("Error getting enable sessions action") + } + + const { action } = actionResponse + + if (!("callData" in action)) { + throw new Error("Error getting enable sessions action") + } + + if (!("callData" in trustAttestersAction)) { + throw new Error("Error getting trust attesters action") + } + + const calls = needToAddTrustAttesters + ? [ + { + to: trustAttestersAction.target, + value: trustAttestersAction.value.valueOf(), + data: trustAttestersAction.callData + }, { to: action.target, - value: BigInt(action.value.toString()), + value: action.value, data: action.callData } - ], - maxFeePerGas, - maxPriorityFeePerGas, - nonce, - account - })) as Hex + ] + : [ + { + to: action.target, + value: action.value, + data: action.callData + } + ] + + if ("action" in actionResponse) { + const userOpHash = (await getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ calls, maxFeePerGas, maxPriorityFeePerGas, nonce, account })) as Hex return { userOpHash: userOpHash, diff --git a/src/sdk/modules/smartSessions/decorators/index.ts b/src/sdk/modules/smartSessionsValidator/decorators/index.ts similarity index 52% rename from src/sdk/modules/smartSessions/decorators/index.ts rename to src/sdk/modules/smartSessionsValidator/decorators/index.ts index c03329057..bbbec6906 100644 --- a/src/sdk/modules/smartSessions/decorators/index.ts +++ b/src/sdk/modules/smartSessionsValidator/decorators/index.ts @@ -1,9 +1,19 @@ import type { Chain, Client, Hash, Transport } from "viem" +import { danActions } from "../../../clients/decorators/dan/decorators" import type { ModularSmartAccount, Module } from "../../utils/Types" -import type { CreateSessionsResponse } from "../Types" -import { type CreateSessionsParameters, createSessions } from "./createSessions" -import { type UseSessionParameters, useSession } from "./useSession" - +import type { GrantPermissionResponse } from "../Types" +import type { SmartSessionModule } from "../toSmartSessionsValidator" +import { + type GrantPermissionParameters, + grantPermission +} from "./grantPermission" +import { type TrustAttestersParameters, trustAttesters } from "./trustAttesters" +import { + type DanClient, + type UseDistributedPermissionParameters, + useDistributedPermission +} from "./useDistributedPermission" +import { type UsePermissionParameters, usePermission } from "./usePermission" /** * Defines the shape of actions available for creating smart sessions. * @@ -18,9 +28,19 @@ export type SmartSessionCreateActions< * @param args - Parameters for creating sessions. * @returns A promise that resolves to the creation response. */ - createSessions: ( - args: CreateSessionsParameters - ) => Promise + grantPermission: ( + args: GrantPermissionParameters + ) => Promise + + /** + * Trusts attesters for a modular smart account. + * + * @param args - Parameters for trusting attesters. + * @returns A promise that resolves to the transaction hash. + */ + trustAttesters: ( + args?: TrustAttestersParameters + ) => Promise } /** @@ -37,8 +57,17 @@ export type SmartSessionUseActions< * @param args - Parameters for using a session. * @returns A promise that resolves to the transaction hash. */ - useSession: ( - args: UseSessionParameters + usePermission: ( + args: UsePermissionParameters + ) => Promise + /** + * Uses a session to perform multiple actions. + * + * @param args - Parameters for using a session. + * @returns A promise that resolves to the transaction hash. + */ + useDistributedPermission: ( + args: UseDistributedPermissionParameters ) => Promise } @@ -53,7 +82,8 @@ export function smartSessionCreateActions(_: Module) { client: Client ): SmartSessionCreateActions => { return { - createSessions: (args) => createSessions(client, args) + grantPermission: (args) => grantPermission(client, args), + trustAttesters: (args) => trustAttesters(client, args) } } } @@ -64,17 +94,24 @@ export function smartSessionCreateActions(_: Module) { * @param smartSessionsModule - The smart sessions module to be set on the client's account. * @returns A function that takes a client and returns SmartSessionUseActions. */ -export function smartSessionUseActions(smartSessionsModule: Module) { +export function smartSessionUseActions( + smartSessionsModule: SmartSessionModule +) { return ( client: Client ): SmartSessionUseActions => { client?.account?.setModule(smartSessionsModule) return { - useSession: (args) => useSession(client, args) + usePermission: (args) => usePermission(client, args), + useDistributedPermission: (args) => { + const danClient = client.extend(danActions()) as unknown as DanClient + return useDistributedPermission(danClient, args) + } } } } -// Re-exporting types and functions for easier access -export type { CreateSessionsParameters, UseSessionParameters } -export { createSessions, useSession } +export * from "./grantPermission" +export * from "./trustAttesters" +export * from "./usePermission" +export * from "./useDistributedPermission" diff --git a/src/sdk/modules/smartSessions/decorators/smartSessions.decorators.test.ts b/src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts similarity index 82% rename from src/sdk/modules/smartSessions/decorators/smartSessions.decorators.test.ts rename to src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts index 8bf3d57ca..d9445b028 100644 --- a/src/sdk/modules/smartSessions/decorators/smartSessions.decorators.test.ts +++ b/src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts @@ -1,11 +1,4 @@ -import { OWNABLE_VALIDATOR_ADDRESS } from "@rhinestone/module-sdk/module" -import { - http, - type Account, - type Address, - type Chain, - type LocalAccount -} from "viem" +import { http, type Address, type Chain, type LocalAccount } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { afterAll, beforeAll, describe, expect, test } from "vitest" import { toNetwork } from "../../../../test/testSetup" @@ -22,8 +15,7 @@ import { createNexusClient } from "../../../clients/createNexusClient" import { createNexusSessionClient } from "../../../clients/createNexusSessionClient" -import type { Module } from "../../utils/Types" -import { toSmartSessions } from "../toSmartSessions" +import { toSmartSessionsValidator } from "../toSmartSessionsValidator" import { smartSessionCreateActions, smartSessionUseActions } from "./" describe("modules.smartSessions.decorators", async () => { @@ -39,8 +31,6 @@ describe("modules.smartSessions.decorators", async () => { let nexusAccountAddress: Address let sessionPublicKey: Address - let sessionsModule: Module - beforeAll(async () => { network = await toNetwork() @@ -67,7 +57,7 @@ describe("modules.smartSessions.decorators", async () => { }) test("should test create smart session decorators", async () => { - const sessionsModule = toSmartSessions({ + const sessionsModule = toSmartSessionsValidator({ account: nexusClient.account, signer: eoaAccount }) @@ -85,15 +75,16 @@ describe("modules.smartSessions.decorators", async () => { ) expect(nexusSessionClient).toBeDefined() - expect(nexusSessionClient.createSessions).toBeTypeOf("function") + expect(nexusSessionClient.grantPermission).toBeTypeOf("function") + expect(nexusSessionClient.trustAttesters).toBeTypeOf("function") }) test("should test use smart session decorators", async () => { - const useSessionsModule = toSmartSessions({ + const usePermissionsModule = toSmartSessionsValidator({ account: nexusClient.account, signer: sessionKeyAccount, moduleData: { - permissionId: "0x" + permissionIds: [] } }) @@ -106,10 +97,10 @@ describe("modules.smartSessions.decorators", async () => { }) const nexusSessionClient = smartSessionNexusClient.extend( - smartSessionUseActions(useSessionsModule) + smartSessionUseActions(usePermissionsModule) ) expect(nexusSessionClient).toBeDefined() - expect(nexusSessionClient.useSession).toBeTypeOf("function") + expect(nexusSessionClient.usePermission).toBeTypeOf("function") }) }) diff --git a/src/sdk/modules/smartSessionsValidator/decorators/trustAttesters.ts b/src/sdk/modules/smartSessionsValidator/decorators/trustAttesters.ts new file mode 100644 index 000000000..fc9393c39 --- /dev/null +++ b/src/sdk/modules/smartSessionsValidator/decorators/trustAttesters.ts @@ -0,0 +1,118 @@ +import { encodeFunctionData } from "viem" +import type { Chain, Client, Hex, Transport } from "viem" +import { sendUserOperation } from "viem/account-abstraction" +import { getAction, parseAccount } from "viem/utils" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import { MOCK_ATTESTER_ADDRESS, REGISTRY_ADDRESS } from "../../../constants" +import type { ModularSmartAccount } from "../../utils/Types" + +/** + * Parameters for trusting attesters in a smart session validator. + * + * @template TModularSmartAccount - Type of the modular smart account, extending ModularSmartAccount or undefined. + */ +export type TrustAttestersParameters< + TModularSmartAccount extends ModularSmartAccount | undefined +> = { + /** The addresses of the attesters to be trusted. */ + attesters?: Hex[] + /** The address of the registry contract. */ + registryAddress?: Hex + /** The maximum fee per gas unit the transaction is willing to pay. */ + maxFeePerGas?: bigint + /** The maximum priority fee per gas unit the transaction is willing to pay. */ + maxPriorityFeePerGas?: bigint + /** The nonce of the transaction. If not provided, it will be determined automatically. */ + nonce?: bigint + /** The modular smart account to use for trusting attesters. If not provided, the client's account will be used. */ + account?: TModularSmartAccount + /** The threshold of the attesters to be trusted. */ + threshold?: number +} + +/** + * Trusts attesters for the smart session validator. + * + * This function prepares and sends a user operation to trust specified attesters + * in the smart session validator's registry. + * + * @template TModularSmartAccount - Type of the modular smart account, extending ModularSmartAccount or undefined. + * @param client - The client used to interact with the blockchain. + * @param parameters - Parameters including the attesters to trust, registry address, and optional gas settings. + * @returns A promise that resolves to the hash of the sent user operation. + * + * @throws {AccountNotFoundError} If no account is provided and the client doesn't have an associated account. + * + * @example + * ```typescript + * const result = await trustAttesters(nexusClient, { + * attesters: ['0x1234...', '0x5678...'], + * registryAddress: '0xabcd...', + * maxFeePerGas: 1000000000n + * }); + * console.log(`Transaction hash: ${result}`); + * ``` + * + * @remarks + * - Ensure that the client has sufficient gas to cover the transaction. + * - The registry address should be the address of the contract managing trusted attesters. + */ +export async function trustAttesters< + TModularSmartAccount extends ModularSmartAccount | undefined +>( + client: Client, + parameters?: TrustAttestersParameters +): Promise { + const { + account: account_ = client.account, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + attesters = [MOCK_ATTESTER_ADDRESS], + registryAddress = REGISTRY_ADDRESS, + threshold = attesters.length + } = parameters ?? {} + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/nexus-client/methods#sendtransaction" + }) + } + + const account = parseAccount(account_) as ModularSmartAccount + + const trustAttestersData = encodeFunctionData({ + abi: [ + { + inputs: [ + { internalType: "uint8", name: "threshold", type: "uint8" }, + { internalType: "address[]", name: "attesters", type: "address[]" } + ], + name: "trustAttesters", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "trustAttesters", + args: [threshold, attesters] + }) + + return getAction( + client, + sendUserOperation, + "sendUserOperation" + )({ + calls: [ + { + to: registryAddress, + value: 0n, + data: trustAttestersData + } + ], + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + account + }) +} diff --git a/src/sdk/modules/smartSessionsValidator/decorators/useDistributedPermission.ts b/src/sdk/modules/smartSessionsValidator/decorators/useDistributedPermission.ts new file mode 100644 index 000000000..fa82c488a --- /dev/null +++ b/src/sdk/modules/smartSessionsValidator/decorators/useDistributedPermission.ts @@ -0,0 +1,113 @@ +import type { Chain, Client, Hex, Transport } from "viem" +import { type BundlerClient, sendUserOperation } from "viem/account-abstraction" +import { getAction } from "viem/utils" +import { ERROR_MESSAGES } from "../../../account" +import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" +import type { Call } from "../../../account/utils/Types" +import type { Signer } from "../../../account/utils/toSigner" +import type { DanActions } from "../../../clients/decorators/dan/decorators" +import { parseModule } from "../../utils/Helpers" +import type { ModularSmartAccount } from "../../utils/Types" +import type { SmartSessionModule } from "../toSmartSessionsValidator" + +/** + * Parameters for using a smart session to execute actions. + * + * @template TModularSmartAccount - Type of the modular smart account, extending ModularSmartAccount or undefined. + */ +export type UseDistributedPermissionParameters< + TModularSmartAccount extends ModularSmartAccount | undefined +> = { + /** Array of executions to perform in the session. Allows for batch transactions if the session is enabled for multiple actions. */ + calls: Call[] + /** The maximum fee per gas unit the transaction is willing to pay. */ + maxFeePerGas?: bigint + /** The maximum priority fee per gas unit the transaction is willing to pay. */ + maxPriorityFeePerGas?: bigint + /** The nonce of the transaction. If not provided, it will be determined automatically. */ + nonce?: bigint + /** The modular smart account to use for the session. If not provided, the client's account will be used. */ + account?: TModularSmartAccount + /** The signer to use for the session. Defaults to the signer of the client. */ + signer?: Signer +} + +export type DanClient = Client< + Transport, + Chain | undefined, + ModularSmartAccount +> & + DanActions & + BundlerClient + +/** + * Executes actions using a smart session. + * + * This function allows for the execution of one or more actions within an enabled smart session. + * It can handle batch transactions if the session is configured for multiple actions. + * + * @template TModularSmartAccount - Type of the modular smart account, extending ModularSmartAccount or undefined. + * @param client - The client used to interact with the blockchain. + * @param parameters - Parameters for using the session, including actions to execute and optional gas settings. + * @returns A promise that resolves to the hash of the sent user operation. + * + * @throws {AccountNotFoundError} If no account is provided and the client doesn't have an associated account. + * + * @example + * ```typescript + * const result = await useDistributedPermission(nexusClient, { + * calls: [ + * { + * to: '0x1234...', + * data: '0xabcdef...' + * } + * ], + * maxFeePerGas: 1000000000n + * }); + * console.log(`Transaction hash: ${result}`); + * ``` + * + * @remarks + * - Ensure that the session is enabled and has the necessary permissions for the actions being executed. + * - For batch transactions, all actions must be permitted within the same session. + * - The function uses the `sendUserOperation` method, which is specific to account abstraction implementations. + */ +export async function useDistributedPermission< + TModularSmartAccount extends ModularSmartAccount | undefined +>( + client: DanClient, + parameters: UseDistributedPermissionParameters +): Promise { + const { account: account_ = client.account } = parameters + + if (!account_) { + throw new AccountNotFoundError({ + docsPath: "/nexus-client/methods#sendtransaction" + }) + } + + const params = { ...parameters, account: account_ } + + const preppedUserOp = await client.prepareUserOperation(params) + const sessionsModule = parseModule(client) as SmartSessionModule + const keyGenData = sessionsModule?.moduleData?.keyGenData + + if (!keyGenData) { + throw new Error(ERROR_MESSAGES.KEY_GEN_DATA_NOT_FOUND) + } + + const { signature } = await client.sigGen({ ...preppedUserOp, keyGenData }) + + if (!signature) { + throw new Error(ERROR_MESSAGES.SIGNATURE_NOT_FOUND) + } + + const extendedSignature = sessionsModule.sigGen(signature) + + return await getAction( + client, + sendUserOperation, + "sendUserOperation" + // @ts-ignore + )({ ...preppedUserOp, account: account_, signature: extendedSignature }) +} diff --git a/src/sdk/modules/smartSessions/decorators/useSession.ts b/src/sdk/modules/smartSessionsValidator/decorators/usePermission.ts similarity index 72% rename from src/sdk/modules/smartSessions/decorators/useSession.ts rename to src/sdk/modules/smartSessionsValidator/decorators/usePermission.ts index 6b3d0ada4..374d3e72c 100644 --- a/src/sdk/modules/smartSessions/decorators/useSession.ts +++ b/src/sdk/modules/smartSessionsValidator/decorators/usePermission.ts @@ -1,20 +1,21 @@ import type { Chain, Client, Hex, Transport } from "viem" import { sendUserOperation } from "viem/account-abstraction" -import { getAction, parseAccount } from "viem/utils" -import type { NexusAccount } from "../../../account/toNexusAccount" +import { getAction } from "viem/utils" import { AccountNotFoundError } from "../../../account/utils/AccountNotFound" -import type { Execution, ModularSmartAccount } from "../../utils/Types" +import type { Call } from "../../../account/utils/Types" +import type { Signer } from "../../../account/utils/toSigner" +import type { ModularSmartAccount } from "../../utils/Types" /** * Parameters for using a smart session to execute actions. * * @template TModularSmartAccount - Type of the modular smart account, extending ModularSmartAccount or undefined. */ -export type UseSessionParameters< +export type UsePermissionParameters< TModularSmartAccount extends ModularSmartAccount | undefined > = { /** Array of executions to perform in the session. Allows for batch transactions if the session is enabled for multiple actions. */ - actions: Execution[] + calls: Call[] /** The maximum fee per gas unit the transaction is willing to pay. */ maxFeePerGas?: bigint /** The maximum priority fee per gas unit the transaction is willing to pay. */ @@ -23,6 +24,8 @@ export type UseSessionParameters< nonce?: bigint /** The modular smart account to use for the session. If not provided, the client's account will be used. */ account?: TModularSmartAccount + /** The signer to use for the session. Defaults to the signer of the client. */ + signer?: Signer } /** @@ -40,12 +43,11 @@ export type UseSessionParameters< * * @example * ```typescript - * const result = await useSession(nexusClient, { - * actions: [ + * const result = await usePermission(nexusClient, { + * calls: [ * { - * target: '0x1234...', - * value: 0n, - * callData: '0xabcdef...' + * to: '0x1234...', + * data: '0xabcdef...' * } * ], * maxFeePerGas: 1000000000n @@ -58,41 +60,24 @@ export type UseSessionParameters< * - For batch transactions, all actions must be permitted within the same session. * - The function uses the `sendUserOperation` method, which is specific to account abstraction implementations. */ -export async function useSession< +export async function usePermission< TModularSmartAccount extends ModularSmartAccount | undefined >( client: Client, - parameters: UseSessionParameters + parameters: UsePermissionParameters ): Promise { - const { - account: account_ = client.account, - maxFeePerGas, - maxPriorityFeePerGas, - nonce, - actions - } = parameters + const { account: account_ = client.account } = parameters if (!account_) { throw new AccountNotFoundError({ - docsPath: "/nexus/nexus-client/methods#sendtransaction" + docsPath: "/nexus-client/methods#sendtransaction" }) } - const account = parseAccount(account_) as NexusAccount - return await getAction( client, sendUserOperation, "sendUserOperation" - )({ - calls: actions.map((action) => ({ - to: action.target, - value: BigInt(action.value.toString()), - data: action.callData - })), - maxFeePerGas, - maxPriorityFeePerGas, - nonce, - account - }) + // @ts-ignore + )({ ...parameters, account: account_ }) } diff --git a/src/sdk/modules/smartSessionsValidator/index.ts b/src/sdk/modules/smartSessionsValidator/index.ts new file mode 100644 index 000000000..1102a3095 --- /dev/null +++ b/src/sdk/modules/smartSessionsValidator/index.ts @@ -0,0 +1,4 @@ +export * from "./decorators" +export * from "./toSmartSessionsValidator" +export * from "./Helpers" +export * from "./Types" diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dan.dx.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dan.dx.test.ts new file mode 100644 index 000000000..a2288bf44 --- /dev/null +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dan.dx.test.ts @@ -0,0 +1,191 @@ +import { SmartSessionMode } from "@rhinestone/module-sdk/module" +import { + http, + type Chain, + type Hex, + type LocalAccount, + encodeFunctionData +} from "viem" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { CounterAbi } from "../../../test/__contracts/abi/CounterAbi" +import { testAddresses } from "../../../test/callDatas" +import { toNetwork } from "../../../test/testSetup" +import { + fundAndDeployClients, + getTestAccount, + killNetwork, + toTestClient +} from "../../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../../test/testUtils" +import { + type NexusClient, + createNexusClient +} from "../../clients/createNexusClient" +import { createNexusSessionClient } from "../../clients/createNexusSessionClient" +import { danActions } from "../../clients/decorators/dan" +import type { Module } from "../utils/Types" +import { parse, stringify } from "./Helpers" +import type { CreateSessionDataParams, SessionData } from "./Types" +import { smartSessionCreateActions, smartSessionUseActions } from "./decorators" +import { toSmartSessionsValidator } from "./toSmartSessionsValidator" + +// This test suite demonstrates how to create and use a smart session using Biconomy's Distributed Sessions (DAN). +// Distributed Sessions enhance security and efficiency by storing session keys on Biconomy's Delegated Authorisation Network (DAN), +// providing features like automated transaction processing and reduced exposure of private keys. + +describe("modules.smartSessions.dan.dx", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utilities and variables + let testClient: MasterClient + let eoaAccount: LocalAccount + let usersNexusClient: NexusClient + let dappAccount: LocalAccount + let zippedSessionDatum: string + let sessionsModule: Module + + beforeAll(async () => { + // Setup test network and accounts + network = await toNetwork("BASE_SEPOLIA_FORKED") + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = getTestAccount(0) + dappAccount = getTestAccount(7) + testClient = toTestClient(chain, getTestAccount(5)) + }) + + afterAll(async () => { + // Clean up the network after tests + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test("should demonstrate creating a smart session using DAN", async () => { + // Initialize the user's Nexus client with DAN actions + usersNexusClient = await createNexusClient({ + signer: eoaAccount, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + const danNexusClient = usersNexusClient.extend(danActions()) + + // Generate a session key using DAN + const keyGenData = await danNexusClient.keyGen() + const sessionPublicKey = keyGenData.sessionPublicKey + + // Fund and deploy the user's smart account + await fundAndDeployClients(testClient, [usersNexusClient]) + + // Initialize the smart sessions validator module + sessionsModule = toSmartSessionsValidator({ + account: usersNexusClient.account, + signer: eoaAccount + }) + + // Install the sessions module + const hash = await usersNexusClient.installModule({ + module: sessionsModule.moduleInitData + }) + + // Extend the Nexus client with smart session creation actions + const nexusSessionClient = usersNexusClient.extend( + smartSessionCreateActions(sessionsModule) + ) + + // Wait for the module installation to complete + const { success: installSuccess } = + await usersNexusClient.waitForUserOperationReceipt({ hash }) + + expect(installSuccess).toBe(true) + + // Define the permissions for the smart session + const sessionRequestedInfo: CreateSessionDataParams[] = [ + { + sessionPublicKey, // Public key of the session stored in DAN + actionPoliciesInfo: [ + { + contractAddress: testAddresses.Counter, + functionSelector: "0x273ea3e3" as Hex // Selector for 'incrementNumber' function + } + ] + } + ] + + // Create the smart session with the specified permissions + const createSessionsResponse = await nexusSessionClient.grantPermission({ + sessionRequestedInfo + }) + + // Wait for the permission grant operation to complete + const { success: sessionCreateSuccess } = + await usersNexusClient.waitForUserOperationReceipt({ + hash: createSessionsResponse.userOpHash + }) + + expect(installSuccess).toBe(sessionCreateSuccess) + + // Prepare the session data to be stored by the dApp. This could be saved in a Database or client side in local storage. + const sessionData: SessionData = { + granter: usersNexusClient.account.address, + sessionPublicKey, + moduleData: { + keyGenData, + permissionIds: createSessionsResponse.permissionIds, + mode: SmartSessionMode.USE + } + } + + // Serialize the session data + zippedSessionDatum = stringify(sessionData) + }, 200000) + + test("should demonstrate using a smart session using DAN", async () => { + // Parse the session data received from the user + const { moduleData, granter } = parse(zippedSessionDatum) + + // Initialize the smart session client's Nexus client + const smartSessionNexusClient = await createNexusSessionClient({ + chain, + accountAddress: granter, + signer: dappAccount, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + // Initialize the smart sessions validator module with the received module data + const usePermissionsModule = toSmartSessionsValidator({ + account: smartSessionNexusClient.account, + signer: dappAccount, + moduleData // This includes the keyGenData + }) + + // Extend the Nexus client with smart session usage and dan actions + const danSessionClient = smartSessionNexusClient + .extend(smartSessionUseActions(usePermissionsModule)) + .extend(danActions()) + + // Use the distributed permission to execute a transaction + const userOpHash = await danSessionClient.useDistributedPermission({ + calls: [ + { + to: testAddresses.Counter, + data: encodeFunctionData({ + abi: CounterAbi, + functionName: "incrementNumber" + }) + } + ] + }) + + // Wait for the transaction to be processed + const { success: sessionUseSuccess } = + await danSessionClient.waitForUserOperationReceipt({ + hash: userOpHash + }) + + expect(sessionUseSuccess).toBe(true) + }, 200000) // Test timeout set to 200 seconds +}) diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dx.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dx.test.ts new file mode 100644 index 000000000..c84df2611 --- /dev/null +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dx.test.ts @@ -0,0 +1,207 @@ +import { SmartSessionMode } from "@rhinestone/module-sdk/module" +import { + http, + type Address, + type Chain, + type Hex, + type LocalAccount, + encodeFunctionData +} from "viem" +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { CounterAbi } from "../../../test/__contracts/abi/CounterAbi" +import { testAddresses } from "../../../test/callDatas" +import { toNetwork } from "../../../test/testSetup" +import { + fundAndDeployClients, + getTestAccount, + killNetwork, + toTestClient +} from "../../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../../test/testUtils" +import { + type NexusClient, + createNexusClient +} from "../../clients/createNexusClient" +import { createNexusSessionClient } from "../../clients/createNexusSessionClient" +import type { Module } from "../utils/Types" +import { parse, stringify } from "./Helpers" +import type { CreateSessionDataParams, SessionData } from "./Types" +import { smartSessionCreateActions, smartSessionUseActions } from "./decorators" +import { toSmartSessionsValidator } from "./toSmartSessionsValidator" + +describe("modules.smartSessions.dx", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let eoaAccount: LocalAccount + let usersNexusClient: NexusClient + let sessionKeyAccount: LocalAccount + let sessionPublicKey: Address + + let zippedSessionDatum: string + let sessionsModule: Module + + beforeAll(async () => { + network = await toNetwork("BASE_SEPOLIA_FORKED") + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = getTestAccount(0) + sessionKeyAccount = privateKeyToAccount(generatePrivateKey()) // Generally belongs to the dapp + sessionPublicKey = sessionKeyAccount.address + testClient = toTestClient(chain, getTestAccount(5)) + }) + + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + /** + * This test demonstrates the creation and use of a smart session from two perspectives: + * + * 1. User Perspective (first test): + * - Create a Nexus client for the user's account + * - Install the smart sessions module on the user's account + * - Create a smart session with specific permissions + * + * 2. Dapp Perspective (second test): + * - Simulate a scenario where the user has left the dapp + * - Create a new Nexus client using the session key + * - Use the session to perform actions on behalf of the user + * + * This test showcases how smart sessions enable controlled, delegated actions + * on a user's smart account, even after the user is no longer actively engaged. + */ + test("should demonstrate creating a smart session from user's perspective", async () => { + // User Perspective: Creating and setting up the smart session + + // Create a Nexus client for the main account (eoaAccount) + // This client will be used to interact with the smart contract account + usersNexusClient = await createNexusClient({ + signer: eoaAccount, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + // Fund the account and deploy the smart contract wallet + await fundAndDeployClients(testClient, [usersNexusClient]) + + // Create a smart sessions module for the user's account + sessionsModule = toSmartSessionsValidator({ + account: usersNexusClient.account, + signer: eoaAccount + }) + + // Install the smart sessions module on the Nexus client's smart contract account + const hash = await usersNexusClient.installModule({ + module: sessionsModule.moduleInitData + }) + + // Extend the Nexus client with smart session creation actions + const nexusSessionClient = usersNexusClient.extend( + smartSessionCreateActions(sessionsModule) + ) + + // Wait for the module installation transaction to be mined and check its success + const { success: installSuccess } = + await usersNexusClient.waitForUserOperationReceipt({ hash }) + + expect(installSuccess).toBe(true) + + // Define the session parameters + // This includes the session key, validator, and action policies + const sessionRequestedInfo: CreateSessionDataParams[] = [ + { + sessionPublicKey, // Public key of the session + actionPoliciesInfo: [ + { + contractAddress: testAddresses.Counter, + functionSelector: "0x273ea3e3" as Hex // Selector for 'incrementNumber' + } + ] + } + ] + + // Create the smart session + const createSessionsResponse = await nexusSessionClient.grantPermission({ + sessionRequestedInfo + }) + + // Wait for the session creation transaction to be mined and check its success + const { success: sessionCreateSuccess } = + await usersNexusClient.waitForUserOperationReceipt({ + hash: createSessionsResponse.userOpHash + }) + + expect(installSuccess).toBe(sessionCreateSuccess) + + // Prepare the session data to be stored by the dApp. This could be saved in a Database by the dApp, or client side in local storage. + const sessionData: SessionData = { + granter: usersNexusClient.account.address, + sessionPublicKey, + moduleData: { + permissionIds: createSessionsResponse.permissionIds, + mode: SmartSessionMode.USE + } + } + + // Zip the session data, and store it for later use by a dapp + zippedSessionDatum = stringify(sessionData) + }, 200000) + + test("should demonstrate using a smart session from dapp's perspective", async () => { + // Now assume the user has left the dapp and the usersNexusClient signer is no longer available + // The following code demonstrates how a dapp can use the session to act on behalf of the user + + // Unzip the session data + const usersSessionData = parse(zippedSessionDatum) + + // Create a new Nexus client for the session + // This client will be used to interact with the smart contract account using the session key + const smartSessionNexusClient = await createNexusSessionClient({ + chain, + accountAddress: usersSessionData.granter, + signer: sessionKeyAccount, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + // Create a new smart sessions module with the session key + const usePermissionsModule = toSmartSessionsValidator({ + account: smartSessionNexusClient.account, + signer: sessionKeyAccount, + moduleData: usersSessionData.moduleData + }) + + // Extend the session client with smart session use actions + const useSmartSessionNexusClient = smartSessionNexusClient.extend( + smartSessionUseActions(usePermissionsModule) + ) + + // Use the session to perform an action (increment the counter) + const userOpHash = await useSmartSessionNexusClient.usePermission({ + calls: [ + { + to: testAddresses.Counter, + data: encodeFunctionData({ + abi: CounterAbi, + functionName: "incrementNumber" + }) + } + ] + }) + + // Wait for the action to be mined and check its success + const { success: sessionUseSuccess } = + await useSmartSessionNexusClient.waitForUserOperationReceipt({ + hash: userOpHash + }) + + expect(sessionUseSuccess).toBe(true) + }, 200000) // Test timeout set to 60 seconds +}) diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.test.ts new file mode 100644 index 000000000..98bab7cad --- /dev/null +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.test.ts @@ -0,0 +1,297 @@ +import { + http, + type Address, + type Chain, + type Hex, + type LocalAccount, + encodeFunctionData, + pad, + toBytes, + toHex +} from "viem" +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { MockRegistryAbi } from "../../../test/__contracts/abi" +import { CounterAbi } from "../../../test/__contracts/abi/CounterAbi" +import { testAddresses } from "../../../test/callDatas" +import { toNetwork } from "../../../test/testSetup" +import { + fundAndDeployClients, + getTestAccount, + killNetwork, + toTestClient +} from "../../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../../test/testUtils" +import { + type NexusClient, + createNexusClient +} from "../../clients/createNexusClient" +import { createNexusSessionClient } from "../../clients/createNexusSessionClient" +import { SIMPLE_SESSION_VALIDATOR_ADDRESS } from "../../constants" +import { parseReferenceValue } from "../utils/Helpers" +import type { Module } from "../utils/Types" +import policies from "./Helpers" +import type { CreateSessionDataParams } from "./Types" +import { ParamCondition } from "./Types" +import { smartSessionCreateActions, smartSessionUseActions } from "./decorators" +import { toSmartSessionsValidator } from "./toSmartSessionsValidator" + +describe("modules.smartSessions", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let eoaAccount: LocalAccount + let nexusClient: NexusClient + let cachedPermissionId: Hex + let sessionKeyAccount: LocalAccount + let sessionPublicKey: Address + + let sessionsModule: Module + + beforeAll(async () => { + network = await toNetwork("BASE_SEPOLIA_FORKED") + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = getTestAccount(0) + sessionKeyAccount = privateKeyToAccount(generatePrivateKey()) // Generally belongs to the dapp + sessionPublicKey = sessionKeyAccount.address + testClient = toTestClient(chain, getTestAccount(5)) + + nexusClient = await createNexusClient({ + signer: eoaAccount, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + sessionsModule = toSmartSessionsValidator({ + account: nexusClient.account, + signer: eoaAccount + }) + await fundAndDeployClients(testClient, [nexusClient]) + }) + + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test.concurrent("should have smart account bytecode", async () => { + const bytecodes = await Promise.all( + [testAddresses.SmartSession, testAddresses.UniActionPolicy].map( + (address) => testClient.getCode({ address }) + ) + ) + expect(bytecodes.every((bytecode) => !!bytecode?.length)).toBeTruthy() + }) + + test.concurrent( + "should parse a human friendly policy reference value to the hex version expected by the contracts", + async () => { + const TWO_THOUSAND_AS_HEX = + "0x00000000000000000000000000000000000000000000000000000000000007d0" + + expect(parseReferenceValue(BigInt(2000))).toBe(TWO_THOUSAND_AS_HEX) + expect(parseReferenceValue(2000)).toBe(TWO_THOUSAND_AS_HEX) + expect(parseReferenceValue("7d0")).toBe(TWO_THOUSAND_AS_HEX) + expect( + parseReferenceValue( + parseReferenceValue(pad(toHex(BigInt(2000)), { size: 32 })) + ) + ).toBe(TWO_THOUSAND_AS_HEX) + } + ) + + test.concurrent("should get a universal action policy", async () => { + const actionConfigData = { + valueLimitPerUse: BigInt(1000), + paramRules: { + length: 2, + rules: [ + { + condition: ParamCondition.EQUAL, + offsetIndex: 0, + isLimited: true, + ref: 1000, + usage: { + limit: BigInt(1000), + used: BigInt(10) + } + }, + { + condition: ParamCondition.LESS_THAN, + offsetIndex: 1, + isLimited: false, + ref: 2000, + usage: { + limit: BigInt(2000), + used: BigInt(100) + } + } + ] + } + } + const installUniversalPolicy = policies.to.universalAction(actionConfigData) + + expect(installUniversalPolicy.policy).toEqual(testAddresses.UniActionPolicy) + expect(installUniversalPolicy.initData).toBeDefined() + }) + + test.concurrent("should get a sudo action policy", async () => { + const installSudoActionPolicy = policies.sudo + expect(installSudoActionPolicy.policy).toBeDefined() + expect(installSudoActionPolicy.initData).toEqual("0x") + }) + + test.concurrent("should get a spending limit policy", async () => { + const installSpendingLimitPolicy = policies.to.spendingLimits([ + { + limit: BigInt(1000), + token: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + } + ]) + + expect(installSpendingLimitPolicy.policy).toBeDefined() + expect(installSpendingLimitPolicy.initData).toBeDefined() + }) + + test.concurrent( + "should have valid smartSessionValidator properties", + async () => { + const smartSessionValidator = toSmartSessionsValidator({ + account: nexusClient.account, + signer: eoaAccount + }) + expect(smartSessionValidator.signMessage).toBeDefined() + expect(smartSessionValidator.signUserOpHash).toBeDefined() + expect(smartSessionValidator.address).toBeDefined() + expect(smartSessionValidator.initData).toBeDefined() + expect(smartSessionValidator.deInitData).toBeDefined() + expect(smartSessionValidator.signer).toBeDefined() + expect(smartSessionValidator.type).toBeDefined() + } + ) + + test.concurrent( + "should install sessions module with no init data", + async () => { + const isInstalledBefore = await nexusClient.isModuleInstalled({ + module: sessionsModule.moduleInitData + }) + + if (!isInstalledBefore) { + const hash = await nexusClient.installModule({ + module: sessionsModule.moduleInitData + }) + + const { success: installSuccess } = + await nexusClient.waitForUserOperationReceipt({ hash }) + expect(installSuccess).toBe(true) + } + + const isInstalledAfter = await nexusClient.isModuleInstalled({ + module: sessionsModule + }) + expect(isInstalledAfter).toBe(true) + } + ) + + test("should create Counter increment session (USE mode) on installed smart session validator", async () => { + const isInstalledBefore = await nexusClient.isModuleInstalled({ + module: sessionsModule + }) + + expect(isInstalledBefore).toBe(true) + + // Note: grantPermission decorator will take care of trusting the attester. + + // session key signer address is declared here + const sessionRequestedInfo: CreateSessionDataParams[] = [ + { + sessionPublicKey, // session key signer + actionPoliciesInfo: [ + { + contractAddress: testAddresses.Counter, // counter address + functionSelector: "0x273ea3e3" as Hex // function selector for increment count + } + ] + } + ] + + const nexusSessionClient = nexusClient.extend( + smartSessionCreateActions(sessionsModule) + ) + + const createSessionsResponse = await nexusSessionClient.grantPermission({ + sessionRequestedInfo + }) + + expect(createSessionsResponse.userOpHash).toBeDefined() + expect(createSessionsResponse.permissionIds).toBeDefined() + ;[cachedPermissionId] = createSessionsResponse.permissionIds + + const receipt = await nexusClient.waitForUserOperationReceipt({ + hash: createSessionsResponse.userOpHash + }) + + expect(receipt.success).toBe(true) + }, 200000) + + test("should make use of already enabled session (USE mode) to increment a counter using a session key", async () => { + const counterBefore = await testClient.readContract({ + address: testAddresses.Counter, + abi: CounterAbi, + functionName: "getNumber" + }) + + const smartSessionNexusClient = await createNexusSessionClient({ + chain, + accountAddress: nexusClient.account.address, + signer: sessionKeyAccount, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + const usePermissionsModule = toSmartSessionsValidator({ + account: smartSessionNexusClient.account, + signer: sessionKeyAccount, + moduleData: { + permissionIds: [cachedPermissionId] + } + }) + + const useSmartSessionNexusClient = smartSessionNexusClient.extend( + smartSessionUseActions(usePermissionsModule) + ) + + const userOpHash = await useSmartSessionNexusClient.usePermission({ + calls: [ + { + to: testAddresses.Counter, + data: encodeFunctionData({ + abi: CounterAbi, + functionName: "incrementNumber" + }) + } + ] + }) + + expect(userOpHash).toBeDefined() + const receipt = + await useSmartSessionNexusClient.waitForUserOperationReceipt({ + hash: userOpHash + }) + expect(receipt.success).toBe(true) + + const counterAfter = await testClient.readContract({ + address: testAddresses.Counter, + abi: CounterAbi, + functionName: "getNumber" + }) + + expect(counterAfter).toBe(counterBefore + BigInt(1)) + }, 200000) +}) diff --git a/src/sdk/modules/smartSessions/toSmartSessions.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.ts similarity index 64% rename from src/sdk/modules/smartSessions/toSmartSessions.ts rename to src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.ts index 3962ee325..7f4ca78e6 100644 --- a/src/sdk/modules/smartSessions/toSmartSessions.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.ts @@ -1,41 +1,39 @@ import { + SMART_SESSIONS_ADDRESS, SmartSessionMode, encodeSmartSessionSignature } from "@rhinestone/module-sdk" -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" import { type Address, type Hex, encodePacked } from "viem" -import addresses from "../../__contracts/addresses" +import type { ModuleMeta } from "../../modules/utils/Types" import type { ModularSmartAccount } from "../utils/Types" import type { Module, ModuleParameters } from "../utils/Types" -import { type ToModuleParameters, toModule } from "../utils/toModule" -import type { UseSessionModuleData } from "./Types" +import { toModule } from "../utils/toModule" +import type { UsePermissionModuleData } from "./Types" const DUMMY_ECDSA_SIG = "0xe8b94748580ca0b4993c9a1b86b5be851bfc076ff5ce3a1ff65bf16392acfcb800f9b4f1aef1555c7fce5599fffb17e7c635502154a0333ba21f3ae491839af51c" -/** - * Represents the implementation parameters for a Smart Session module. - */ -export type SmartSessionImplementation = ModuleParameters & { - moduleData?: UseSessionModuleData +export type SmartSessionModule = Module & { + sigGen: (signature: Hex) => Hex + moduleData?: UsePermissionModuleData } /** * Arguments for getting the initialization data for a Use Session module. */ -export type UseSessionModuleGetInitDataArgs = { +export type UsePermissionModuleGetInitDataArgs = { signerAddress: Address } /** * Parameters for creating a Use Session module. */ -export type UseSessionModuleParameters = Omit< - ToModuleParameters, - "accountAddress" +export type UsePermissionModuleParameters = Omit< + ModuleParameters, + "accountAddress" | "address" > & { account: ModularSmartAccount - moduleData?: UseSessionModuleData + moduleData?: UsePermissionModuleData } /** @@ -44,10 +42,10 @@ export type UseSessionModuleParameters = Omit< * @param _ - Optional arguments (currently unused). * @returns The module metadata including address, type, and initialization data. */ -export const getUseSessionModuleInitData = ( - _?: UseSessionModuleGetInitDataArgs +export const getUsePermissionModuleInitData = ( + _?: UsePermissionModuleGetInitDataArgs ): ModuleMeta => ({ - module: addresses.SmartSession, + address: SMART_SESSIONS_ADDRESS, type: "validator", initData: "0x" }) @@ -58,9 +56,9 @@ export const getUseSessionModuleInitData = ( * @param signerAddress - The address of the signer for the session. * @returns The encoded initialization data as a hexadecimal string. */ -export const getUseSessionInitData = ({ +export const getUsePermissionInitData = ({ signerAddress -}: UseSessionModuleGetInitDataArgs): Hex => +}: UsePermissionModuleGetInitDataArgs): Hex => encodePacked(["address"], [signerAddress]) /** @@ -74,7 +72,7 @@ export const getUseSessionInitData = ({ * * @example * ```typescript - * const smartSessionsModule = toSmartSessions({ + * const smartSessionsModule = toSmartSessionsValidator({ * account: mySmartAccount, * signer: mySigner, * moduleData: { @@ -90,9 +88,9 @@ export const getUseSessionInitData = ({ * - It uses the SmartSession address from the predefined addresses. * - The default session mode is USE if not specified. */ -export const toSmartSessions = ( - parameters: UseSessionModuleParameters -): Module => { +export const toSmartSessionsValidator = ( + parameters: UsePermissionModuleParameters +): SmartSessionModule => { const { account, signer, @@ -102,38 +100,51 @@ export const toSmartSessions = ( moduleInitArgs: moduleInitArgs_ = { signerAddress: signer.address }, initArgs: initArgs_ = { signerAddress: signer.address }, moduleData: { - permissionId = "0x", + permissionIdIndex = 0, + permissionIds = [], mode = SmartSessionMode.USE, - enableSessionData + enableSessionData, + keyGenData: _ } = {} } = parameters - const initData = initData_ ?? getUseSessionInitData(initArgs_) + const initData = initData_ ?? getUsePermissionInitData(initArgs_) const moduleInitData = - moduleInitData_ ?? getUseSessionModuleInitData(moduleInitArgs_) + moduleInitData_ ?? getUsePermissionModuleInitData(moduleInitArgs_) return toModule({ + ...parameters, signer, accountAddress: account.address, - address: addresses.SmartSession, + address: SMART_SESSIONS_ADDRESS, initData, moduleInitData, deInitData, getStubSignature: async () => encodeSmartSessionSignature({ mode, - permissionId, + permissionId: permissionIds[permissionIdIndex], enableSessionData, signature: DUMMY_ECDSA_SIG }), signUserOpHash: async (userOpHash: Hex) => encodeSmartSessionSignature({ mode, - permissionId, + permissionId: permissionIds[permissionIdIndex], enableSessionData, signature: await signer.signMessage({ message: { raw: userOpHash as Hex } }) - }) - }) + }), + extend: { + sigGen: (signature: Hex): Hex => { + return encodeSmartSessionSignature({ + mode, + permissionId: permissionIds[permissionIdIndex], + enableSessionData, + signature + }) + } + } + }) as SmartSessionModule } diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.uni.policy.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.uni.policy.test.ts new file mode 100644 index 000000000..7856a883d --- /dev/null +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.uni.policy.test.ts @@ -0,0 +1,319 @@ +import { + http, + type AbiFunction, + type Address, + type Chain, + type Hex, + type LocalAccount, + type PublicClient, + encodeFunctionData, + getContract, + slice, + toBytes, + toFunctionSelector, + toHex +} from "viem" +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { MockRegistryAbi } from "../../../test/__contracts/abi" +import { MockCalleeAbi } from "../../../test/__contracts/abi/MockCalleeAbi" +import { testAddresses } from "../../../test/callDatas" +import { toNetwork } from "../../../test/testSetup" +import { + fundAndDeployClients, + getTestAccount, + killNetwork, + toTestClient +} from "../../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../../test/testUtils" +import { + type NexusClient, + createNexusClient +} from "../../clients/createNexusClient" +import { createNexusSessionClient } from "../../clients/createNexusSessionClient" +import { SMART_SESSIONS_ADDRESS } from "../../constants" +import type { Module } from "../utils/Types" +import { isPermissionEnabled } from "./Helpers" +import type { CreateSessionDataParams, Rule } from "./Types" +import { ParamCondition } from "./Types" +import { smartSessionCreateActions, smartSessionUseActions } from "./decorators" +import { toSmartSessionsValidator } from "./toSmartSessionsValidator" + +describe("modules.smartSessions.uniPolicy", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let eoaAccount: LocalAccount + let nexusClient: NexusClient + let nexusAccountAddress: Address + let sessionKeyAccount: LocalAccount + let sessionPublicKey: Address + let cachedPermissionId: Hex + + let sessionsModule: Module + + beforeAll(async () => { + network = await toNetwork("BASE_SEPOLIA_FORKED") + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = getTestAccount(0) + sessionKeyAccount = privateKeyToAccount(generatePrivateKey()) // Generally belongs to the dapp + sessionPublicKey = sessionKeyAccount.address + + testClient = toTestClient(chain, getTestAccount(5)) + + nexusClient = await createNexusClient({ + signer: eoaAccount, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() + + sessionsModule = toSmartSessionsValidator({ + account: nexusClient.account, + signer: eoaAccount + }) + + await fundAndDeployClients(testClient, [nexusClient]) + }) + + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test("should add balance to mock callee", async () => { + const mockContract = getContract({ + address: testAddresses.MockCallee, + abi: MockCalleeAbi, + client: testClient + }) + const balUint = 123n + const balBytes32 = `0x${balUint.toString(16).padStart(64, "0")}` + const balancesBefore = await mockContract.read.bals([nexusAccountAddress]) + const hash = await nexusClient.sendTransaction({ + calls: [ + { + to: testAddresses.MockCallee, + data: encodeFunctionData({ + abi: MockCalleeAbi, + functionName: "addBalance", + args: [nexusAccountAddress, balUint, balBytes32 as Hex] + }) + } + ] + }) + const { status } = await nexusClient.waitForTransactionReceipt({ hash }) + expect(status).toBe("success") + const balanceAfter = await mockContract.read.bals([nexusAccountAddress]) + expect(balanceAfter[0]).toBeGreaterThan(balancesBefore[0]) + }, 90000) + + test("should install smartSessionValidator with no init data", async () => { + const isInstalledBefore = await nexusClient.isModuleInstalled({ + module: sessionsModule.moduleInitData + }) + + if (!isInstalledBefore) { + const hash = await nexusClient.installModule({ + module: sessionsModule.moduleInitData + }) + + const { success: installSuccess } = + await nexusClient.waitForUserOperationReceipt({ hash }) + expect(installSuccess).toBe(true) + } + + const isInstalledAfter = await nexusClient.isModuleInstalled({ + module: { + type: "validator", + address: SMART_SESSIONS_ADDRESS + } + }) + expect(isInstalledAfter).toBe(true) + }) + + test("should create MockCallee add balance session (USE mode) on installed smart session validator", async () => { + const isInstalledBefore = await nexusClient.isModuleInstalled({ + module: { + type: "validator", + address: SMART_SESSIONS_ADDRESS + } + }) + + expect(isInstalledBefore).toBe(true) + + const smartSessionNexusClient = nexusClient.extend( + smartSessionCreateActions(sessionsModule) + ) + + const trustAttestersHash = await smartSessionNexusClient.trustAttesters() + const userOpReceipt = + await smartSessionNexusClient.waitForUserOperationReceipt({ + hash: trustAttestersHash + }) + const { status } = await testClient.waitForTransactionReceipt({ + hash: userOpReceipt.receipt.transactionHash + }) + expect(status).toBe("success") + + const functionSelector = "addBalance(address,uint256,bytes32)" + + const unparsedFunctionSelector = functionSelector as AbiFunction | string + const parsedFunctionSelector = slice( + toFunctionSelector(unparsedFunctionSelector), + 0, + 4 + ) + + const maxUintDeposit = 123456n + const minBytes32Deposit = `0x${maxUintDeposit + .toString(16) + .padStart(64, "0")}` + + const rules: Rule[] = [ + { + condition: ParamCondition.EQUAL, + offsetIndex: 0, + isLimited: false, + ref: nexusAccountAddress, + usage: { + limit: BigInt(0), + used: BigInt(0) + } + }, + { + condition: ParamCondition.LESS_THAN, + offsetIndex: 1, + isLimited: true, + ref: maxUintDeposit, + usage: { + limit: BigInt(maxUintDeposit), + used: BigInt(0) + } + }, + { + condition: ParamCondition.GREATER_THAN, + offsetIndex: 2, + isLimited: false, + ref: minBytes32Deposit, + usage: { + limit: BigInt(0), + used: BigInt(0) + } + } + ] + + const sessionRequestedInfo: CreateSessionDataParams[] = [ + { + sessionPublicKey, + actionPoliciesInfo: [ + { + contractAddress: testAddresses.MockCallee, // mock callee address + functionSelector: parsedFunctionSelector, // addBalance function selector + rules + } + ] + } + ] + + const createSessionsResponse = + await smartSessionNexusClient.grantPermission({ sessionRequestedInfo }) + + expect(createSessionsResponse.userOpHash).toBeDefined() + expect(createSessionsResponse.permissionIds).toBeDefined() + ;[cachedPermissionId] = createSessionsResponse.permissionIds + + const receipt = await nexusClient.waitForUserOperationReceipt({ + hash: createSessionsResponse.userOpHash + }) + + expect(receipt.success).toBe(true) + + const isEnabled = await isPermissionEnabled({ + client: nexusClient.account.client as PublicClient, + accountAddress: nexusClient.account.address, + permissionId: cachedPermissionId + }) + expect(isEnabled).toBe(true) + }, 200000) + + test("should make use of already enabled session (USE mode) to add balance to MockCallee using a session key", async () => { + const isEnabled = await isPermissionEnabled({ + client: nexusClient.account.client as PublicClient, + accountAddress: nexusClient.account.address, + permissionId: cachedPermissionId + }) + expect(isEnabled).toBe(true) + + const mockContract = getContract({ + address: testAddresses.MockCallee, + abi: MockCalleeAbi, + client: testClient + }) + + // Note: if you try to add more than maxUintDeposit then you would get this below error. + // Error: https://openchain.xyz/signatures?query=0x3b577361 + const balToAddUint = 1234n + + // Note: if you try to add less than minBytes32Deposit then you would get this below error. + // Error: https://openchain.xyz/signatures?query=0x3b577361 + const balToAddBytes32 = `0x${BigInt(1234567) + .toString(16) + .padStart(64, "0")}` + + const balancesBefore = await mockContract.read.bals([nexusAccountAddress]) + + // helpful for out of range test. If time range limit has been provided in the policy. + // await testClient.setNextBlockTimestamp({ + // timestamp: 9727001666n + // }) + + const smartSessionNexusClient = await createNexusSessionClient({ + chain, + accountAddress: nexusClient.account.address, + signer: sessionKeyAccount, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + const usePermissionsModule = toSmartSessionsValidator({ + account: smartSessionNexusClient.account, + signer: sessionKeyAccount, + moduleData: { + permissionIds: [cachedPermissionId] + } + }) + + const useSmartSessionNexusClient = smartSessionNexusClient.extend( + smartSessionUseActions(usePermissionsModule) + ) + + const userOpHash = await useSmartSessionNexusClient.usePermission({ + calls: [ + { + to: testAddresses.MockCallee, + data: encodeFunctionData({ + abi: MockCalleeAbi, + functionName: "addBalance", + args: [nexusAccountAddress, balToAddUint, balToAddBytes32 as Hex] + }) + } + ] + }) + + expect(userOpHash).toBeDefined() + const receipt = await nexusClient.waitForUserOperationReceipt({ + hash: userOpHash + }) + expect(receipt.success).toBe(true) + + const balanceAfter = await mockContract.read.bals([nexusAccountAddress]) + expect(balanceAfter[0]).toBeGreaterThan(balancesBefore[0]) + }, 200000) +}) diff --git a/src/sdk/modules/utils/Helpers.ts b/src/sdk/modules/utils/Helpers.ts index 4a2dd9427..6f9ae57dc 100644 --- a/src/sdk/modules/utils/Helpers.ts +++ b/src/sdk/modules/utils/Helpers.ts @@ -1,20 +1,44 @@ -import { type ByteArray, type Hex, isHex, pad, toHex } from "viem" +import { + type ByteArray, + type Chain, + type Client, + type Hex, + type Transport, + isHex, + pad, + toHex +} from "viem" import { ERROR_MESSAGES } from "../../account/index.js" +import type { AnyData, ModularSmartAccount } from "./Types.js" + +/** + * Represents a hardcoded hex value reference. + * Used when you want to bypass automatic hex conversion. + */ export type HardcodedReference = { + /** The raw hex value */ raw: Hex } + +/** + * Base types that can be converted to hex references. + */ type BaseReferenceValue = string | number | bigint | boolean | ByteArray + +/** + * Union type of all possible reference values that can be converted to hex. + * Includes both basic types and hardcoded references. + */ export type AnyReferenceValue = BaseReferenceValue | HardcodedReference + /** + * Parses a reference value into a 32-byte hex string. + * Handles various input types including Ethereum addresses, numbers, booleans, and raw hex values. * - * parseReferenceValue - * - * Parses the reference value to a hex string. - * The reference value can be hardcoded using the {@link HardcodedReference} type. - * Otherwise, it can be a string, number, bigint, boolean, or ByteArray. + * @param referenceValue - The value to convert to hex + * @returns A 32-byte hex string (66 characters including '0x' prefix) * - * @param referenceValue {@link AnyReferenceValue} - * @returns Hex + * @throws {Error} If the resulting hex string is invalid or not 32 bytes */ export function parseReferenceValue(referenceValue: AnyReferenceValue): Hex { let result: Hex @@ -45,6 +69,13 @@ export function parseReferenceValue(referenceValue: AnyReferenceValue): Hex { return result } +/** + * Sanitizes an ECDSA signature by ensuring the 'v' value is either 27 or 28. + * Also ensures the signature has a '0x' prefix. + * + * @param signature - The hex signature to sanitize + * @returns A properly formatted signature with correct 'v' value + */ export function sanitizeSignature(signature: Hex): Hex { let signature_ = signature const potentiallyIncorrectV = Number.parseInt(signature_.slice(-2), 16) @@ -57,3 +88,24 @@ export function sanitizeSignature(signature: Hex): Hex { } return signature_ as Hex } + +/** + * Extracts and validates the active module from a client's account. + * + * @param client - The viem Client instance with an optional modular smart account + * @returns The active module from the account + * + * @throws {Error} If no module is currently activated + */ +export const parseModule = < + TModularSmartAccount extends ModularSmartAccount | undefined, + chain extends Chain | undefined +>( + client: Client +): AnyData => { + const activeModule = client?.account?.getModule() + if (!activeModule) { + throw new Error(ERROR_MESSAGES.MODULE_NOT_ACTIVATED) + } + return activeModule +} diff --git a/src/sdk/modules/utils/Types.ts b/src/sdk/modules/utils/Types.ts index 38f30c762..f040d356f 100644 --- a/src/sdk/modules/utils/Types.ts +++ b/src/sdk/modules/utils/Types.ts @@ -1,5 +1,4 @@ -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" -import type { Address, Chain, Hex, SignableMessage } from "viem" +import type { Address, Assign, Chain, Hex, SignableMessage } from "viem" import type { SmartAccount } from "viem/account-abstraction" import type { Signer } from "./../../account/utils/toSigner" export type ModuleVersion = "1.0.0" // | 'V1_0_1' @@ -68,23 +67,34 @@ export type ModuleActions = { export type ModuleParameters = { /** The hexadecimal address of the module. */ address: Hex - /** Initialization data for the module. */ - initData: Hex - /** De-initialization data for the module. */ - deInitData: Hex /** Signer of the Module. */ signer: Signer + /** account */ + account?: ModularSmartAccount + /** Data for the module */ + data?: Record +} & Partial & + Partial + +export type RequiredModuleParameters = { + /** Optional initialization data for the module. */ + initData: Hex + /** Optional metadata for module initialization. */ + moduleInitData: ModuleMeta + /** Optional data for de-initializing the module. */ + deInitData: Hex + /** Optional arguments for module initialization. */ + moduleInitArgs: AnyData + /** Optional arguments for initialization. */ + initArgs: AnyData /** The smart account address */ accountAddress: Hex - /** The module initData */ - moduleInitData: ModuleMeta - /** The module initArgs */ - moduleInitArgs?: AnyData - /** The initArgs for initData */ - initArgs?: AnyData -} & Partial + /** Extend the Module with custom properties. */ + extend?: extend | undefined +} -export type Module = ModuleParameters & +export type BaseModule = Omit & + RequiredModuleParameters & ModuleActions & { /** For compatibility with module-sdk. */ module: Hex @@ -92,11 +102,25 @@ export type Module = ModuleParameters & signer: Signer /** Type of module. */ type: ModuleType + /** Data to be set on the module */ + setData: (r: Record) => void + /** Get data from the module */ + getData: () => Record } +export type Module = + Assign + export type Modularity = { getModule: () => Module | undefined setModule: (module: Module) => void } export type ModularSmartAccount = SmartAccount & Modularity + +export type ModuleMeta = { + address: Hex + type: ModuleType + initData?: Hex + deInitData?: Hex +} diff --git a/src/sdk/modules/utils/index.ts b/src/sdk/modules/utils/index.ts new file mode 100644 index 000000000..b0f5a08e5 --- /dev/null +++ b/src/sdk/modules/utils/index.ts @@ -0,0 +1,5 @@ +export * from "./Types" +export * from "./toModule" +export * from "./Helpers" +export * from "./Uid" +export * from "./Constants" diff --git a/src/sdk/modules/utils/toModule.test.ts b/src/sdk/modules/utils/toModule.test.ts index 01e77a943..1095d6a84 100644 --- a/src/sdk/modules/utils/toModule.test.ts +++ b/src/sdk/modules/utils/toModule.test.ts @@ -47,7 +47,7 @@ describe("modules.toModule", async () => { deInitData: "0x", signer: eoaAccount, moduleInitData: { - module: "0x0000000000000000000000000000000000000000", + address: "0x0000000000000000000000000000000000000000", type: "validator", initData: "0x" } @@ -58,14 +58,18 @@ describe("modules.toModule", async () => { "accountAddress": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "address": "0x0000000000000000000000000000000000000000", "deInitData": "0x", + "getData": [Function], "getStubSignature": [Function], + "initArgs": {}, "initData": "0x", "module": "0x0000000000000000000000000000000000000000", + "moduleInitArgs": "0x", "moduleInitData": { + "address": "0x0000000000000000000000000000000000000000", "initData": "0x", - "module": "0x0000000000000000000000000000000000000000", "type": "validator", }, + "setData": [Function], "signMessage": [Function], "signUserOpHash": [Function], "signer": { diff --git a/src/sdk/modules/utils/toModule.ts b/src/sdk/modules/utils/toModule.ts index 19b0a1345..2eda70cc9 100644 --- a/src/sdk/modules/utils/toModule.ts +++ b/src/sdk/modules/utils/toModule.ts @@ -1,36 +1,14 @@ -import type { Module as ModuleMeta } from "@rhinestone/module-sdk" import type { Hex, SignableMessage } from "viem" -import type { Signer } from "../../account/utils/toSigner.js" import { sanitizeSignature } from "./Helpers.js" -import type { AnyData, Module, ModuleParameters } from "./Types.js" +import type { Module, ModuleParameters } from "./Types.js" /** - * Parameters for creating a module. - */ -export type ToModuleParameters = { - /** The signer associated with the module. */ - signer: Signer - /** The address of the account that the module is associated with. */ - accountAddress: Hex - /** Optional initialization data for the module. */ - initData?: Hex - /** Optional metadata for module initialization. */ - moduleInitData?: ModuleMeta - /** Optional data for de-initializing the module. */ - deInitData?: Hex - /** Optional arguments for module initialization. */ - moduleInitArgs?: AnyData - /** Optional arguments for initialization. */ - initArgs?: AnyData -} - -/** - * Creates a Module object from the given implementation parameters. + * Creates a Module object from the given parameters parameters. * - * This function takes the module implementation details and constructs a standardized + * This function takes the module parameters details and constructs a standardized * Module object with methods for signing and generating stub signatures. * - * @param implementation - The parameters defining the module implementation. + * @param parameters - The parameters defining the module parameters. * @returns A Module object with standardized methods and properties. * * @example @@ -49,36 +27,51 @@ export type ToModuleParameters = { * - The `getStubSignature` method generates a dummy signature for testing or placeholder purposes. * - The `signUserOpHash` and `signMessage` methods use the provided signer to create actual signatures. */ -export function toModule(implementation: ModuleParameters): Module { +export function toModule(parameters: ModuleParameters): Module { const { - accountAddress, - address, - initData, - deInitData, - signer, - moduleInitData, + account, + extend, + initArgs = {}, + deInitData = "0x", + initData = "0x", + moduleInitArgs = "0x", + accountAddress = account?.address ?? "0x", + moduleInitData = { + address: "0x", + type: "validator" + }, ...rest - } = implementation + } = parameters + + let data_ = parameters.data ?? {} + const setData = (d: Record) => { + data_ = d + } + const getData = () => data_ return { - address, - module: address, - accountAddress, - moduleInitData, - signer, - type: "validator", + ...parameters, initData, + moduleInitData, + moduleInitArgs, deInitData, + accountAddress, + initArgs, + setData, + getData, + module: parameters.address, + type: "validator", getStubSignature: async () => { - const dynamicPart = address.substring(2).padEnd(40, "0") + const dynamicPart = parameters.address.substring(2).padEnd(40, "0") return `0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000${dynamicPart}000000000000000000000000000000000000000000000000000000000000004181d4b4981670cb18f99f0b4a66446df1bf5b204d24cfcb659bf38ba27a4359b5711649ec2423c5e1247245eba2964679b6a1dbb85c992ae40b9b00c6935b02ff1b00000000000000000000000000000000000000000000000000000000000000` as Hex }, signUserOpHash: async (userOpHash: Hex) => - await signer.signMessage({ + await parameters.signer.signMessage({ message: { raw: userOpHash } }), signMessage: async (message: SignableMessage) => - sanitizeSignature(await signer.signMessage({ message })), + sanitizeSignature(await parameters.signer.signMessage({ message })), + ...extend, ...rest } } diff --git a/src/test/README.md b/src/test/README.md index 92656bfa3..0cf7d7178 100644 --- a/src/test/README.md +++ b/src/test/README.md @@ -30,7 +30,7 @@ bun run lint --apply-unsafe > **Note**: > - Do not edit these files manually; they will be overridden if/when a new Nexus deployment occurs. -> - Avoid hardcoding important addresses (e.g., `const K1_VALIDATOR_ADDRESS = "0x"`). Use `./src/addresses.ts` instead. +> - Avoid hardcoding important addresses (e.g., `const k1ValidatorAddress = "0x"`). Use `./src/addresses.ts` instead. ## Network Scopes for Tests @@ -57,7 +57,7 @@ localhostTest("should be used in the following way", async({ config: { bundlerUr ``` > **Note:** -> Please avoid using multiple nested describe() blocks in a single test file, as it is unnecessary and can lead to confusion regarding network scope. +> Please avoid using multiple nested describe blocks in a single test file, as it is unnecessary and can lead to confusion regarding network scope. > Using *many* test files is preferable, as describe blocks run in parallel. ## Testing on Testnets or New Chains diff --git a/src/test/__contracts/abi/MockAttesterAbi.ts b/src/test/__contracts/abi/MockAttesterAbi.ts new file mode 100644 index 000000000..4fd715ea7 --- /dev/null +++ b/src/test/__contracts/abi/MockAttesterAbi.ts @@ -0,0 +1,40 @@ +export const MockAttesterAbi = [ + { + name: "attest", + type: "function", + inputs: [ + { + name: "", + type: "address" + }, + { + name: "", + type: "bytes32" + }, + { + name: "", + type: "tuple", + components: [ + { + name: "", + type: "address" + }, + { + name: "", + type: "uint48" + }, + { + name: "", + type: "bytes" + }, + { + name: "", + type: "uint256[]" + } + ] + } + ], + outputs: [], + stateMutability: "nonpayable" + } +] as const diff --git a/src/test/__contracts/abi/MockRegistryAbi.ts b/src/test/__contracts/abi/MockRegistryAbi.ts index 0b058e905..5215a2c43 100644 --- a/src/test/__contracts/abi/MockRegistryAbi.ts +++ b/src/test/__contracts/abi/MockRegistryAbi.ts @@ -1,69 +1,361 @@ export const MockRegistryAbi = [ + { inputs: [], name: "AccessDenied", type: "error" }, + { inputs: [], name: "AlreadyAttested", type: "error" }, + { + inputs: [{ internalType: "address", name: "module", type: "address" }], + name: "AlreadyRegistered", + type: "error" + }, + { inputs: [], name: "AlreadyRevoked", type: "error" }, + { inputs: [], name: "AttestationNotFound", type: "error" }, + { inputs: [], name: "DifferentResolvers", type: "error" }, + { inputs: [], name: "ExternalError_ModuleRegistration", type: "error" }, + { inputs: [], name: "ExternalError_ResolveAttestation", type: "error" }, + { inputs: [], name: "ExternalError_ResolveRevocation", type: "error" }, + { inputs: [], name: "ExternalError_SchemaValidation", type: "error" }, + { + inputs: [{ internalType: "address", name: "factory", type: "address" }], + name: "FactoryCallFailed", + type: "error" + }, + { inputs: [], name: "InsufficientAttestations", type: "error" }, + { inputs: [], name: "InvalidAddress", type: "error" }, + { inputs: [], name: "InvalidAttestation", type: "error" }, + { inputs: [], name: "InvalidDeployment", type: "error" }, + { inputs: [], name: "InvalidExpirationTime", type: "error" }, + { inputs: [], name: "InvalidModuleType", type: "error" }, + { inputs: [], name: "InvalidModuleTypes", type: "error" }, + { + inputs: [ + { + internalType: "contract IExternalResolver", + name: "resolver", + type: "address" + } + ], + name: "InvalidResolver", + type: "error" + }, + { + inputs: [{ internalType: "ResolverUID", name: "uid", type: "bytes32" }], + name: "InvalidResolverUID", + type: "error" + }, + { inputs: [], name: "InvalidSalt", type: "error" }, + { inputs: [], name: "InvalidSchema", type: "error" }, + { + inputs: [ + { + internalType: "contract IExternalSchemaValidator", + name: "validator", + type: "address" + } + ], + name: "InvalidSchemaValidator", + type: "error" + }, + { inputs: [], name: "InvalidSignature", type: "error" }, + { inputs: [], name: "InvalidThreshold", type: "error" }, + { inputs: [], name: "InvalidTrustedAttesterInput", type: "error" }, + { + inputs: [ + { internalType: "address", name: "moduleAddress", type: "address" } + ], + name: "ModuleAddressIsNotContract", + type: "error" + }, + { + inputs: [{ internalType: "address", name: "module", type: "address" }], + name: "ModuleNotFoundInRegistry", + type: "error" + }, + { inputs: [], name: "NoTrustedAttestersFound", type: "error" }, + { inputs: [], name: "ResolverAlreadyExists", type: "error" }, + { + inputs: [{ internalType: "address", name: "attester", type: "address" }], + name: "RevokedAttestation", + type: "error" + }, + { + inputs: [{ internalType: "SchemaUID", name: "uid", type: "bytes32" }], + name: "SchemaAlreadyExists", + type: "error" + }, { anonymous: false, inputs: [ + { + indexed: true, + internalType: "address", + name: "moduleAddress", + type: "address" + }, + { + indexed: true, + internalType: "address", + name: "attester", + type: "address" + }, { indexed: false, + internalType: "SchemaUID", + name: "schemaUID", + type: "bytes32" + }, + { + indexed: true, + internalType: "AttestationDataRef", + name: "sstore2Pointer", + type: "address" + } + ], + name: "Attested", + type: "event" + }, + { + anonymous: false, + inputs: [ + { + indexed: true, internalType: "address", - name: "sender", + name: "implementation", type: "address" } ], - name: "Log", + name: "ModuleRegistration", type: "event" }, { anonymous: false, - inputs: [], + inputs: [ + { + indexed: true, + internalType: "ResolverUID", + name: "uid", + type: "bytes32" + }, + { + indexed: true, + internalType: "address", + name: "resolver", + type: "address" + } + ], + name: "NewResolver", + type: "event" + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "ResolverUID", + name: "uid", + type: "bytes32" + }, + { + indexed: false, + internalType: "address", + name: "newOwner", + type: "address" + } + ], + name: "NewResolverOwner", + type: "event" + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "smartAccount", + type: "address" + } + ], name: "NewTrustedAttesters", type: "event" }, { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "contract IExternalResolver", + name: "resolver", + type: "address" + } + ], + name: "ResolverRevocationError", + type: "event" + }, + { + anonymous: false, inputs: [ { + indexed: true, internalType: "address", - name: "module", + name: "moduleAddress", type: "address" }, { - internalType: "address[]", - name: "attesters", - type: "address[]" + indexed: true, + internalType: "address", + name: "revoker", + type: "address" }, { - internalType: "uint256", - name: "threshold", - type: "uint256" + indexed: false, + internalType: "SchemaUID", + name: "schema", + type: "bytes32" } ], - name: "check", - outputs: [], - stateMutability: "view", - type: "function" + name: "Revoked", + type: "event" }, { + anonymous: false, inputs: [ { + indexed: true, + internalType: "SchemaUID", + name: "uid", + type: "bytes32" + }, + { + indexed: true, internalType: "address", - name: "module", + name: "registerer", type: "address" - }, + } + ], + name: "SchemaRegistered", + type: "event" + }, + { + inputs: [ + { internalType: "SchemaUID", name: "schemaUID", type: "bytes32" }, + { internalType: "address", name: "attester", type: "address" }, { - internalType: "uint256", - name: "moduleType", - type: "uint256" + components: [ + { internalType: "address", name: "moduleAddress", type: "address" }, + { internalType: "uint48", name: "expirationTime", type: "uint48" }, + { internalType: "bytes", name: "data", type: "bytes" }, + { + internalType: "ModuleType[]", + name: "moduleTypes", + type: "uint256[]" + } + ], + internalType: "struct AttestationRequest[]", + name: "requests", + type: "tuple[]" }, + { internalType: "bytes", name: "signature", type: "bytes" } + ], + name: "attest", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { internalType: "SchemaUID", name: "schemaUID", type: "bytes32" }, + { internalType: "address", name: "attester", type: "address" }, { - internalType: "address[]", - name: "attesters", - type: "address[]" + components: [ + { internalType: "address", name: "moduleAddress", type: "address" }, + { internalType: "uint48", name: "expirationTime", type: "uint48" }, + { internalType: "bytes", name: "data", type: "bytes" }, + { + internalType: "ModuleType[]", + name: "moduleTypes", + type: "uint256[]" + } + ], + internalType: "struct AttestationRequest", + name: "request", + type: "tuple" }, + { internalType: "bytes", name: "signature", type: "bytes" } + ], + name: "attest", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { internalType: "SchemaUID", name: "schemaUID", type: "bytes32" }, { - internalType: "uint256", - name: "threshold", - type: "uint256" + components: [ + { internalType: "address", name: "moduleAddress", type: "address" }, + { internalType: "uint48", name: "expirationTime", type: "uint48" }, + { internalType: "bytes", name: "data", type: "bytes" }, + { + internalType: "ModuleType[]", + name: "moduleTypes", + type: "uint256[]" + } + ], + internalType: "struct AttestationRequest", + name: "request", + type: "tuple" } ], + name: "attest", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { internalType: "SchemaUID", name: "schemaUID", type: "bytes32" }, + { + components: [ + { internalType: "address", name: "moduleAddress", type: "address" }, + { internalType: "uint48", name: "expirationTime", type: "uint48" }, + { internalType: "bytes", name: "data", type: "bytes" }, + { + internalType: "ModuleType[]", + name: "moduleTypes", + type: "uint256[]" + } + ], + internalType: "struct AttestationRequest[]", + name: "requests", + type: "tuple[]" + } + ], + name: "attest", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [{ internalType: "address", name: "attester", type: "address" }], + name: "attesterNonce", + outputs: [{ internalType: "uint256", name: "nonce", type: "uint256" }], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { internalType: "bytes32", name: "salt", type: "bytes32" }, + { internalType: "bytes", name: "initCode", type: "bytes" } + ], + name: "calcModuleAddress", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { internalType: "address", name: "module", type: "address" }, + { internalType: "address[]", name: "attesters", type: "address[]" }, + { internalType: "uint256", name: "threshold", type: "uint256" } + ], name: "check", outputs: [], stateMutability: "view", @@ -71,89 +363,459 @@ export const MockRegistryAbi = [ }, { inputs: [ + { internalType: "address", name: "module", type: "address" }, + { internalType: "ModuleType", name: "moduleType", type: "uint256" }, + { internalType: "address[]", name: "attesters", type: "address[]" }, + { internalType: "uint256", name: "threshold", type: "uint256" } + ], + name: "check", + outputs: [], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { internalType: "address", name: "module", type: "address" }, + { internalType: "ModuleType", name: "moduleType", type: "uint256" } + ], + name: "check", + outputs: [], + stateMutability: "view", + type: "function" + }, + { + inputs: [{ internalType: "address", name: "module", type: "address" }], + name: "check", + outputs: [], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { internalType: "address", name: "smartAccount", type: "address" }, + { internalType: "address", name: "module", type: "address" } + ], + name: "checkForAccount", + outputs: [], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { internalType: "address", name: "smartAccount", type: "address" }, + { internalType: "address", name: "module", type: "address" }, + { internalType: "ModuleType", name: "moduleType", type: "uint256" } + ], + name: "checkForAccount", + outputs: [], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { internalType: "bytes32", name: "salt", type: "bytes32" }, + { internalType: "ResolverUID", name: "resolverUID", type: "bytes32" }, + { internalType: "bytes", name: "initCode", type: "bytes" }, + { internalType: "bytes", name: "metadata", type: "bytes" }, + { internalType: "bytes", name: "resolverContext", type: "bytes" } + ], + name: "deployModule", + outputs: [ + { internalType: "address", name: "moduleAddress", type: "address" } + ], + stateMutability: "payable", + type: "function" + }, + { + inputs: [ + { internalType: "address", name: "factory", type: "address" }, + { internalType: "bytes", name: "callOnFactory", type: "bytes" }, + { internalType: "bytes", name: "metadata", type: "bytes" }, + { internalType: "ResolverUID", name: "resolverUID", type: "bytes32" }, + { internalType: "bytes", name: "resolverContext", type: "bytes" } + ], + name: "deployViaFactory", + outputs: [ + { internalType: "address", name: "moduleAddress", type: "address" } + ], + stateMutability: "payable", + type: "function" + }, + { + inputs: [], + name: "eip712Domain", + outputs: [ + { internalType: "bytes1", name: "fields", type: "bytes1" }, + { internalType: "string", name: "name", type: "string" }, + { internalType: "string", name: "version", type: "string" }, + { internalType: "uint256", name: "chainId", type: "uint256" }, + { internalType: "address", name: "verifyingContract", type: "address" }, + { internalType: "bytes32", name: "salt", type: "bytes32" }, + { internalType: "uint256[]", name: "extensions", type: "uint256[]" } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { internalType: "address", name: "module", type: "address" }, + { internalType: "address", name: "attester", type: "address" } + ], + name: "findAttestation", + outputs: [ { - internalType: "address", - name: "module", - type: "address" - }, + components: [ + { internalType: "uint48", name: "time", type: "uint48" }, + { internalType: "uint48", name: "expirationTime", type: "uint48" }, + { internalType: "uint48", name: "revocationTime", type: "uint48" }, + { + internalType: "PackedModuleTypes", + name: "moduleTypes", + type: "uint32" + }, + { internalType: "address", name: "moduleAddress", type: "address" }, + { internalType: "address", name: "attester", type: "address" }, + { + internalType: "AttestationDataRef", + name: "dataPointer", + type: "address" + }, + { internalType: "SchemaUID", name: "schemaUID", type: "bytes32" } + ], + internalType: "struct AttestationRecord", + name: "attestation", + type: "tuple" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { internalType: "address", name: "module", type: "address" }, + { internalType: "address[]", name: "attesters", type: "address[]" } + ], + name: "findAttestations", + outputs: [ { - internalType: "uint256", - name: "moduleType", - type: "uint256" + components: [ + { internalType: "uint48", name: "time", type: "uint48" }, + { internalType: "uint48", name: "expirationTime", type: "uint48" }, + { internalType: "uint48", name: "revocationTime", type: "uint48" }, + { + internalType: "PackedModuleTypes", + name: "moduleTypes", + type: "uint32" + }, + { internalType: "address", name: "moduleAddress", type: "address" }, + { internalType: "address", name: "attester", type: "address" }, + { + internalType: "AttestationDataRef", + name: "dataPointer", + type: "address" + }, + { internalType: "SchemaUID", name: "schemaUID", type: "bytes32" } + ], + internalType: "struct AttestationRecord[]", + name: "attestations", + type: "tuple[]" } ], - name: "check", - outputs: [], stateMutability: "view", type: "function" }, { inputs: [ + { internalType: "address", name: "moduleAddress", type: "address" } + ], + name: "findModule", + outputs: [ { - internalType: "address", - name: "module", - type: "address" + components: [ + { internalType: "ResolverUID", name: "resolverUID", type: "bytes32" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "bytes", name: "metadata", type: "bytes" } + ], + internalType: "struct ModuleRecord", + name: "moduleRecord", + type: "tuple" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [{ internalType: "ResolverUID", name: "uid", type: "bytes32" }], + name: "findResolver", + outputs: [ + { + components: [ + { + internalType: "contract IExternalResolver", + name: "resolver", + type: "address" + }, + { internalType: "address", name: "resolverOwner", type: "address" } + ], + internalType: "struct ResolverRecord", + name: "", + type: "tuple" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [{ internalType: "SchemaUID", name: "uid", type: "bytes32" }], + name: "findSchema", + outputs: [ + { + components: [ + { internalType: "uint48", name: "registeredAt", type: "uint48" }, + { + internalType: "contract IExternalSchemaValidator", + name: "validator", + type: "address" + }, + { internalType: "string", name: "schema", type: "string" } + ], + internalType: "struct SchemaRecord", + name: "", + type: "tuple" } ], - name: "check", - outputs: [], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { internalType: "address", name: "smartAccount", type: "address" } + ], + name: "findTrustedAttesters", + outputs: [ + { internalType: "address[]", name: "attesters", type: "address[]" } + ], stateMutability: "view", type: "function" }, { inputs: [ { - internalType: "address", - name: "smartAccount", - type: "address" + components: [ + { internalType: "address", name: "moduleAddress", type: "address" }, + { internalType: "uint48", name: "expirationTime", type: "uint48" }, + { internalType: "bytes", name: "data", type: "bytes" }, + { + internalType: "ModuleType[]", + name: "moduleTypes", + type: "uint256[]" + } + ], + internalType: "struct AttestationRequest", + name: "request", + type: "tuple" }, + { internalType: "address", name: "attester", type: "address" } + ], + name: "getDigest", + outputs: [{ internalType: "bytes32", name: "digest", type: "bytes32" }], + stateMutability: "view", + type: "function" + }, + { + inputs: [ { - internalType: "address", - name: "module", - type: "address" - } + components: [ + { internalType: "address", name: "moduleAddress", type: "address" } + ], + internalType: "struct RevocationRequest[]", + name: "requests", + type: "tuple[]" + }, + { internalType: "address", name: "attester", type: "address" } ], - name: "checkForAccount", - outputs: [], + name: "getDigest", + outputs: [{ internalType: "bytes32", name: "digest", type: "bytes32" }], stateMutability: "view", type: "function" }, { inputs: [ { - internalType: "address", - name: "smartAccount", - type: "address" + components: [ + { internalType: "address", name: "moduleAddress", type: "address" } + ], + internalType: "struct RevocationRequest", + name: "request", + type: "tuple" }, + { internalType: "address", name: "attester", type: "address" } + ], + name: "getDigest", + outputs: [{ internalType: "bytes32", name: "digest", type: "bytes32" }], + stateMutability: "view", + type: "function" + }, + { + inputs: [ { - internalType: "address", - name: "module", + components: [ + { internalType: "address", name: "moduleAddress", type: "address" }, + { internalType: "uint48", name: "expirationTime", type: "uint48" }, + { internalType: "bytes", name: "data", type: "bytes" }, + { + internalType: "ModuleType[]", + name: "moduleTypes", + type: "uint256[]" + } + ], + internalType: "struct AttestationRequest[]", + name: "requests", + type: "tuple[]" + }, + { internalType: "address", name: "attester", type: "address" } + ], + name: "getDigest", + outputs: [{ internalType: "bytes32", name: "digest", type: "bytes32" }], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { internalType: "ResolverUID", name: "resolverUID", type: "bytes32" }, + { internalType: "address", name: "moduleAddress", type: "address" }, + { internalType: "bytes", name: "metadata", type: "bytes" }, + { internalType: "bytes", name: "resolverContext", type: "bytes" } + ], + name: "registerModule", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { + internalType: "contract IExternalResolver", + name: "resolver", + type: "address" + } + ], + name: "registerResolver", + outputs: [{ internalType: "ResolverUID", name: "uid", type: "bytes32" }], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { internalType: "string", name: "schema", type: "string" }, + { + internalType: "contract IExternalSchemaValidator", + name: "validator", type: "address" + } + ], + name: "registerSchema", + outputs: [{ internalType: "SchemaUID", name: "uid", type: "bytes32" }], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { + components: [ + { internalType: "address", name: "moduleAddress", type: "address" } + ], + internalType: "struct RevocationRequest[]", + name: "requests", + type: "tuple[]" + } + ], + name: "revoke", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { internalType: "address", name: "attester", type: "address" }, + { + components: [ + { internalType: "address", name: "moduleAddress", type: "address" } + ], + internalType: "struct RevocationRequest[]", + name: "requests", + type: "tuple[]" }, + { internalType: "bytes", name: "signature", type: "bytes" } + ], + name: "revoke", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ { - internalType: "uint256", - name: "moduleType", - type: "uint256" + components: [ + { internalType: "address", name: "moduleAddress", type: "address" } + ], + internalType: "struct RevocationRequest", + name: "request", + type: "tuple" } ], - name: "checkForAccount", + name: "revoke", outputs: [], - stateMutability: "view", + stateMutability: "nonpayable", type: "function" }, { inputs: [ + { internalType: "address", name: "attester", type: "address" }, { - internalType: "uint8", - name: "", - type: "uint8" + components: [ + { internalType: "address", name: "moduleAddress", type: "address" } + ], + internalType: "struct RevocationRequest", + name: "request", + type: "tuple" }, + { internalType: "bytes", name: "signature", type: "bytes" } + ], + name: "revoke", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { internalType: "ResolverUID", name: "uid", type: "bytes32" }, { - internalType: "address[]", - name: "attesters", - type: "address[]" + internalType: "contract IExternalResolver", + name: "resolver", + type: "address" } ], + name: "setResolver", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { internalType: "ResolverUID", name: "uid", type: "bytes32" }, + { internalType: "address", name: "newOwner", type: "address" } + ], + name: "transferResolverOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { internalType: "uint8", name: "threshold", type: "uint8" }, + { internalType: "address[]", name: "attesters", type: "address[]" } + ], name: "trustAttesters", outputs: [], stateMutability: "nonpayable", diff --git a/src/test/__contracts/abi/MockSignatureValidatorAbi.ts b/src/test/__contracts/abi/MockSignatureValidatorAbi.ts new file mode 100644 index 000000000..f620a3eb7 --- /dev/null +++ b/src/test/__contracts/abi/MockSignatureValidatorAbi.ts @@ -0,0 +1,36 @@ +export const MockSignatureValidatorAbi = [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor" + }, + { + inputs: [ + { + internalType: "bytes32", + name: "hash", + type: "bytes32" + }, + { + internalType: "bytes", + name: "signature", + type: "bytes" + }, + { + internalType: "address", + name: "signer", + type: "address" + } + ], + name: "verify", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool" + } + ], + stateMutability: "view", + type: "function" + } +] as const diff --git a/src/test/__contracts/abi/MockTokenAbi.ts b/src/test/__contracts/abi/MockTokenAbi.ts index 9340841ed..213ee0cf4 100644 --- a/src/test/__contracts/abi/MockTokenAbi.ts +++ b/src/test/__contracts/abi/MockTokenAbi.ts @@ -1,9 +1,106 @@ export const MockTokenAbi = [ { - inputs: [{ internalType: "address", name: "owner", type: "address" }], + inputs: [ + { + internalType: "string", + name: "name", + type: "string" + }, + { + internalType: "string", + name: "symbol", + type: "string" + } + ], stateMutability: "nonpayable", type: "constructor" }, + { + inputs: [ + { + internalType: "address", + name: "spender", + type: "address" + }, + { + internalType: "uint256", + name: "allowance", + type: "uint256" + }, + { + internalType: "uint256", + name: "needed", + type: "uint256" + } + ], + name: "ERC20InsufficientAllowance", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address" + }, + { + internalType: "uint256", + name: "balance", + type: "uint256" + }, + { + internalType: "uint256", + name: "needed", + type: "uint256" + } + ], + name: "ERC20InsufficientBalance", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "approver", + type: "address" + } + ], + name: "ERC20InvalidApprover", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "receiver", + type: "address" + } + ], + name: "ERC20InvalidReceiver", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address" + } + ], + name: "ERC20InvalidSender", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "spender", + type: "address" + } + ], + name: "ERC20InvalidSpender", + type: "error" + }, { anonymous: false, inputs: [ @@ -35,24 +132,15 @@ export const MockTokenAbi = [ { indexed: true, internalType: "address", - name: "previousOwner", + name: "from", type: "address" }, { indexed: true, internalType: "address", - name: "newOwner", + name: "to", type: "address" - } - ], - name: "OwnershipTransferred", - type: "event" - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "from", type: "address" }, - { indexed: true, internalType: "address", name: "to", type: "address" }, + }, { indexed: false, internalType: "uint256", @@ -65,62 +153,96 @@ export const MockTokenAbi = [ }, { inputs: [ - { internalType: "address", name: "owner", type: "address" }, - { internalType: "address", name: "spender", type: "address" } + { + internalType: "address", + name: "owner", + type: "address" + }, + { + internalType: "address", + name: "spender", + type: "address" + } ], name: "allowance", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256" + } + ], stateMutability: "view", type: "function" }, { inputs: [ - { internalType: "address", name: "spender", type: "address" }, - { internalType: "uint256", name: "amount", type: "uint256" } + { + internalType: "address", + name: "spender", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + } ], name: "approve", - outputs: [{ internalType: "bool", name: "", type: "bool" }], + outputs: [ + { + internalType: "bool", + name: "", + type: "bool" + } + ], stateMutability: "nonpayable", type: "function" }, { - inputs: [{ internalType: "address", name: "account", type: "address" }], + inputs: [ + { + internalType: "address", + name: "account", + type: "address" + } + ], name: "balanceOf", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256" + } + ], stateMutability: "view", type: "function" }, { inputs: [], name: "decimals", - outputs: [{ internalType: "uint8", name: "", type: "uint8" }], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { internalType: "address", name: "spender", type: "address" }, - { internalType: "uint256", name: "subtractedValue", type: "uint256" } - ], - name: "decreaseAllowance", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { internalType: "address", name: "spender", type: "address" }, - { internalType: "uint256", name: "addedValue", type: "uint256" } + outputs: [ + { + internalType: "uint8", + name: "", + type: "uint8" + } ], - name: "increaseAllowance", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "nonpayable", + stateMutability: "view", type: "function" }, { inputs: [ - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "amount", type: "uint256" } + { + internalType: "address", + name: "sender", + type: "address" + }, + { + internalType: "uint256", + name: "amount", + type: "uint256" + } ], name: "mint", outputs: [], @@ -130,84 +252,92 @@ export const MockTokenAbi = [ { inputs: [], name: "name", - outputs: [{ internalType: "string", name: "", type: "string" }], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "nativeToTokenRatio", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "owner", - outputs: [{ internalType: "address", name: "", type: "address" }], + outputs: [ + { + internalType: "string", + name: "", + type: "string" + } + ], stateMutability: "view", type: "function" }, - { - inputs: [], - name: "publicMint", - outputs: [], - stateMutability: "payable", - type: "function" - }, - { - inputs: [], - name: "renounceOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, { inputs: [], name: "symbol", - outputs: [{ internalType: "string", name: "", type: "string" }], + outputs: [ + { + internalType: "string", + name: "", + type: "string" + } + ], stateMutability: "view", type: "function" }, { inputs: [], name: "totalSupply", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256" + } + ], stateMutability: "view", type: "function" }, { inputs: [ - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "amount", type: "uint256" } + { + internalType: "address", + name: "to", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + } ], name: "transfer", - outputs: [{ internalType: "bool", name: "", type: "bool" }], + outputs: [ + { + internalType: "bool", + name: "", + type: "bool" + } + ], stateMutability: "nonpayable", type: "function" }, { inputs: [ - { internalType: "address", name: "from", type: "address" }, - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "amount", type: "uint256" } + { + internalType: "address", + name: "from", + type: "address" + }, + { + internalType: "address", + name: "to", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + } ], name: "transferFrom", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [{ internalType: "address", name: "newOwner", type: "address" }], - name: "transferOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [], - name: "withdraw", - outputs: [], + outputs: [ + { + internalType: "bool", + name: "", + type: "bool" + } + ], stateMutability: "nonpayable", type: "function" } diff --git a/src/test/__contracts/abi/index.ts b/src/test/__contracts/abi/index.ts index 89bb0fb58..379bdf81f 100644 --- a/src/test/__contracts/abi/index.ts +++ b/src/test/__contracts/abi/index.ts @@ -6,9 +6,9 @@ export * from "./NexusBootstrapAbi" export * from "./CounterAbi" export * from "./MockValidatorAbi" export * from "./MockTokenAbi" +export * from "./MockAttesterAbi" export * from "./BootstrapLibAbi" export * from "./MockRegistryAbi" export * from "./MockHandlerAbi" export * from "./TokenWithPermitAbi" export * from "./MockExecutorAbi" -export * from "./MockCalleeAbi" diff --git a/src/test/__contracts/mockAddresses.ts b/src/test/__contracts/mockAddresses.ts index 89362039b..2dbcf98ee 100644 --- a/src/test/__contracts/mockAddresses.ts +++ b/src/test/__contracts/mockAddresses.ts @@ -3,13 +3,13 @@ export const mockAddresses = { MockHook: "0x8a81354786B34949133ceE3c7D7772eE04Ed9749", Stakeable: "0x33F4F96959eC465DBEee63CdEc09fc1a224bCE15", - NexusAccountFactory: "0x9eb6AeE874Ca0a6763b9a8Ca404D0A31055C80a3", + NexusAccountFactory: "0x814BbF9394Ce52c20994F71014D81fCc121202F8", BiconomyMetaFactory: "0x3bCF58bbEfD78C2445d883127946Edd1D3544073", - NexusBootstrap: "0xb4C237566bcE54a832B8e84d0A4BfEaA0C3B0343", + NexusBootstrap: "0xe3ee05eA9D3aDf82d08b87A272B4171f94f890ca", Counter: "0x2b4e7a9e2040729933D3C8284a95e460fBB2257E", MockValidator: "0x6DAA3CDa6886dcff35248dA93D79600aea267d0C", MockToken: "0x29515466c8d02e60BFb7DF4255908Eb271b7f244", - BootstrapLib: "0x53Fbd943Da8d372fe35086d3A09d0aCD7Cfe9f1a", + BootstrapLib: "0x45d7F0109b614c796eF2C1B32341c882F18c6B40", MockRegistry: "0xFA344eABd10bedfa29A3BDC549fB246D48998f8A", MockHandler: "0x6821519337864B001a6DD6Eb7ca8E5B79Ece511E", TokenWithPermit: "0xc76c8504BE016999637AA6385198bAab78aa98d9", diff --git a/src/test/callDatas.ts b/src/test/callDatas.ts index 782094c25..75933b364 100644 --- a/src/test/callDatas.ts +++ b/src/test/callDatas.ts @@ -1,90 +1,108 @@ import type { Address, Hex } from "viem" +import { baseSepolia } from "viem/chains" +import { + MOCK_ATTESTER_ADDRESS, + OWNABLE_EXECUTOR_ADDRESS, + OWNABLE_VALIDATOR_ADDRESS, + REGISTRY_ADDRESS, + SIMPLE_SESSION_VALIDATOR_ADDRESS, + SMART_SESSIONS_ADDRESS +} from "../sdk/constants" export const TEST_CONTRACTS: Record< string, { chainId: number; name: string; address: Hex } > = { - // Rhinestone Ownables + // Rhinestone OwnableValidator: { - chainId: 11155111, + chainId: baseSepolia.id, name: "OwnableValidator", - address: "0x6605F8785E09a245DD558e55F9A0f4A508434503" + address: OWNABLE_VALIDATOR_ADDRESS }, OwnableExecutor: { - chainId: 11155111, + chainId: baseSepolia.id, name: "OwnableExecutor", - address: "0xc98B026383885F41d9a995f85FC480E9bb8bB891" + address: OWNABLE_EXECUTOR_ADDRESS }, - // Smart sessions: TODO: update with latest address SmartSession: { - chainId: 84532, + chainId: baseSepolia.id, name: "SmartSession", - address: "0x3834aD7f5f73fAd19C089a924F18e6F3417d1ac2" + address: SMART_SESSIONS_ADDRESS }, SimpleSessionValidator: { - chainId: 84532, - name: "Simple Session Validator", - address: "0xAAAdFd794A1781e4Fd3eA64985F107a7Ac2b3872" + chainId: baseSepolia.id, + name: "SimpleSessionValidator", + address: SIMPLE_SESSION_VALIDATOR_ADDRESS }, UniActionPolicy: { - chainId: 84532, + chainId: baseSepolia.id, name: "UniActionPolicy", - address: "0x28120dC008C36d95DE5fa0603526f219c1Ba80f6" + address: "0x148CD6c24F4dd23C396E081bBc1aB1D92eeDe2BF" }, Counter: { - chainId: 84532, + chainId: baseSepolia.id, name: "Counter", address: "0x14e4829E655F0b3a1793838dDd47273D5341d416" }, MockCallee: { - chainId: 84532, + chainId: baseSepolia.id, name: "MockCallee", address: "0x29FdD9D9A9f8CD8dCa0F4764bf0F959183DF4139" }, MockToken: { - chainId: 84532, + chainId: baseSepolia.id, name: "MockToken", address: "0x0006be192b4E06770eaa624AE7648DBF9051221c" }, + TokenWithPermit: { + chainId: baseSepolia.id, + name: "TokenWithPermit", + address: "0x51fdb803fD49f0f5bd03de0400a8F17dA2Aa6999" + }, MockAttester: { - chainId: 84532, + chainId: baseSepolia.id, name: "MockAttester", - address: "0xA4C777199658a41688E9488c4EcbD7a2925Cc23A" + address: MOCK_ATTESTER_ADDRESS }, MockRegistry: { - chainId: 84532, + chainId: baseSepolia.id, name: "MockRegistry", - address: "0x000000000069E2a187AEFFb852bF3cCdC95151B2" + address: REGISTRY_ADDRESS }, TimeFramePolicy: { - chainId: 84532, + chainId: baseSepolia.id, name: "TimeFramePolicy", address: "0x0B7BB9bD65858593D97f12001FaDa94828307805" }, UsageLimitPolicy: { - chainId: 84532, + chainId: baseSepolia.id, name: "UsageLimitPolicy", address: "0x80EF509D2F79eA332540e9698bDbc7B7FA3E1f74" }, ValueLimitPolicy: { - chainId: 84532, + chainId: baseSepolia.id, name: "ValueLimitPolicy", address: "0xDe9688b24c00699Ad51961ef90Ce5a9a8C49982B" }, WalletConnectCoSigner: { - chainId: 84532, + chainId: baseSepolia.id, name: "WalletConnect CoSigner", address: "0x24084171C36Fa6dfdf41D9C89A51F600ed35A731" }, MockK1Validator: { - chainId: 84532, + chainId: baseSepolia.id, name: "MockK1Validator", address: "0x2db5c5A93c71A2562b751Ad3eaB18BFB5fb96374" }, UserOperationBuilder: { - chainId: 84532, + chainId: baseSepolia.id, name: "UserOperationBuilder", address: "0xb07D7605a1AAeE4e56915363418229c127fF7C3D" + }, + MockSignatureValidator: { + chainId: baseSepolia.id, + name: "MockSignatureValidator", + address: "0x0d0C730F50a6da2725d4CD4eb91Bc678Bd377F7D" } } diff --git a/src/test/playground.test.ts b/src/test/playground.test.ts index 0d82046ea..d9ab8755d 100644 --- a/src/test/playground.test.ts +++ b/src/test/playground.test.ts @@ -8,7 +8,6 @@ import { createPublicClient, createWalletClient } from "viem" -import type { UserOperationReceipt } from "viem/account-abstraction" import { beforeAll, describe, expect, test } from "vitest" import { playgroundTrue } from "../sdk/account/utils/Utils" import { createBicoPaymasterClient } from "../sdk/clients/createBicoPaymasterClient" @@ -17,19 +16,19 @@ import { createNexusClient } from "../sdk/clients/createNexusClient" import { 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" +import { + type NetworkConfig, + type TestnetParams, + getTestParamsForTestnet +} from "./testUtils" describe.skipIf(!playgroundTrue)("playground", () => { let network: NetworkConfig + // Required for "PUBLIC_TESTNET" networks + let testParams: TestnetParams // Nexus Config let chain: Chain let bundlerUrl: string - let paymasterUrl: undefined | string let walletClient: WalletClient // Test utils @@ -44,7 +43,6 @@ describe.skipIf(!playgroundTrue)("playground", () => { chain = network.chain bundlerUrl = network.bundlerUrl - paymasterUrl = network.paymasterUrl eoaAccount = network.account as PrivateKeyAccount recipientAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" // vitalik.eth @@ -59,19 +57,8 @@ describe.skipIf(!playgroundTrue)("playground", () => { chain, transport: http() }) - }) - - test("should have factory and k1Validator deployed", async () => { - const byteCodes = await Promise.all([ - publicClient.getCode({ - address: k1ValidatorAddress - }), - publicClient.getCode({ - address: factoryAddress - }) - ]) - expect(byteCodes.every(Boolean)).toBeTruthy() + testParams = getTestParamsForTestnet(publicClient) }) test("should init the smart account", async () => { @@ -80,8 +67,12 @@ describe.skipIf(!playgroundTrue)("playground", () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress, - factoryAddress + paymaster: network.paymasterUrl + ? createBicoPaymasterClient({ + transport: http(network.paymasterUrl) + }) + : undefined, + ...testParams }) }) @@ -111,7 +102,6 @@ describe.skipIf(!playgroundTrue)("playground", () => { value: 1000000000000000000n }) const receipt = await publicClient.waitForTransactionReceipt({ hash }) - console.log({ receipt }) } expect(balancesAreOfCorrectType).toBeTruthy() }) @@ -126,8 +116,7 @@ describe.skipIf(!playgroundTrue)("playground", () => { to: recipientAddress, value: 1n } - ], - preVerificationGas: 800000000n + ] }) const { status } = await publicClient.waitForTransactionReceipt({ hash }) const balanceAfter = await publicClient.getBalance({ @@ -137,93 +126,20 @@ describe.skipIf(!playgroundTrue)("playground", () => { expect(balanceAfter - balanceBefore).toBe(1n) }) - test("should send some native token using the paymaster", async () => { - if (!paymasterUrl) { - console.log("No paymaster url provided") - return - } - - nexusClient = await createNexusClient({ - signer: eoaAccount, - chain, - transport: http(), - bundlerTransport: http(bundlerUrl), - k1ValidatorAddress, - factoryAddress, - paymaster: createBicoPaymasterClient({ - paymasterUrl - }) + test("should send a user operation using nexusClient.sendUserOperation", async () => { + const balanceBefore = await publicClient.getBalance({ + address: recipientAddress }) - expect(async () => - nexusClient.sendTransaction({ - calls: [ - { - to: eoaAccount.address, - value: 1n - } - ] - }) - ).rejects.toThrow() - }) - - test("should send sequential user ops", async () => { - const start = performance.now() - const nexusClient = await createNexusClient({ - signer: eoaAccount, - chain, - transport: http(), - bundlerTransport: http(bundlerUrl), - k1ValidatorAddress, - factoryAddress + const userOpHash = await nexusClient.sendUserOperation({ + calls: [{ to: recipientAddress, value: 1n }] }) - const receipts: UserOperationReceipt[] = [] - for (let i = 0; i < 3; i++) { - const hash = await nexusClient.sendUserOperation({ - calls: [ - { - to: recipientAddress, - value: 0n - } - ] - }) - const receipt = await nexusClient.waitForUserOperationReceipt({ hash }) - receipts.push(receipt) - } - expect(receipts.every((receipt) => receipt.success)).toBeTruthy() - const end = performance.now() - console.log(`Time taken: ${end - start} milliseconds`) - }) - - test("should send parallel user ops", async () => { - const start = performance.now() - const nexusClient = await createNexusClient({ - signer: eoaAccount, - chain, - transport: http(), - bundlerTransport: http(bundlerUrl), - k1ValidatorAddress, - factoryAddress + const { success } = await nexusClient.waitForUserOperationReceipt({ + hash: userOpHash }) - const userOpPromises: Promise<`0x${string}`>[] = [] - for (let i = 0; i < 3; i++) { - userOpPromises.push( - nexusClient.sendUserOperation({ - calls: [ - { - to: recipientAddress, - value: 0n - } - ] - }) - ) - } - const hashes = await Promise.all(userOpPromises) - expect(hashes.length).toBe(3) - const receipts = await Promise.all( - hashes.map((hash) => nexusClient.waitForUserOperationReceipt({ hash })) - ) - expect(receipts.every((receipt) => receipt.success)).toBeTruthy() - const end = performance.now() - console.log(`Time taken: ${end - start} milliseconds`) + const balanceAfter = await publicClient.getBalance({ + address: recipientAddress + }) + expect(success).toBe("true") + expect(balanceAfter - balanceBefore).toBe(1n) }) }) diff --git a/src/test/testSetup.ts b/src/test/testSetup.ts index dd9813f80..8b002ebb2 100644 --- a/src/test/testSetup.ts +++ b/src/test/testSetup.ts @@ -44,16 +44,19 @@ export type TestFileNetworkType = | "FILE_LOCALHOST" | "COMMON_LOCALHOST" | "PUBLIC_TESTNET" + | "BASE_SEPOLIA_FORKED" export const toNetwork = async ( networkType: TestFileNetworkType = "FILE_LOCALHOST" -): Promise => - await (networkType === "COMMON_LOCALHOST" +): Promise => { + const forkBaseSepolia = networkType === "BASE_SEPOLIA_FORKED" + return await (networkType === "COMMON_LOCALHOST" ? // @ts-ignore inject("globalNetwork") - : networkType === "FILE_LOCALHOST" - ? initLocalhostNetwork() - : initTestnetNetwork()) + : networkType === "PUBLIC_TESTNET" + ? initTestnetNetwork() + : initLocalhostNetwork(forkBaseSepolia)) +} export const playgroundTrue = process.env.RUN_PLAYGROUND === "true" export const paymasterTruthy = !!process.env.PAYMASTER_URL diff --git a/src/test/testUtils.ts b/src/test/testUtils.ts index a3ff67293..1f25420bb 100644 --- a/src/test/testUtils.ts +++ b/src/test/testUtils.ts @@ -1,8 +1,7 @@ import { config } from "dotenv" -import { BytesLike, getAddress, getBytes, hexlify } from "ethers" import getPort from "get-port" // @ts-ignore -import { alto, anvil } from "prool/instances" +import { type AnvilParameters, alto, anvil } from "prool/instances" import { http, type Account, @@ -10,6 +9,7 @@ import { type Chain, type Hex, type PrivateKeyAccount, + type PublicClient, createPublicClient, createTestClient, createWalletClient, @@ -20,19 +20,24 @@ import { } 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 { getChain, getCustomChain, safeMultiplier } from "../sdk/account/utils" import { Logger } from "../sdk/account/utils/Logger" -import { createBicoBundlerClient } from "../sdk/clients/createBicoBundlerClient" import { type NexusClient, createNexusClient } from "../sdk/clients/createNexusClient" +import { + ENTRYPOINT_SIMULATIONS_ADDRESS, + ENTRY_POINT_ADDRESS, + MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, + MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS +} from "../sdk/constants" import { ENTRY_POINT_SIMULATIONS_CREATECALL, ENTRY_POINT_V07_CREATECALL, TEST_CONTRACTS } from "./callDatas" + import * as hardhatExec from "./executables" config() @@ -124,26 +129,24 @@ export const initTestnetNetwork = async (): Promise => { } } -export const initLocalhostNetwork = - async (): Promise => { - const configuredNetwork = await initAnvilPayload() - const bundlerConfig = await initBundlerInstance({ - rpcUrl: configuredNetwork.rpcUrl - }) - await ensureBundlerIsReady( - bundlerConfig.bundlerUrl, - getTestChainFromPort(configuredNetwork.rpcPort) - ) - allInstances.set( - configuredNetwork.instance.port, - configuredNetwork.instance - ) - allInstances.set( - bundlerConfig.bundlerInstance.port, - bundlerConfig.bundlerInstance - ) - return { ...configuredNetwork, ...bundlerConfig } - } +export const initLocalhostNetwork = async ( + shouldForkBaseSepolia = false +): Promise => { + const configuredNetwork = await initAnvilPayload(shouldForkBaseSepolia) + const bundlerConfig = await initBundlerInstance({ + rpcUrl: configuredNetwork.rpcUrl + }) + await ensureBundlerIsReady( + bundlerConfig.bundlerUrl, + getTestChainFromPort(configuredNetwork.rpcPort) + ) + allInstances.set(configuredNetwork.instance.port, configuredNetwork.instance) + allInstances.set( + bundlerConfig.bundlerInstance.port, + bundlerConfig.bundlerInstance + ) + return { ...configuredNetwork, ...bundlerConfig } +} export type MasterClient = ReturnType export const toTestClient = (chain: Chain, account: Account) => @@ -164,10 +167,10 @@ export const toBundlerInstance = async ({ bundlerPort: number }): Promise => { const instance = alto({ - entrypoints: [contracts.entryPoint.address], + entrypoints: [ENTRY_POINT_ADDRESS], rpcUrl: rpcUrl, executorPrivateKeys: [pKey], - entrypointSimulationContract: contracts.entryPointSimulations.address, + entrypointSimulationContract: ENTRYPOINT_SIMULATIONS_ADDRESS, safeMode: false, port: bundlerPort }) @@ -195,15 +198,22 @@ export const ensureBundlerIsReady = async ( } export const toConfiguredAnvil = async ({ - rpcPort -}: { rpcPort: number }): Promise => { - const instance = anvil({ + rpcPort, + shouldForkBaseSepolia = false +}: { + rpcPort: number + shouldForkBaseSepolia: boolean +}): Promise => { + const config: AnvilParameters = { hardfork: "Cancun", chainId: rpcPort, port: rpcPort, - codeSizeLimit: 1000000000000 - // forkUrl: "https://base-sepolia.gateway.tenderly.co/2oxlNZ7oiNCUpXzrWFuIHx" - }) + codeSizeLimit: 1000000000000, + forkUrl: shouldForkBaseSepolia + ? "https://virtual.base-sepolia.rpc.tenderly.co/6deb172f-d5d9-4ae3-9d1d-8f04d52714d6" + : undefined + } + const instance = anvil(config) await instance.start() await initDeployments(rpcPort) return instance @@ -234,12 +244,14 @@ export const initDeployments = async (rpcPort: number) => { } const portOptions = { exclude: [] as number[] } -export const initAnvilPayload = async (): Promise => { +export const initAnvilPayload = async ( + shouldForkBaseSepolia = false +): Promise => { const rpcPort = await getPort(portOptions) portOptions.exclude.push(rpcPort) const rpcUrl = `http://localhost:${rpcPort}` const chain = getTestChainFromPort(rpcPort) - const instance = await toConfiguredAnvil({ rpcPort }) + const instance = await toConfiguredAnvil({ rpcPort, shouldForkBaseSepolia }) return { rpcUrl, chain, instance, rpcPort } } @@ -478,3 +490,18 @@ export const setByteCodeDynamic = async ( ) ) } + +export type TestnetParams = ReturnType +export const getTestParamsForTestnet = (publicClient: PublicClient) => ({ + k1ValidatorAddress: MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, + factoryAddress: MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + userOperation: { + estimateFeesPerGas: async (_) => { + const feeData = await publicClient.estimateFeesPerGas() + return { + maxFeePerGas: safeMultiplier(feeData.maxFeePerGas, 1.25), + maxPriorityFeePerGas: safeMultiplier(feeData.maxPriorityFeePerGas, 1.25) + } + } + } +})