diff --git a/packages/smart-accounts-kit/src/signer.ts b/packages/smart-accounts-kit/src/signer.ts index f0faf79..b2910a2 100644 --- a/packages/smart-accounts-kit/src/signer.ts +++ b/packages/smart-accounts-kit/src/signer.ts @@ -206,20 +206,56 @@ const resolveStateless7702Signer = ( throw new Error('Invalid signer config'); }; -export const resolveSigner = (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 => { - const { implementation } = config; +}): 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; +}): 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 297720a..b3949de 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 a01a94a..fc30b36 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 75532d9..8a0712e 100644 --- a/packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts +++ b/packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts @@ -142,6 +142,131 @@ 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({