Skip to content

Commit 2a94081

Browse files
Make signer param optional in toMetaMaskSmartAccount (#178)
* 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 <jeffsmale90@users.noreply.github.com> * Add doc comments to resolveSigner functions Also resolve a little bit of formatting --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: jeffsmale90 <jeffsmale90@users.noreply.github.com>
1 parent abe1266 commit 2a94081

4 files changed

Lines changed: 202 additions & 11 deletions

File tree

packages/smart-accounts-kit/src/signer.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -206,20 +206,56 @@ const resolveStateless7702Signer = (
206206
throw new Error('Invalid signer config');
207207
};
208208

209-
export const resolveSigner = <TImplementation extends Implementation>(config: {
209+
/**
210+
* Resolve a signer from a configuration object.
211+
*
212+
* @param config - The configuration object.
213+
* @param config.implementation - The implementation type.
214+
* @param config.signer - The signer configuration object.
215+
* @returns The resolved signer.
216+
*/
217+
export function resolveSigner<TImplementation extends Implementation>(config: {
210218
implementation: TImplementation;
211219
signer: SignerConfigByImplementation<TImplementation>;
212-
}): InternalSigner => {
213-
const { implementation } = config;
220+
}): InternalSigner;
221+
222+
/**
223+
* Resolve a signer from a configuration object. If no signer is provided, return null.
224+
*
225+
* @param config - The configuration object.
226+
* @param config.implementation - The implementation type.
227+
* @param config.signer - The signer configuration object.
228+
* @returns The resolved signer or null if no signer is provided.
229+
*/
230+
export function resolveSigner<TImplementation extends Implementation>(config: {
231+
implementation: TImplementation;
232+
signer?: SignerConfigByImplementation<TImplementation>;
233+
}): InternalSigner | null;
234+
235+
/**
236+
* Resolve a signer from a configuration object. If no signer is provided, return null.
237+
*
238+
* @param config - The configuration object.
239+
* @param config.implementation - The implementation type.
240+
* @param config.signer - The signer configuration object.
241+
* @returns The resolved signer or null if no signer is provided.
242+
*/
243+
export function resolveSigner<TImplementation extends Implementation>(config: {
244+
implementation: TImplementation;
245+
signer?: SignerConfigByImplementation<TImplementation>;
246+
}): InternalSigner | null {
247+
const { implementation, signer } = config;
248+
249+
if (!signer) {
250+
return null;
251+
}
214252

215253
if (implementation === Implementation.Hybrid) {
216-
return resolveHybridSigner(config.signer as HybridSignerConfig);
254+
return resolveHybridSigner(signer as HybridSignerConfig);
217255
} else if (implementation === Implementation.MultiSig) {
218-
return resolveMultiSigSigner(config.signer as MultiSigSignerConfig);
256+
return resolveMultiSigSigner(signer as MultiSigSignerConfig);
219257
} else if (implementation === Implementation.Stateless7702) {
220-
return resolveStateless7702Signer(
221-
config.signer as Stateless7702SignerConfig,
222-
);
258+
return resolveStateless7702Signer(signer as Stateless7702SignerConfig);
223259
}
224260
throw new Error(`Implementation type '${implementation}' not supported`);
225-
};
261+
}

packages/smart-accounts-kit/src/toMetaMaskSmartAccount.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ export async function toMetaMaskSmartAccount<
126126
};
127127

128128
const signDelegation = async (delegationParams: SignDelegationParams) => {
129+
if (!signer) {
130+
throw new Error(
131+
'Cannot sign delegation: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.',
132+
);
133+
}
134+
129135
const { delegation, chainId } = delegationParams;
130136

131137
const delegationStruct = toDelegationStruct({
@@ -149,6 +155,12 @@ export async function toMetaMaskSmartAccount<
149155
};
150156

151157
const signUserOperation = async (userOpParams: SignUserOperationParams) => {
158+
if (!signer) {
159+
throw new Error(
160+
'Cannot sign user operation: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.',
161+
);
162+
}
163+
152164
const { chainId } = userOpParams;
153165

154166
const packedUserOp = toPackedUserOperation({
@@ -185,6 +197,24 @@ export async function toMetaMaskSmartAccount<
185197
const encodeCalls = async (calls: readonly Call[]) =>
186198
encodeCallsForCaller(address, calls);
187199

200+
const signerMethods = signer ?? {
201+
signMessage: async () => {
202+
throw new Error(
203+
'Cannot sign message: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.',
204+
);
205+
},
206+
signTypedData: async () => {
207+
throw new Error(
208+
'Cannot sign typed data: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.',
209+
);
210+
},
211+
getStubSignature: async () => {
212+
throw new Error(
213+
'Cannot get stub signature: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.',
214+
);
215+
},
216+
};
217+
188218
const smartAccount = await toSmartAccount({
189219
abi,
190220
client,
@@ -196,7 +226,7 @@ export async function toMetaMaskSmartAccount<
196226
getNonce,
197227
signUserOperation,
198228
signDelegation,
199-
...signer,
229+
...signerMethods,
200230
});
201231

202232
// Override isDeployed only for EIP-7702 implementation to check proper delegation code

packages/smart-accounts-kit/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export type ToMetaMaskSmartAccountParameters<
152152
> = {
153153
client: PublicClient;
154154
implementation: TImplementation;
155-
signer: SignerConfigByImplementation<TImplementation>;
155+
signer?: SignerConfigByImplementation<TImplementation>;
156156
environment?: SmartAccountsEnvironment;
157157
} & OneOf<
158158
| {

packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,131 @@ describe('MetaMaskSmartAccount', () => {
142142
expect(smartAccount).toBeInstanceOf(Object);
143143
});
144144
});
145+
describe('optional signer', () => {
146+
it('creates a MetaMaskSmartAccount for Hybrid implementation without signer', async () => {
147+
const smartAccount = await toMetaMaskSmartAccount({
148+
client: publicClient,
149+
implementation: Implementation.Hybrid,
150+
deployParams: [alice.address, [], [], []],
151+
deploySalt: '0x0',
152+
environment,
153+
});
154+
155+
expect(isAddress(smartAccount.address)).toBe(true);
156+
expect(smartAccount).to.have.property('getAddress');
157+
expect(smartAccount).to.have.property('getNonce');
158+
expect(smartAccount).to.have.property('encodeCalls');
159+
});
160+
161+
it('creates a MetaMaskSmartAccount for MultiSig implementation without signer', async () => {
162+
const smartAccount = await toMetaMaskSmartAccount({
163+
client: publicClient,
164+
implementation: Implementation.MultiSig,
165+
deployParams: [[alice.address, bob.address], 2n],
166+
deploySalt: '0x0',
167+
environment,
168+
});
169+
170+
expect(isAddress(smartAccount.address)).toBe(true);
171+
expect(smartAccount).to.have.property('getAddress');
172+
expect(smartAccount).to.have.property('getNonce');
173+
expect(smartAccount).to.have.property('encodeCalls');
174+
});
175+
176+
it('creates a MetaMaskSmartAccount for Stateless7702 implementation without signer', async () => {
177+
const smartAccount = await toMetaMaskSmartAccount({
178+
client: publicClient,
179+
implementation: Implementation.Stateless7702,
180+
address: alice.address,
181+
environment,
182+
});
183+
184+
expect(smartAccount.address).to.equal(alice.address);
185+
expect(smartAccount).to.have.property('getAddress');
186+
expect(smartAccount).to.have.property('getNonce');
187+
expect(smartAccount).to.have.property('encodeCalls');
188+
});
189+
190+
it('allows non-signing operations without signer - getAddress', async () => {
191+
const smartAccount = await toMetaMaskSmartAccount({
192+
client: publicClient,
193+
implementation: Implementation.Hybrid,
194+
deployParams: [alice.address, [], [], []],
195+
deploySalt: '0x0',
196+
environment,
197+
});
198+
199+
const address = await smartAccount.getAddress();
200+
expect(isAddress(address)).toBe(true);
201+
});
202+
203+
it('allows non-signing operations without signer - encodeCalls', async () => {
204+
const smartAccount = await toMetaMaskSmartAccount({
205+
client: publicClient,
206+
implementation: Implementation.Hybrid,
207+
deployParams: [alice.address, [], [], []],
208+
deploySalt: '0x0',
209+
environment,
210+
});
211+
212+
const encoded = await smartAccount.encodeCalls([
213+
{ to: alice.address, data: '0x', value: 0n },
214+
]);
215+
expect(isHex(encoded)).toBe(true);
216+
});
217+
218+
it('throws error when signUserOperation is called without signer', async () => {
219+
const smartAccount = await toMetaMaskSmartAccount({
220+
client: publicClient,
221+
implementation: Implementation.Hybrid,
222+
deployParams: [alice.address, [], [], []],
223+
deploySalt: '0x0',
224+
environment,
225+
});
226+
227+
const userOperation = {
228+
callData: '0x',
229+
sender: alice.address,
230+
nonce: 0n,
231+
callGasLimit: 1000000n,
232+
preVerificationGas: 1000000n,
233+
verificationGasLimit: 1000000n,
234+
maxFeePerGas: 1000000000000000000n,
235+
maxPriorityFeePerGas: 1000000000000000000n,
236+
signature: '0x',
237+
} as const;
238+
239+
await expect(
240+
smartAccount.signUserOperation(userOperation),
241+
).rejects.toThrow(
242+
'Cannot sign user operation: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.',
243+
);
244+
});
245+
246+
it('throws error when signDelegation is called without signer', async () => {
247+
const smartAccount = await toMetaMaskSmartAccount({
248+
client: publicClient,
249+
implementation: Implementation.Hybrid,
250+
deployParams: [alice.address, [], [], []],
251+
deploySalt: '0x0',
252+
environment,
253+
});
254+
255+
const delegation = {
256+
delegate: alice.address,
257+
delegator: bob.address,
258+
authority:
259+
'0x0000000000000000000000000000000000000000000000000000000000000000' as const,
260+
caveats: [],
261+
salt: '0x0000000000000000000000000000000000000000000000000000000000000000' as const,
262+
};
263+
264+
await expect(smartAccount.signDelegation({ delegation })).rejects.toThrow(
265+
'Cannot sign delegation: signer not provided. Specify a signer in toMetaMaskSmartAccount() to perform signing operations.',
266+
);
267+
});
268+
});
269+
145270
describe('signUserOperation()', () => {
146271
it('signs a user operation for MultiSig implementation', async () => {
147272
const smartAccount = await toMetaMaskSmartAccount({

0 commit comments

Comments
 (0)