Skip to content

feat: splitting signing methods into prepare and sign #1629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
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
42 changes: 40 additions & 2 deletions aa-sdk/core/src/account/smartContractAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ export enum DeploymentState {
DEPLOYED = "0x2",
}

export type SignatureRequest =
| {
type: "personal_sign";
data: SignableMessage;
}
| {
type: "eth_signTypedData_v4";
data: TypedDataDefinition;
};

export type SigningMethods = {
prepareSign: (request: SignatureRequest) => Promise<SignatureRequest>;
formatSign: (signature: Hex) => Promise<Hex>;
};

export type GetEntryPointFromAccount<
TAccount extends SmartContractAccount | undefined,
TAccountOverride extends SmartContractAccount = SmartContractAccount,
Expand Down Expand Up @@ -126,7 +141,7 @@ export type SmartContractAccount<
getFactoryData: () => Promise<Hex>;
getEntryPoint: () => EntryPointDef<TEntryPointVersion>;
getImplementationAddress: () => Promise<NullAddress | Address>;
};
} & SigningMethods;
// [!endregion SmartContractAccount]

export interface AccountEntryPointRegistry<Name extends string = string>
Expand Down Expand Up @@ -158,7 +173,8 @@ export type ToSmartContractAccountParams<
signUserOperationHash?: (uoHash: Hex) => Promise<Hex>;
encodeUpgradeToAndCall?: (params: UpgradeToAndCallParams) => Promise<Hex>;
getImplementationAddress?: () => Promise<NullAddress | Address>;
} & Omit<CustomSource, "signTransaction" | "address">;
} & Omit<CustomSource, "signTransaction" | "address"> &
(SigningMethods | Never<SigningMethods>);
// [!endregion ToSmartContractAccountParams]

/**
Expand Down Expand Up @@ -351,6 +367,8 @@ export async function toSmartContractAccount(
signUserOperationHash,
encodeUpgradeToAndCall,
getImplementationAddress,
prepareSign: prepareSign_,
formatSign: formatSign_,
} = params;

const client = createBundlerClient({
Expand Down Expand Up @@ -502,6 +520,24 @@ export async function toSmartContractAccount(
throw new InvalidEntryPointError(chain, entryPoint.version);
}

if ((prepareSign_ && !formatSign_) || (!prepareSign_ && formatSign_)) {
throw new Error(
"Must implement both prepareSign and formatSign or neither",
);
}

const prepareSign =
prepareSign_ ??
(() => {
throw new Error("prepareSign not implemented");
});

const formatSign =
formatSign_ ??
(() => {
throw new Error("formatSign not implemented");
});

return {
...account,
source,
Expand All @@ -525,5 +561,7 @@ export async function toSmartContractAccount(
signMessageWith6492,
signTypedDataWith6492,
getImplementationAddress: getImplementationAddress_,
prepareSign,
formatSign,
};
}
117 changes: 73 additions & 44 deletions account-kit/smart-contracts/src/light-account/accounts/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ import {
type Address,
type Chain,
type Hex,
type SignTypedDataParameters,
type Transport,
type TypedData,
type TypedDataDefinition,
} from "viem";
import type {
LightAccountEntryPointVersion,
LightAccountType,
LightAccountVersion,
} from "../types.js";
import { AccountVersionRegistry } from "../utils.js";
import type { SignatureRequest } from "@aa-sdk/core";

enum SignatureType {
EOA = "0x00",
Expand Down Expand Up @@ -142,11 +144,11 @@ export async function createLightAccountBase<
});
};

const signWith1271Wrapper = async (
const get1271Wrapper = (
hashedMessage: Hex,
version: string,
): Promise<Hex> => {
return signer.signTypedData({
): TypedDataDefinition => {
return {
// EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
// https://github.com/alchemyplatform/light-account/blob/main/src/LightAccount.sol#L236
domain: {
Expand All @@ -162,7 +164,45 @@ export async function createLightAccountBase<
message: hashedMessage,
},
primaryType: "LightAccountMessage",
});
};
};

const prepareSign = async (
params: SignatureRequest,
): Promise<SignatureRequest> => {
const messageHash =
params.type === "personal_sign"
? hashMessage(params.data)
: hashTypedData(params.data);

switch (version as string) {
case "v1.0.1":
return params;
case "v1.0.2":
throw new Error(
`Version ${String(version)} of LightAccount doesn't support 1271`,
);
case "v1.1.0":
return {
type: "eth_signTypedData_v4",
data: get1271Wrapper(messageHash, "1"),
};
case "v2.0.0":
return {
type: "eth_signTypedData_v4",
data: get1271Wrapper(messageHash, "2"),
};
default:
throw new Error(`Unknown version ${String(version)} of LightAccount`);
}
};

const formatSign = async (
signature: `0x${string}`,
): Promise<`0x${string}`> => {
return version === "v2.0.0"
? concat([SignatureType.EOA, signature])
: signature;
};

const account = await toSmartContractAccount({
Expand All @@ -172,6 +212,8 @@ export async function createLightAccountBase<
accountAddress,
source: type,
getAccountInitCode,
prepareSign,
formatSign,
encodeExecute: async ({ target, data, value }) => {
return encodeFunctionData({
abi,
Expand Down Expand Up @@ -207,46 +249,33 @@ export async function createLightAccountBase<
}
},
async signMessage({ message }) {
switch (version as string) {
case "v1.0.1":
return signer.signMessage(message);
case "v1.0.2":
throw new Error(`${type} ${String(version)} doesn't support 1271`);
case "v1.1.0":
return signWith1271Wrapper(hashMessage(message), "1");
case "v2.0.0":
const signature = await signWith1271Wrapper(
hashMessage(message),
"2",
);
// TODO: handle case where signer is an SCA.
return concat([SignatureType.EOA, signature]);
default:
throw new Error(`Unknown version ${type} of ${String(version)}`);
}
const { type, data } = await prepareSign({
type: "personal_sign",
data: message,
});

const sig =
type === "personal_sign"
? await signer.signMessage(data)
: await signer.signTypedData(data);

return formatSign(sig);
},
async signTypedData(params) {
switch (version as string) {
case "v1.0.1":
return signer.signTypedData(
params as unknown as SignTypedDataParameters,
);
case "v1.0.2":
throw new Error(
`Version ${String(version)} of LightAccount doesn't support 1271`,
);
case "v1.1.0":
return signWith1271Wrapper(hashTypedData(params), "1");
case "v2.0.0":
const signature = await signWith1271Wrapper(
hashTypedData(params),
"2",
);
// TODO: handle case where signer is an SCA.
return concat([SignatureType.EOA, signature]);
default:
throw new Error(`Unknown version ${String(version)} of LightAccount`);
}
async signTypedData<
const typedData extends TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
>(params: TypedDataDefinition<typedData, primaryType>) {
const { type, data } = await prepareSign({
type: "eth_signTypedData_v4",
data: params as TypedDataDefinition,
});

const sig =
type === "personal_sign"
? await signer.signMessage(data)
: await signer.signTypedData(data);

return formatSign(sig);
},
getDummySignature: (): Hex => {
const signature =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe("Light Account Tests", () => {
switch (version) {
case "v1.0.2":
await expect(account.signMessage({ message })).rejects.toThrowError(
"LightAccount v1.0.2 doesn't support 1271",
"Version v1.0.2 of LightAccount doesn't support 1271",
);
break;
case "v1.0.1":
Expand Down Expand Up @@ -423,6 +423,32 @@ describe("Light Account Tests", () => {
expect(owners).toContain(ownerAddress);
}, 200000);

it.each(versions)(
"should expose prepare and format functions that work",
async (version) => {
if (version !== "v1.0.2") {
const provider = await givenConnectedProvider({ signer, version });
const message = "hello world";

const { type, data } = await provider.account.prepareSign({
type: "personal_sign",
data: message,
});

const signature = await provider.account.formatSign(
await (type === "personal_sign"
? provider.account.getSigner().signMessage(data)
: provider.account.getSigner().signTypedData(data)),
);

const fullSignature = await provider.signMessage({ message });

// We use `includes` to check against 6492, and slice to remove the 0x prefix
expect(fullSignature.includes(signature.slice(2))).toBe(true);
}
},
);

const givenConnectedProvider = ({
signer,
version = "v1.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export type CreateMAV2BaseParams<
| "signMessage"
| "signTypedData"
| "getDummySignature"
| "prepareSign"
| "formatSign"
> & {
signer: TSigner;
signerEntity?: SignerEntity;
Expand Down
Loading
Loading