Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 45 additions & 9 deletions packages/smart-accounts-kit/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,20 +206,56 @@ const resolveStateless7702Signer = (
throw new Error('Invalid signer config');
};

export const resolveSigner = <TImplementation extends Implementation>(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<TImplementation extends Implementation>(config: {
implementation: TImplementation;
signer: SignerConfigByImplementation<TImplementation>;
}): 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<TImplementation extends Implementation>(config: {
implementation: TImplementation;
signer?: SignerConfigByImplementation<TImplementation>;
}): 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<TImplementation extends Implementation>(config: {
implementation: TImplementation;
signer?: SignerConfigByImplementation<TImplementation>;
}): 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`);
};
}
32 changes: 31 additions & 1 deletion packages/smart-accounts-kit/src/toMetaMaskSmartAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/smart-accounts-kit/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export type ToMetaMaskSmartAccountParameters<
> = {
client: PublicClient;
implementation: TImplementation;
signer: SignerConfigByImplementation<TImplementation>;
signer?: SignerConfigByImplementation<TImplementation>;
environment?: SmartAccountsEnvironment;
} & OneOf<
| {
Expand Down
125 changes: 125 additions & 0 deletions packages/smart-accounts-kit/test/toMetaMaskSmartAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading