Skip to content

Commit

Permalink
feat: add native signer, implement signMessage and signTypedData
Browse files Browse the repository at this point in the history
  • Loading branch information
howydev committed Dec 21, 2024
1 parent 7df9b5b commit ffed732
Show file tree
Hide file tree
Showing 12 changed files with 834 additions and 50 deletions.
16 changes: 16 additions & 0 deletions aa-sdk/core/src/errors/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,19 @@ export class InvalidNonceKeyError extends BaseError {
super(`Nonce key is ${nonceKey} but has to be less than 2**152`);
}
}

/**
* Error class denoting that the provided entity id is invalid because it's overriding the native entity id.
*/
export class EntityIdOverrideError extends BaseError {
override name = "InvalidNonceKeyError";

/**
* Initializes a new instance of the error message with a default message indicating that the nonce key is invalid.
*/
constructor() {
super(
`Installing entityId of 0 overrides the owner's entity id in the account`
);
}
}
1 change: 1 addition & 0 deletions aa-sdk/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export {
InvalidRpcUrlError,
InvalidEntityIdError,
InvalidNonceKeyError,
EntityIdOverrideError,
} from "./errors/client.js";
export {
EntryPointNotFoundError,
Expand Down
119 changes: 119 additions & 0 deletions account-kit/smart-contracts/src/ma-v2/account/nativeSMASigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { SmartAccountSigner } from "@aa-sdk/core";
import {
hashMessage,
hashTypedData,
type Hex,
type SignableMessage,
type TypedData,
type TypedDataDefinition,
type Chain,
type Address,
} from "viem";

import { packUOSignature, pack1271Signature } from "../utils.js";
/**
* Creates an object with methods for generating a dummy signature, signing user operation hashes, signing messages, and signing typed data.
*
* @example
* ```ts
* import { singleSignerMessageSigner } from "@account-kit/smart-contracts";
* import { LocalAccountSigner } from "@aa-sdk/core";
*
* const MNEMONIC = "...":
*
* const account = createSMAV2Account({ config });
*
* const signer = LocalAccountSigner.mnemonicToAccountSigner(MNEMONIC);
*
* const messageSigner = singleSignerMessageSigner(signer, chain);
* ```
*
* @param {SmartAccountSigner} signer Signer to use for signing operations
* @param {Chain} chain Chain object for the signer
* @param {Address} accountAddress address of the smart account using this signer
* @param {number} entityId the entity id of the signing validation
* @returns {object} an object with methods for signing operations and managing signatures
*/
export const nativeSMASigner = (
signer: SmartAccountSigner,
chain: Chain,
accountAddress: Address,
entityId: number
) => {
return {
getDummySignature: (): Hex => {
const dummyEcdsaSignature =
"0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";

return packUOSignature({
// orderedHookData: [],
validationSignature: dummyEcdsaSignature,
});
},

signUserOperationHash: (uoHash: Hex): Promise<Hex> => {
return signer.signMessage({ raw: uoHash }).then((signature: Hex) =>
packUOSignature({
// orderedHookData: [],
validationSignature: signature,
})
);
},

// we apply the expected 1271 packing here since the account contract will expect it
async signMessage({ message }: { message: SignableMessage }): Promise<Hex> {
const hash = hashMessage(message);

return pack1271Signature({
validationSignature: await signer.signTypedData({
domain: {
chainId: Number(chain.id),
verifyingContract: accountAddress,
},
types: {
ReplaySafeHash: [{ name: "hash", type: "bytes32" }],
},
message: {
hash,
},
primaryType: "ReplaySafeHash",
}),
entityId,
});
},

// TODO: maybe move "sign deferred actions" to a separate function?
// we don't apply the expected 1271 packing since deferred sigs use typed data sigs and don't expect the 1271 packing
signTypedData: async <
const typedData extends TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData
>(
typedDataDefinition: TypedDataDefinition<typedData, primaryType>
): Promise<Hex> => {
// the accounts domain already gives replay protection across accounts for deferred actions, so we don't need to apply another wrapping
const isDeferredAction =
typedDataDefinition?.primaryType === "DeferredAction" &&
typedDataDefinition?.domain?.verifyingContract === accountAddress;

return isDeferredAction
? signer.signTypedData(typedDataDefinition)
: pack1271Signature({
validationSignature: await signer.signTypedData({
domain: {
chainId: Number(chain.id),
verifyingContract: accountAddress,
},
types: {
ReplaySafeHash: [{ name: "hash", type: "bytes32" }],
},
message: {
hash: await hashTypedData(typedDataDefinition),
},
primaryType: "ReplaySafeHash",
}),
entityId,
});
},
};
};
148 changes: 138 additions & 10 deletions account-kit/smart-contracts/src/ma-v2/account/semiModularAccountV2.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,81 @@
import type {
EntryPointDef,
SmartAccountSigner,
AccountOp,
SmartContractAccountWithSigner,
ToSmartContractAccountParams,
} from "@aa-sdk/core";
import {
createBundlerClient,
getEntryPoint,
toSmartContractAccount,
InvalidEntityIdError,
InvalidNonceKeyError,
getAccountAddress,
} from "@aa-sdk/core";
import {
concatHex,
encodeFunctionData,
getContract,
maxUint32,
maxUint152,
zeroAddress,
type Address,
type Chain,
type Hex,
type Transport,
} from "viem";
import { accountFactoryAbi } from "../abis/accountFactoryAbi.js";
import { getDefaultMAV2FactoryAddress } from "../utils.js";
import { standardExecutor } from "../../msca/account/standardExecutor.js";
import {
getDefaultMAV2FactoryAddress,
DEFAULT_OWNER_ENTITY_ID,
} from "../utils.js";
import { singleSignerMessageSigner } from "../modules/single-signer-validation/signer.js";
import { InvalidEntityIdError, InvalidNonceKeyError } from "@aa-sdk/core";
import { nativeSMASigner } from "./nativeSMASigner.js";
import { modularAccountAbi } from "../abis/modularAccountAbi.js";
import { serializeModuleEntity } from "../actions/common/utils.js";

export const DEFAULT_OWNER_ENTITY_ID = 0;
const executeUserOpSelector: Hex = "0x8DD7712F";

export type SignerEntity = {
isGlobalValidation: boolean;
entityId: number;
};

export type ExecutionDataView = {
module: Address;
skipRuntimeValidation: boolean;
allowGlobalValidation: boolean;
executionHooks: readonly Hex[];
};

export type ValidationDataView = {
validationHooks: readonly Hex[];
executionHooks: readonly Hex[];
selectors: readonly Hex[];
validationFlags: number;
};

export type ValidationDataParams =
| {
validationModuleAddress: Address;
entityId?: never;
}
| {
validationModuleAddress?: never;
entityId: number;
};

export type SMAV2Account<
TSigner extends SmartAccountSigner = SmartAccountSigner
> = SmartContractAccountWithSigner<"SMAV2Account", TSigner, "0.7.0"> &
SignerEntity;
SignerEntity & {
getExecutionData: (selector: Hex) => Promise<ExecutionDataView>;
getValidationData: (
args: ValidationDataParams
) => Promise<ValidationDataView>;
encodeCallData: (callData: Hex) => Promise<Hex>;
};

export type CreateSMAV2AccountParams<
TTransport extends Transport = Transport,
Expand Down Expand Up @@ -80,7 +119,7 @@ export async function createSMAV2Account(
accountAddress,
entryPoint = getEntryPoint(chain, { version: "0.7.0" }),
isGlobalValidation = true,
entityId = 0,
entityId = DEFAULT_OWNER_ENTITY_ID,
} = config;

if (entityId > Number(maxUint32)) {
Expand Down Expand Up @@ -110,15 +149,53 @@ export async function createSMAV2Account(
]);
};

const encodeExecute: (tx: AccountOp) => Promise<Hex> = async ({
target,
data,
value,
}) =>
await encodeCallData(
encodeFunctionData({
abi: modularAccountAbi,
functionName: "execute",
args: [target, value ?? 0n, data],
})
);

const encodeBatchExecute: (txs: AccountOp[]) => Promise<Hex> = async (txs) =>
await encodeCallData(
encodeFunctionData({
abi: modularAccountAbi,
functionName: "executeBatch",
args: [
txs.map((tx) => ({
target: tx.target,
data: tx.data,
value: tx.value ?? 0n,
})),
],
})
);

const _accountAddress = await getAccountAddress({
client,
entryPoint,
accountAddress,
getAccountInitCode,
});

const baseAccount = await toSmartContractAccount({
transport,
chain,
entryPoint,
accountAddress,
accountAddress: _accountAddress,
source: `SMAV2Account`,
encodeExecute,
encodeBatchExecute,
getAccountInitCode,
...standardExecutor,
...singleSignerMessageSigner(signer),
...(entityId === DEFAULT_OWNER_ENTITY_ID
? nativeSMASigner(signer, chain, _accountAddress, entityId)
: singleSignerMessageSigner(signer, chain, _accountAddress, entityId)),
});

// TODO: add deferred action flag
Expand All @@ -139,16 +216,67 @@ export async function createSMAV2Account(
(isGlobalValidation ? 1n : 0n);

return entryPointContract.read.getNonce([
baseAccount.address,
_accountAddress,
fullNonceKey,
]) as Promise<bigint>;
};

const accountContract = getContract({
address: _accountAddress,
abi: modularAccountAbi,
client,
});

const getExecutionData = async (selector: Hex) => {
if (!(await baseAccount.isAccountDeployed())) {
return {
module: zeroAddress,
skipRuntimeValidation: false,
allowGlobalValidation: false,
executionHooks: [],
};
}

return await accountContract.read.getExecutionData([selector]);
};

const getValidationData = async (args: ValidationDataParams) => {
if (!(await baseAccount.isAccountDeployed())) {
return {
validationHooks: [],
executionHooks: [],
selectors: [],
validationFlags: 0,
};
}

const { validationModuleAddress, entityId } = args;
return await accountContract.read.getValidationData([
serializeModuleEntity({
moduleAddress: validationModuleAddress ?? zeroAddress,
entityId: entityId ?? Number(maxUint32),
}),
]);
};

const encodeCallData = async (callData: Hex): Promise<Hex> => {
const validationData = await getValidationData({
entityId: Number(entityId),
});

return validationData.executionHooks.length
? concatHex([executeUserOpSelector, callData])
: callData;
};

return {
...baseAccount,
getAccountNonce,
getSigner: () => signer,
isGlobalValidation,
entityId,
getExecutionData,
getValidationData,
encodeCallData,
};
}
Loading

0 comments on commit ffed732

Please sign in to comment.