From 16d7451e528e52dd046de15c0a66062d0123b35d Mon Sep 17 00:00:00 2001 From: joepegler Date: Mon, 25 Nov 2024 14:23:17 +0000 Subject: [PATCH] chore: release (#133) Co-authored-by: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Co-authored-by: Vasile Gabriel Marian <56271768+VGabriel45@users.noreply.github.com> Co-authored-by: VGabriel45 --- .size-limit.json | 8 +- CHANGELOG.md | 42 + README.md | 13 +- bun.lockb | Bin 295791 -> 299874 bytes package.json | 123 +- scripts/fetch:deployment.ts | 2 +- scripts/send:userOp.ts | 39 +- src/sdk/__contracts/abi/K1ValidatorAbi.ts | 317 ----- .../__contracts/abi/K1ValidatorFactoryAbi.ts | 386 ------ src/sdk/__contracts/abi/NexusAbi.ts | 1215 ----------------- src/sdk/__contracts/abi/SmartSessionAbi.ts | 827 ----------- src/sdk/__contracts/abi/index.ts | 6 - src/sdk/__contracts/addresses.ts | 12 - src/sdk/__contracts/index.ts | 35 - src/sdk/account/index.ts | 2 +- src/sdk/account/toNexusAccount.test.ts | 104 +- src/sdk/account/toNexusAccount.ts | 165 ++- src/sdk/account/utils/Constants.ts | 7 + src/sdk/account/utils/Types.ts | 24 - src/sdk/account/utils/Utils.ts | 154 ++- src/sdk/account/utils/deepHexlify.ts | 30 + src/sdk/account/utils/toHolder.ts | 85 -- src/sdk/account/utils/toSigner.ts | 87 +- src/sdk/account/utils/toValidator.ts | 63 + src/sdk/account/utils/utils.test.ts | 42 + .../clients/createBicoBundlerClient.test.ts | 19 +- .../clients/createBicoPaymasterClient.test.ts | 38 +- src/sdk/clients/createNexusClient.test.ts | 113 +- src/sdk/clients/createNexusClient.ts | 27 +- .../clients/createNexusSessionClient.test.ts | 101 +- src/sdk/clients/createNexusSessionClient.ts | 4 +- src/sdk/clients/decorators/dan/Helpers.ts | 77 ++ .../dan/decorators/dan.decorators.test.ts | 149 ++ .../decorators/dan/decorators/index.ts | 43 + .../decorators/dan/decorators/keyGen.ts | 138 ++ .../decorators/dan/decorators/sigGen.ts | 118 ++ src/sdk/clients/decorators/dan/index.ts | 2 + .../clients/decorators/erc7579/accountId.ts | 2 +- .../erc7579/erc7579.decorators.test.ts | 12 +- .../decorators/erc7579/getActiveHook.ts | 2 +- .../erc7579/getFallbackBySelector.ts | 2 +- .../erc7579/getInstalledExecutors.ts | 2 +- .../erc7579/getInstalledValidators.ts | 2 +- .../decorators/erc7579/getPreviousModule.ts | 8 +- src/sdk/clients/decorators/erc7579/index.ts | 4 +- .../decorators/erc7579/installModule.ts | 8 +- .../decorators/erc7579/installModules.ts | 2 +- .../decorators/erc7579/isModuleInstalled.ts | 10 +- .../decorators/erc7579/moduleActivator.ts | 20 + .../erc7579/supportsExecutionMode.ts | 2 +- .../decorators/erc7579/supportsModule.ts | 2 +- .../decorators/erc7579/uninstallFallback.ts | 8 +- .../decorators/erc7579/uninstallModule.ts | 10 +- .../decorators/erc7579/uninstallModules.ts | 8 +- src/sdk/clients/decorators/index.ts | 4 + .../clients/decorators/smartAccount/index.ts | 8 +- .../smartAccount/sendTransaction.ts | 30 +- src/sdk/clients/index.ts | 4 +- .../abi/EIP1271Abi.ts | 0 src/sdk/constants/abi/ERC7484RegistryAbi.ts | 13 + .../abi/EntryPointABI.ts | 0 src/sdk/constants/abi/SmartSessionAbi.ts | 184 +++ .../abi/UniActionPolicyAbi.ts | 0 src/sdk/constants/abi/index.ts | 4 + src/sdk/constants/index.ts | 47 + src/sdk/index.ts | 1 + src/sdk/modules/README.md | 2 +- src/sdk/modules/index.ts | 11 +- src/sdk/modules/k1Validator/index.ts | 1 + .../toK1Validator.test.ts} | 14 +- .../toK1.ts => k1Validator/toK1Validator.ts} | 20 +- .../decorators/addOwner.ts | 2 +- .../decorators/getAddOwnerTx.ts | 2 +- .../decorators/getOwners.ts | 3 +- .../decorators/getRemoveOwnerTx.ts | 2 +- .../decorators/getSetThresholdTx.ts | 2 +- .../decorators/getThreshold.ts | 2 +- .../decorators/index.ts | 0 .../decorators/ownables.decorators.test.ts | 4 +- .../decorators/prepareSignatures.ts | 2 +- .../decorators/removeOwner.ts | 2 +- .../decorators/setThreshold.ts | 2 +- src/sdk/modules/ownableValidator/index.ts | 2 + .../toOwnableValidator.dan.test.ts | 118 ++ .../toOwnableValidator.dx.test.ts | 139 ++ .../toOwnableValidator.executor.test.ts | 173 +++ .../toOwnableValidator.test.ts | 304 +++++ .../toOwnableValidator.ts} | 30 +- src/sdk/modules/ownables/toOwnables.test.ts | 581 -------- .../smartSessions/smartSessions.test.ts | 780 ----------- .../Helpers.ts | 129 +- .../Types.ts | 61 +- .../decorators/grantPermission.ts} | 150 +- .../decorators/index.ts | 67 +- .../smartSessions.decorators.test.ts | 27 +- .../decorators/trustAttesters.ts | 118 ++ .../decorators/useDistributedPermission.ts | 113 ++ .../decorators/usePermission.ts} | 51 +- .../modules/smartSessionsValidator/index.ts | 4 + .../toSmartSessionsValidator.dan.dx.test.ts | 191 +++ .../toSmartSessionsValidator.dx.test.ts | 207 +++ .../toSmartSessionsValidator.test.ts | 297 ++++ .../toSmartSessionsValidator.ts} | 75 +- ...oSmartSessionsValidator.uni.policy.test.ts | 319 +++++ src/sdk/modules/utils/Helpers.ts | 68 +- src/sdk/modules/utils/Types.ts | 52 +- src/sdk/modules/utils/index.ts | 5 + src/sdk/modules/utils/toModule.test.ts | 8 +- src/sdk/modules/utils/toModule.ts | 79 +- src/test/README.md | 4 +- src/test/__contracts/abi/MockAttesterAbi.ts | 40 + src/test/__contracts/abi/MockRegistryAbi.ts | 790 ++++++++++- .../abi/MockSignatureValidatorAbi.ts | 36 + src/test/__contracts/abi/MockTokenAbi.ts | 322 +++-- src/test/__contracts/abi/index.ts | 2 +- src/test/__contracts/mockAddresses.ts | 6 +- src/test/callDatas.ts | 70 +- src/test/playground.test.ts | 138 +- src/test/testSetup.ts | 13 +- src/test/testUtils.ts | 97 +- 120 files changed, 5386 insertions(+), 5367 deletions(-) delete mode 100644 src/sdk/__contracts/abi/K1ValidatorAbi.ts delete mode 100644 src/sdk/__contracts/abi/K1ValidatorFactoryAbi.ts delete mode 100644 src/sdk/__contracts/abi/NexusAbi.ts delete mode 100644 src/sdk/__contracts/abi/SmartSessionAbi.ts delete mode 100644 src/sdk/__contracts/abi/index.ts delete mode 100644 src/sdk/__contracts/addresses.ts delete mode 100644 src/sdk/__contracts/index.ts create mode 100644 src/sdk/account/utils/deepHexlify.ts delete mode 100644 src/sdk/account/utils/toHolder.ts create mode 100644 src/sdk/account/utils/toValidator.ts create mode 100644 src/sdk/clients/decorators/dan/Helpers.ts create mode 100644 src/sdk/clients/decorators/dan/decorators/dan.decorators.test.ts create mode 100644 src/sdk/clients/decorators/dan/decorators/index.ts create mode 100644 src/sdk/clients/decorators/dan/decorators/keyGen.ts create mode 100644 src/sdk/clients/decorators/dan/decorators/sigGen.ts create mode 100644 src/sdk/clients/decorators/dan/index.ts create mode 100644 src/sdk/clients/decorators/erc7579/moduleActivator.ts create mode 100644 src/sdk/clients/decorators/index.ts rename src/sdk/{__contracts => constants}/abi/EIP1271Abi.ts (100%) create mode 100644 src/sdk/constants/abi/ERC7484RegistryAbi.ts rename src/sdk/{__contracts => constants}/abi/EntryPointABI.ts (100%) create mode 100644 src/sdk/constants/abi/SmartSessionAbi.ts rename src/sdk/{__contracts => constants}/abi/UniActionPolicyAbi.ts (100%) create mode 100644 src/sdk/constants/abi/index.ts create mode 100644 src/sdk/constants/index.ts create mode 100644 src/sdk/modules/k1Validator/index.ts rename src/sdk/modules/{k1/toK1.test.ts => k1Validator/toK1Validator.test.ts} (93%) rename src/sdk/modules/{k1/toK1.ts => k1Validator/toK1Validator.ts} (84%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/addOwner.ts (98%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/getAddOwnerTx.ts (97%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/getOwners.ts (96%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/getRemoveOwnerTx.ts (97%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/getSetThresholdTx.ts (97%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/getThreshold.ts (97%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/index.ts (100%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/ownables.decorators.test.ts (96%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/prepareSignatures.ts (94%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/removeOwner.ts (98%) rename src/sdk/modules/{ownables => ownableValidator}/decorators/setThreshold.ts (98%) create mode 100644 src/sdk/modules/ownableValidator/index.ts create mode 100644 src/sdk/modules/ownableValidator/toOwnableValidator.dan.test.ts create mode 100644 src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts create mode 100644 src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts create mode 100644 src/sdk/modules/ownableValidator/toOwnableValidator.test.ts rename src/sdk/modules/{ownables/toOwnables.ts => ownableValidator/toOwnableValidator.ts} (84%) delete mode 100644 src/sdk/modules/ownables/toOwnables.test.ts delete mode 100644 src/sdk/modules/smartSessions/smartSessions.test.ts rename src/sdk/modules/{smartSessions => smartSessionsValidator}/Helpers.ts (63%) rename src/sdk/modules/{smartSessions => smartSessionsValidator}/Types.ts (74%) rename src/sdk/modules/{smartSessions/decorators/createSessions.ts => smartSessionsValidator/decorators/grantPermission.ts} (67%) rename src/sdk/modules/{smartSessions => smartSessionsValidator}/decorators/index.ts (52%) rename src/sdk/modules/{smartSessions => smartSessionsValidator}/decorators/smartSessions.decorators.test.ts (82%) create mode 100644 src/sdk/modules/smartSessionsValidator/decorators/trustAttesters.ts create mode 100644 src/sdk/modules/smartSessionsValidator/decorators/useDistributedPermission.ts rename src/sdk/modules/{smartSessions/decorators/useSession.ts => smartSessionsValidator/decorators/usePermission.ts} (72%) create mode 100644 src/sdk/modules/smartSessionsValidator/index.ts create mode 100644 src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dan.dx.test.ts create mode 100644 src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dx.test.ts create mode 100644 src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.test.ts rename src/sdk/modules/{smartSessions/toSmartSessions.ts => smartSessionsValidator/toSmartSessionsValidator.ts} (64%) create mode 100644 src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.uni.policy.test.ts create mode 100644 src/sdk/modules/utils/index.ts create mode 100644 src/test/__contracts/abi/MockAttesterAbi.ts create mode 100644 src/test/__contracts/abi/MockSignatureValidatorAbi.ts 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 d5e38587410be954946648f58744864ce80d7f26..3d7bee68438818e77d68f03f95dd490393e9107e 100755 GIT binary patch delta 61060 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 L@~(6+TH^J8p37|k delta 58787 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 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) + } + } + } +})