From a6efedf6e1ef6d7c3de024b52042230b3724a6b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Mar 2026 19:28:17 +0000 Subject: [PATCH 1/2] Make signer parameter optional in toMetaMaskSmartAccount - Update ToMetaMaskSmartAccountParameters type to make signer optional - Add overloaded resolveSigner function to handle optional signer - Update toMetaMaskSmartAccount to provide stub signer methods that throw descriptive errors when signer is not provided - Add error messages for signDelegation and signUserOperation when called without signer - Add comprehensive tests for optional signer functionality including: - Creating smart accounts without signers for all implementations - Testing non-signing operations (getAddress, encodeCalls) work without signer - Testing signing operations throw appropriate errors without signer Fixes #163 Co-authored-by: jeffsmale90 --- packages/smart-accounts-kit/src/signer.ts | 30 +++-- .../src/toMetaMaskSmartAccount.ts | 32 ++++- packages/smart-accounts-kit/src/types.ts | 2 +- .../test/toMetaMaskSmartAccount.test.ts | 126 ++++++++++++++++++ 4 files changed, 179 insertions(+), 11 deletions(-) diff --git a/packages/smart-accounts-kit/src/signer.ts b/packages/smart-accounts-kit/src/signer.ts index f0faf790..b01ebd49 100644 --- a/packages/smart-accounts-kit/src/signer.ts +++ b/packages/smart-accounts-kit/src/signer.ts @@ -206,20 +206,32 @@ const resolveStateless7702Signer = ( throw new Error('Invalid signer config'); }; -export const resolveSigner = (config: { +export function resolveSigner(config: { implementation: TImplementation; signer: SignerConfigByImplementation; -}): InternalSigner => { - const { implementation } = config; +}): InternalSigner; + +export function resolveSigner(config: { + implementation: TImplementation; + signer?: SignerConfigByImplementation; +}): InternalSigner | null; + +export function resolveSigner(config: { + implementation: TImplementation; + signer?: SignerConfigByImplementation; +}): InternalSigner | null { + const { implementation, signer } = config; + + if (!signer) { + return null; + } if (implementation === Implementation.Hybrid) { - return resolveHybridSigner(config.signer as HybridSignerConfig); + return resolveHybridSigner(signer as HybridSignerConfig); } else if (implementation === Implementation.MultiSig) { - return resolveMultiSigSigner(config.signer as MultiSigSignerConfig); + return resolveMultiSigSigner(signer as MultiSigSignerConfig); } else if (implementation === Implementation.Stateless7702) { - return resolveStateless7702Signer( - config.signer as Stateless7702SignerConfig, - ); + return resolveStateless7702Signer(signer as Stateless7702SignerConfig); } throw new Error(`Implementation type '${implementation}' not supported`); -}; +} diff --git a/packages/smart-accounts-kit/src/toMetaMaskSmartAccount.ts b/packages/smart-accounts-kit/src/toMetaMaskSmartAccount.ts index 297720a8..b3949de4 100644 --- a/packages/smart-accounts-kit/src/toMetaMaskSmartAccount.ts +++ b/packages/smart-accounts-kit/src/toMetaMaskSmartAccount.ts @@ -126,6 +126,12 @@ export async function toMetaMaskSmartAccount< }; const signDelegation = async (delegationParams: SignDelegationParams) => { + if (!signer) { + throw new Error( + 'Cannot sign delegation: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.', + ); + } + const { delegation, chainId } = delegationParams; const delegationStruct = toDelegationStruct({ @@ -149,6 +155,12 @@ export async function toMetaMaskSmartAccount< }; const signUserOperation = async (userOpParams: SignUserOperationParams) => { + if (!signer) { + throw new Error( + 'Cannot sign user operation: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.', + ); + } + const { chainId } = userOpParams; const packedUserOp = toPackedUserOperation({ @@ -185,6 +197,24 @@ export async function toMetaMaskSmartAccount< const encodeCalls = async (calls: readonly Call[]) => encodeCallsForCaller(address, calls); + const signerMethods = signer ?? { + signMessage: async () => { + throw new Error( + 'Cannot sign message: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.', + ); + }, + signTypedData: async () => { + throw new Error( + 'Cannot sign typed data: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.', + ); + }, + getStubSignature: async () => { + throw new Error( + 'Cannot get stub signature: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.', + ); + }, + }; + const smartAccount = await toSmartAccount({ abi, client, @@ -196,7 +226,7 @@ export async function toMetaMaskSmartAccount< getNonce, signUserOperation, signDelegation, - ...signer, + ...signerMethods, }); // Override isDeployed only for EIP-7702 implementation to check proper delegation code diff --git a/packages/smart-accounts-kit/src/types.ts b/packages/smart-accounts-kit/src/types.ts index a01a94a3..fc30b361 100644 --- a/packages/smart-accounts-kit/src/types.ts +++ b/packages/smart-accounts-kit/src/types.ts @@ -152,7 +152,7 @@ export type ToMetaMaskSmartAccountParameters< > = { client: PublicClient; implementation: TImplementation; - signer: SignerConfigByImplementation; + signer?: SignerConfigByImplementation; environment?: SmartAccountsEnvironment; } & OneOf< | { diff --git a/packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts b/packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts index 75532d96..7aec67b9 100644 --- a/packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts +++ b/packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts @@ -142,6 +142,132 @@ describe('MetaMaskSmartAccount', () => { expect(smartAccount).toBeInstanceOf(Object); }); }); + describe('optional signer', () => { + it('creates a MetaMaskSmartAccount for Hybrid implementation without signer', async () => { + const smartAccount = await toMetaMaskSmartAccount({ + client: publicClient, + implementation: Implementation.Hybrid, + deployParams: [alice.address, [], [], []], + deploySalt: '0x0', + environment, + }); + + expect(isAddress(smartAccount.address)).toBe(true); + expect(smartAccount).to.have.property('getAddress'); + expect(smartAccount).to.have.property('getNonce'); + expect(smartAccount).to.have.property('encodeCalls'); + }); + + it('creates a MetaMaskSmartAccount for MultiSig implementation without signer', async () => { + const smartAccount = await toMetaMaskSmartAccount({ + client: publicClient, + implementation: Implementation.MultiSig, + deployParams: [[alice.address, bob.address], 2n], + deploySalt: '0x0', + environment, + }); + + expect(isAddress(smartAccount.address)).toBe(true); + expect(smartAccount).to.have.property('getAddress'); + expect(smartAccount).to.have.property('getNonce'); + expect(smartAccount).to.have.property('encodeCalls'); + }); + + it('creates a MetaMaskSmartAccount for Stateless7702 implementation without signer', async () => { + const smartAccount = await toMetaMaskSmartAccount({ + client: publicClient, + implementation: Implementation.Stateless7702, + address: alice.address, + environment, + }); + + expect(smartAccount.address).to.equal(alice.address); + expect(smartAccount).to.have.property('getAddress'); + expect(smartAccount).to.have.property('getNonce'); + expect(smartAccount).to.have.property('encodeCalls'); + }); + + it('allows non-signing operations without signer - getAddress', async () => { + const smartAccount = await toMetaMaskSmartAccount({ + client: publicClient, + implementation: Implementation.Hybrid, + deployParams: [alice.address, [], [], []], + deploySalt: '0x0', + environment, + }); + + const address = await smartAccount.getAddress(); + expect(isAddress(address)).toBe(true); + }); + + it('allows non-signing operations without signer - encodeCalls', async () => { + const smartAccount = await toMetaMaskSmartAccount({ + client: publicClient, + implementation: Implementation.Hybrid, + deployParams: [alice.address, [], [], []], + deploySalt: '0x0', + environment, + }); + + const encoded = await smartAccount.encodeCalls([ + { to: alice.address, data: '0x', value: 0n }, + ]); + expect(isHex(encoded)).toBe(true); + }); + + it('throws error when signUserOperation is called without signer', async () => { + const smartAccount = await toMetaMaskSmartAccount({ + client: publicClient, + implementation: Implementation.Hybrid, + deployParams: [alice.address, [], [], []], + deploySalt: '0x0', + environment, + }); + + const userOperation = { + callData: '0x', + sender: alice.address, + nonce: 0n, + callGasLimit: 1000000n, + preVerificationGas: 1000000n, + verificationGasLimit: 1000000n, + maxFeePerGas: 1000000000000000000n, + maxPriorityFeePerGas: 1000000000000000000n, + signature: '0x', + } as const; + + await expect( + smartAccount.signUserOperation(userOperation), + ).rejects.toThrow( + 'Cannot sign user operation: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.', + ); + }); + + it('throws error when signDelegation is called without signer', async () => { + const smartAccount = await toMetaMaskSmartAccount({ + client: publicClient, + implementation: Implementation.Hybrid, + deployParams: [alice.address, [], [], []], + deploySalt: '0x0', + environment, + }); + + const delegation = { + delegate: alice.address, + delegator: bob.address, + authority: '0x0000000000000000000000000000000000000000000000000000000000000000' as const, + caveats: [], + salt: '0x0000000000000000000000000000000000000000000000000000000000000000' as const, + }; + + await expect( + smartAccount.signDelegation({ delegation }), + ).rejects.toThrow( + 'Cannot sign delegation: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.', + ); + }); + }); + describe('signUserOperation()', () => { it('signs a user operation for MultiSig implementation', async () => { const smartAccount = await toMetaMaskSmartAccount({ From 381134403d9b25afde05e4d428c40c139980aff3 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:46:33 +1300 Subject: [PATCH 2/2] Add doc comments to resolveSigner functions Also resolve a little bit of formatting --- packages/smart-accounts-kit/src/signer.ts | 24 +++++++++++++++++++ .../test/toMetaMaskSmartAccount.test.ts | 7 +++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/smart-accounts-kit/src/signer.ts b/packages/smart-accounts-kit/src/signer.ts index b01ebd49..b2910a25 100644 --- a/packages/smart-accounts-kit/src/signer.ts +++ b/packages/smart-accounts-kit/src/signer.ts @@ -206,16 +206,40 @@ const resolveStateless7702Signer = ( throw new Error('Invalid signer config'); }; +/** + * Resolve a signer from a configuration object. + * + * @param config - The configuration object. + * @param config.implementation - The implementation type. + * @param config.signer - The signer configuration object. + * @returns The resolved signer. + */ export function resolveSigner(config: { implementation: TImplementation; signer: SignerConfigByImplementation; }): InternalSigner; +/** + * Resolve a signer from a configuration object. If no signer is provided, return null. + * + * @param config - The configuration object. + * @param config.implementation - The implementation type. + * @param config.signer - The signer configuration object. + * @returns The resolved signer or null if no signer is provided. + */ export function resolveSigner(config: { implementation: TImplementation; signer?: SignerConfigByImplementation; }): InternalSigner | null; +/** + * Resolve a signer from a configuration object. If no signer is provided, return null. + * + * @param config - The configuration object. + * @param config.implementation - The implementation type. + * @param config.signer - The signer configuration object. + * @returns The resolved signer or null if no signer is provided. + */ export function resolveSigner(config: { implementation: TImplementation; signer?: SignerConfigByImplementation; diff --git a/packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts b/packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts index 7aec67b9..8a0712ec 100644 --- a/packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts +++ b/packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts @@ -255,14 +255,13 @@ describe('MetaMaskSmartAccount', () => { const delegation = { delegate: alice.address, delegator: bob.address, - authority: '0x0000000000000000000000000000000000000000000000000000000000000000' as const, + authority: + '0x0000000000000000000000000000000000000000000000000000000000000000' as const, caveats: [], salt: '0x0000000000000000000000000000000000000000000000000000000000000000' as const, }; - await expect( - smartAccount.signDelegation({ delegation }), - ).rejects.toThrow( + await expect(smartAccount.signDelegation({ delegation })).rejects.toThrow( 'Cannot sign delegation: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.', ); });