From eba027054515ecb8812ce891ac8f6c746b64d156 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Tue, 3 Dec 2024 14:10:28 +0000 Subject: [PATCH] chore: add sudo policy --- 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 - src/sdk/clients/decorators/index.ts | 1 - .../toOwnableValidator.dan.test.ts | 118 ---------- .../modules/smartSessionsValidator/Helpers.ts | 72 ++++-- .../modules/smartSessionsValidator/Types.ts | 34 ++- .../decorators/grantPermission.ts | 17 +- .../decorators/index.ts | 22 +- .../decorators/useDistributedPermission.ts | 113 ---------- .../toSmartSessionsValidator.dan.dx.test.ts | 193 ---------------- .../toSmartSessionsValidator.dx.test.ts | 46 ++-- ...SmartSessionsValidator.sudo.policy.test.ts | 213 ++++++++++++++++++ .../toSmartSessionsValidator.test.ts | 24 +- .../toSmartSessionsValidator.ts | 15 +- ...oSmartSessionsValidator.uni.policy.test.ts | 2 +- 19 files changed, 339 insertions(+), 1058 deletions(-) delete mode 100644 src/sdk/clients/decorators/dan/Helpers.ts delete mode 100644 src/sdk/clients/decorators/dan/decorators/dan.decorators.test.ts delete mode 100644 src/sdk/clients/decorators/dan/decorators/index.ts delete mode 100644 src/sdk/clients/decorators/dan/decorators/keyGen.ts delete mode 100644 src/sdk/clients/decorators/dan/decorators/sigGen.ts delete mode 100644 src/sdk/clients/decorators/dan/index.ts delete mode 100644 src/sdk/modules/ownableValidator/toOwnableValidator.dan.test.ts delete mode 100644 src/sdk/modules/smartSessionsValidator/decorators/useDistributedPermission.ts delete mode 100644 src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dan.dx.test.ts create mode 100644 src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.sudo.policy.test.ts diff --git a/src/sdk/clients/decorators/dan/Helpers.ts b/src/sdk/clients/decorators/dan/Helpers.ts deleted file mode 100644 index 2321509a3..000000000 --- a/src/sdk/clients/decorators/dan/Helpers.ts +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index 4308477aa..000000000 --- a/src/sdk/clients/decorators/dan/decorators/dan.decorators.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -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 deleted file mode 100644 index 5117e96da..000000000 --- a/src/sdk/clients/decorators/dan/decorators/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 2d4ed4ee7..000000000 --- a/src/sdk/clients/decorators/dan/decorators/keyGen.ts +++ /dev/null @@ -1,138 +0,0 @@ -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 deleted file mode 100644 index 363348d20..000000000 --- a/src/sdk/clients/decorators/dan/decorators/sigGen.ts +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index f06563064..000000000 --- a/src/sdk/clients/decorators/dan/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./decorators" -export * from "./Helpers" diff --git a/src/sdk/clients/decorators/index.ts b/src/sdk/clients/decorators/index.ts index 4aff93b4d..a651fc255 100644 --- a/src/sdk/clients/decorators/index.ts +++ b/src/sdk/clients/decorators/index.ts @@ -1,4 +1,3 @@ export * from "./erc7579" export * from "./smartAccount" export * from "./bundler" -export * from "./dan" diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.dan.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.dan.test.ts deleted file mode 100644 index d76fbe91d..000000000 --- a/src/sdk/modules/ownableValidator/toOwnableValidator.dan.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -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/smartSessionsValidator/Helpers.ts b/src/sdk/modules/smartSessionsValidator/Helpers.ts index 745c4a8d3..b61a5783f 100644 --- a/src/sdk/modules/smartSessionsValidator/Helpers.ts +++ b/src/sdk/modules/smartSessionsValidator/Helpers.ts @@ -1,4 +1,9 @@ -import type { ActionData, PolicyData, Session } from "@rhinestone/module-sdk" +import { + type ActionData, + type PolicyData, + type Session, + getSudoPolicy +} from "@rhinestone/module-sdk" import { type Abi, type AbiFunction, @@ -30,7 +35,8 @@ import type { FullCreateSessionDataParams, RawActionConfig, Rule, - SpendingLimitsParams + SpendingLimitsParams, + SudoPolicyData } from "./Types" export const MAX_RULES = 16 @@ -88,6 +94,23 @@ export const applyDefaults = ( sessionInfo.sessionValidatorAddress ?? SIMPLE_SESSION_VALIDATOR_ADDRESS } } +/** + * Creates an ActionData object for a sudo policy. + * + * @param contractAddress - The address of the contract. + * @param functionSelector - The function selector or AbiFunction. + * @returns An ActionData object. + */ +export const createSudoData = ( + contractAddress: Address, + functionSelector: string | AbiFunction +): ActionData => ({ + actionTargetSelector: (typeof functionSelector === "string" + ? functionSelector + : functionSelector.name) as Hex, + actionTarget: contractAddress, + actionPolicies: [getSudoPolicy()] +}) /** * Creates an ActionData object. @@ -237,14 +260,6 @@ export const toTimeRangePolicy = ( return timeFramePolicyData } -/** - * A PolicyData object representing a sudo policy. - */ -export const sudoPolicy: PolicyData = { - policy: "0x529Ad04F4D83aAb25144a90267D4a1443B84f5A6", - initData: "0x" -} - /** * Converts SpendingLimitsParams to a SpendingLimitsPolicy. * @@ -270,8 +285,7 @@ export const policies = { to: { universalAction: toUniversalActionPolicy, spendingLimits: toSpendingLimitsPolicy - }, - sudo: sudoPolicy + } } as const /** @@ -350,20 +364,40 @@ export const getTrustedAttesters = async ({ * @param params.actionPolicyData - The ActionPolicyData object to apply to each function in the ABI * @returns An array of ActionPolicyData objects */ -export function toContractWhitelist({ +export const abi2ActionPolicy = ({ abi, actionPolicyData }: { abi: Abi - actionPolicyData: Omit -}): ActionPolicyData[] { - // Filter out only the functions from the ABI - return abi + actionPolicyData: Omit & { + rules?: never // Rules should not be available here because they should be custom per method, not used in a loop + } +}): ActionPolicyData[] => + abi .filter((item): item is AbiFunction => item.type === "function") .map((func) => ({ ...actionPolicyData, functionSelector: toFunctionSelector(func) })) -} -export default policies +/** + * Converts an ABI to a list of SudoPolicyData objects. + * + * @param params - The parameters object + * @param params.abi - The ABI to convert + * @param params.contractAddress - The address of the contract + * @returns An array of SudoPolicyData objects + */ +export const abi2SudoPolicy = ({ + abi, + contractAddress +}: { + abi: Abi + contractAddress: Address +}): SudoPolicyData[] => + abi + .filter((item): item is AbiFunction => item.type === "function") + .map((func) => ({ + contractAddress, + functionSelector: toFunctionSelector(func) + })) diff --git a/src/sdk/modules/smartSessionsValidator/Types.ts b/src/sdk/modules/smartSessionsValidator/Types.ts index c650ccf06..16f7479b8 100644 --- a/src/sdk/modules/smartSessionsValidator/Types.ts +++ b/src/sdk/modules/smartSessionsValidator/Types.ts @@ -3,7 +3,6 @@ import type { SmartSessionMode } from "@rhinestone/module-sdk" 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" @@ -60,8 +59,6 @@ export type UsePermissionModuleData = { 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 } @@ -93,11 +90,18 @@ export type CreateSessionDataParams = OptionalSessionKeyData & { 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[] -} +} & OneOf< + | { + /** Array of sudo policy data for the session. */ + sudoPoliciesInfo: SudoPolicyData[] + } + | { + /** Array of action policy data for the session. */ + actionPoliciesInfo: ActionPolicyData[] + } + > export type FullCreateSessionDataParams = { /** Public key for the session. Required for K1 algorithm validators. */ @@ -114,10 +118,24 @@ export type FullCreateSessionDataParams = { 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[] +} & OneOf< + | { + /** Array of sudo policy data for the session. */ + sudoPoliciesInfo: SudoPolicyData[] + } + | { + /** Array of action policy data for the session. */ + actionPoliciesInfo: ActionPolicyData[] + } +> + +export type SudoPolicyData = { + /** The address of the contract to be included in the policy */ + contractAddress: Hex + /** The specific function selector from the contract to be included in the policy */ + functionSelector: string | AbiFunction } /** diff --git a/src/sdk/modules/smartSessionsValidator/decorators/grantPermission.ts b/src/sdk/modules/smartSessionsValidator/decorators/grantPermission.ts index 21b26de59..ed361357c 100644 --- a/src/sdk/modules/smartSessionsValidator/decorators/grantPermission.ts +++ b/src/sdk/modules/smartSessionsValidator/decorators/grantPermission.ts @@ -21,6 +21,7 @@ import { applyDefaults, createActionConfig, createActionData, + createSudoData, generateSalt, getPermissionId, toTimeRangePolicy, @@ -81,15 +82,22 @@ export const getPermissionAction = async ({ // Start populating the session for each param provided for (const sessionInfo of sessionRequestedInfo) { const actionPolicies: ActionData[] = [] - for (const actionPolicyInfo of sessionInfo.actionPoliciesInfo) { + + for (const sudoPolicy of sessionInfo.sudoPoliciesInfo ?? []) { + const sudoPolicyData = createSudoData( + sudoPolicy.contractAddress, + sudoPolicy.functionSelector + ) + actionPolicies.push(sudoPolicyData) + } + + for (const actionPolicyInfo of sessionInfo.actionPoliciesInfo ?? []) { // TODO: make it easy to generate rules for particular contract and selectors. const actionConfig = createActionConfig( actionPolicyInfo.rules ?? [], actionPolicyInfo.valueLimit ) - // one may also pass baked up policyData. - // create uni action policy here.. const uniActionPolicyData = toUniversalActionPolicy(actionConfig) // create time range policy here.. @@ -228,7 +236,8 @@ export async function grantPermission< throw new Error(ERROR_MESSAGES.CHAIN_NOT_FOUND) } - const defaultedSessionRequestedInfo = sessionRequestedInfo.map(applyDefaults) + const defaultedSessionRequestedInfo: FullCreateSessionDataParams[] = + sessionRequestedInfo.map(applyDefaults) const attestersToTrust = attesters ?? [MOCK_ATTESTER_ADDRESS] const actionResponse = await getPermissionAction({ diff --git a/src/sdk/modules/smartSessionsValidator/decorators/index.ts b/src/sdk/modules/smartSessionsValidator/decorators/index.ts index bbbec6906..b9e94b152 100644 --- a/src/sdk/modules/smartSessionsValidator/decorators/index.ts +++ b/src/sdk/modules/smartSessionsValidator/decorators/index.ts @@ -1,5 +1,4 @@ import type { Chain, Client, Hash, Transport } from "viem" -import { danActions } from "../../../clients/decorators/dan/decorators" import type { ModularSmartAccount, Module } from "../../utils/Types" import type { GrantPermissionResponse } from "../Types" import type { SmartSessionModule } from "../toSmartSessionsValidator" @@ -8,11 +7,6 @@ import { 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. @@ -60,15 +54,6 @@ export type SmartSessionUseActions< 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 } /** @@ -102,11 +87,7 @@ export function smartSessionUseActions( ): SmartSessionUseActions => { client?.account?.setModule(smartSessionsModule) return { - usePermission: (args) => usePermission(client, args), - useDistributedPermission: (args) => { - const danClient = client.extend(danActions()) as unknown as DanClient - return useDistributedPermission(danClient, args) - } + usePermission: (args) => usePermission(client, args) } } } @@ -114,4 +95,3 @@ export function smartSessionUseActions( export * from "./grantPermission" export * from "./trustAttesters" export * from "./usePermission" -export * from "./useDistributedPermission" diff --git a/src/sdk/modules/smartSessionsValidator/decorators/useDistributedPermission.ts b/src/sdk/modules/smartSessionsValidator/decorators/useDistributedPermission.ts deleted file mode 100644 index fa82c488a..000000000 --- a/src/sdk/modules/smartSessionsValidator/decorators/useDistributedPermission.ts +++ /dev/null @@ -1,113 +0,0 @@ -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/smartSessionsValidator/toSmartSessionsValidator.dan.dx.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dan.dx.test.ts deleted file mode 100644 index 08d9d6dcf..000000000 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dan.dx.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -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, toContractWhitelist } 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) - - const contractWhitelist = toContractWhitelist({ - abi: CounterAbi, - actionPolicyData: { - contractAddress: testAddresses.Counter - } - }) - - // Define the permissions for the smart session - const sessionRequestedInfo: CreateSessionDataParams[] = [ - { - sessionPublicKey, // Public key of the session stored in DAN - actionPoliciesInfo: [...contractWhitelist] - } - ] - - // 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 index 169501eea..9f4a0416b 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dx.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dx.test.ts @@ -25,7 +25,7 @@ import { } from "../../clients/createNexusClient" import { createNexusSessionClient } from "../../clients/createNexusSessionClient" import type { Module } from "../utils/Types" -import { parse, stringify, toContractWhitelist } from "./Helpers" +import { abi2ActionPolicy, parse, stringify } from "./Helpers" import type { CreateSessionDataParams, SessionData } from "./Types" import { smartSessionCreateActions, smartSessionUseActions } from "./decorators" import { toSmartSessionsValidator } from "./toSmartSessionsValidator" @@ -113,25 +113,26 @@ describe("modules.smartSessions.dx", async () => { expect(installSuccess).toBe(true) - const actionPoliciesInfo = toContractWhitelist({ - abi: CounterAbi, - actionPolicyData: { - contractAddress: testAddresses.Counter - } - }) - // Define the session parameters // This includes the session key, validator, and action policies - const sessionRequestedInfo: CreateSessionDataParams[] = [ - { - sessionPublicKey, // Public key of the session - actionPoliciesInfo - } - ] - - // Create the smart session const createSessionsResponse = await nexusSessionClient.grantPermission({ - sessionRequestedInfo + sessionRequestedInfo: [ + { + sessionPublicKey, // Public key of the session + // sessionValidUntil: number + // sessionValidAfter: number + // chainIds: bigint[] + actionPoliciesInfo: abi2ActionPolicy({ + abi: CounterAbi, + actionPolicyData: { + contractAddress: testAddresses.Counter + // validUntil?: number + // validAfter?: number + // valueLimit?: bigint + } + }) + } + ] }) // Wait for the session creation transaction to be mined and check its success @@ -140,7 +141,7 @@ describe("modules.smartSessions.dx", async () => { hash: createSessionsResponse.userOpHash }) - expect(installSuccess).toBe(sessionCreateSuccess) + expect(sessionCreateSuccess).toBe(true) // 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 = { @@ -185,7 +186,7 @@ describe("modules.smartSessions.dx", async () => { smartSessionUseActions(usePermissionsModule) ) - // Use the session to perform an action (increment the counter) + // Use the session to perform an action (increment and decrement the counter using the same permissionId) const userOpHash = await useSmartSessionNexusClient.usePermission({ calls: [ { @@ -194,6 +195,13 @@ describe("modules.smartSessions.dx", async () => { abi: CounterAbi, functionName: "incrementNumber" }) + }, + { + to: testAddresses.Counter, + data: encodeFunctionData({ + abi: CounterAbi, + functionName: "decrementNumber" + }) } ] }) diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.sudo.policy.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.sudo.policy.test.ts new file mode 100644 index 000000000..fa20026c2 --- /dev/null +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.sudo.policy.test.ts @@ -0,0 +1,213 @@ +import { SmartSessionMode } from "@rhinestone/module-sdk/module" +import { + http, + type AbiFunction, + type Address, + type Chain, + type Hex, + type LocalAccount, + type PublicClient, + encodeFunctionData, + getContract, + slice, + toFunctionSelector +} 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 { 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 type { Module } from "../utils/Types" +import { abi2SudoPolicy, parse, stringify } from "./Helpers" +import type { CreateSessionDataParams, SessionData } from "./Types" +import { smartSessionCreateActions, smartSessionUseActions } from "./decorators" +import { toSmartSessionsValidator } from "./toSmartSessionsValidator" + +describe("modules.smartSessions.sudo.policy", 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 zippedSessionDatum: string // Session data to be stored by the dApp + + 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 install smartSessionValidator with no init data", async () => { + // Create a smart sessions module for the user's account + sessionsModule = toSmartSessionsValidator({ + account: nexusClient.account, + signer: eoaAccount + }) + + // Install the smart sessions module on the Nexus client's smart contract account + const hash = await nexusClient.installModule({ + module: sessionsModule.moduleInitData + }) + + // Extend the Nexus client with smart session creation actions + const usersNexusClient = nexusClient.extend( + smartSessionCreateActions(sessionsModule) + ) + + const createSessionsResponse = await usersNexusClient.grantPermission({ + sessionRequestedInfo: [ + { + sessionPublicKey, // session key signer + // sessionValidUntil: number + // sessionValidAfter: number + // chainIds: bigint[] + sudoPoliciesInfo: abi2SudoPolicy({ + abi: CounterAbi, + contractAddress: testAddresses.Counter + }) + } + ] + }) + + // Wait for the session creation transaction to be mined and check its success + const { success: sessionCreateSuccess } = + await usersNexusClient.waitForUserOperationReceipt({ + hash: createSessionsResponse.userOpHash + }) + + expect(sessionCreateSuccess).toBe(true) + + // 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 as Hex, + sessionPublicKey, + moduleData: { + permissionIds: createSessionsResponse.permissionIds, + mode: SmartSessionMode.USE + } + } + + // Zip the session data, and store it for later use by a dapp + zippedSessionDatum = stringify(sessionData) + }) + + 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 createNexusClient({ + 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) + ) + + const counterBefore = await testClient.readContract({ + address: testAddresses.Counter, + abi: CounterAbi, + functionName: "getNumber" + }) + + // 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" + }) + }, + { + to: testAddresses.Counter, + data: encodeFunctionData({ + abi: CounterAbi, + functionName: "decrementNumber" + }) + } + ] + }) + + const counterAfter = await testClient.readContract({ + address: testAddresses.Counter, + abi: CounterAbi, + functionName: "getNumber" + }) + + // Counter should be unchanged + expect(counterAfter).toBe(counterBefore) + + // 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 index ba5cb821e..a0726b84c 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.test.ts @@ -27,7 +27,7 @@ import { import { createNexusSessionClient } from "../../clients/createNexusSessionClient" import { parseReferenceValue } from "../utils/Helpers" import type { Module } from "../utils/Types" -import policies, { toContractWhitelist } from "./Helpers" +import { abi2ActionPolicy, toUniversalActionPolicy } from "./Helpers" import type { CreateSessionDataParams } from "./Types" import { ParamCondition } from "./Types" import { smartSessionCreateActions, smartSessionUseActions } from "./decorators" @@ -86,7 +86,7 @@ describe("modules.smartSessions", async () => { }) test("should convert an ABI to a contract whitelist", async () => { - const contractWhitelist = toContractWhitelist({ + const contractWhitelist = abi2ActionPolicy({ abi: CounterAbi, actionPolicyData: { contractAddress: testAddresses.Counter @@ -165,30 +165,12 @@ describe("modules.smartSessions", async () => { ] } } - const installUniversalPolicy = policies.to.universalAction(actionConfigData) + const installUniversalPolicy = toUniversalActionPolicy(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 () => { diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.ts index 7f4ca78e6..24530d235 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.ts @@ -103,8 +103,7 @@ export const toSmartSessionsValidator = ( permissionIdIndex = 0, permissionIds = [], mode = SmartSessionMode.USE, - enableSessionData, - keyGenData: _ + enableSessionData } = {} } = parameters @@ -135,16 +134,6 @@ export const toSmartSessionsValidator = ( 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 index 7856a883d..cef0fefc8 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.uni.policy.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.uni.policy.test.ts @@ -39,7 +39,7 @@ import { ParamCondition } from "./Types" import { smartSessionCreateActions, smartSessionUseActions } from "./decorators" import { toSmartSessionsValidator } from "./toSmartSessionsValidator" -describe("modules.smartSessions.uniPolicy", async () => { +describe("modules.smartSessions.uni.policy", async () => { let network: NetworkConfig let chain: Chain let bundlerUrl: string