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 20, 2024
1 parent df3501a commit 4b79e47
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 26 deletions.
123 changes: 123 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,123 @@
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 {TSigner} 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 = <TSigner extends SmartAccountSigner>(
signer: TSigner,
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<`0x${string}`> {
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,
});
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
toSmartContractAccount,
InvalidEntityIdError,
InvalidNonceKeyError,
getAccountAddress,
} from "@aa-sdk/core";
import {
concatHex,
Expand All @@ -30,6 +31,7 @@ import {
DEFAULT_OWNER_ENTITY_ID,
} from "../utils.js";
import { singleSignerMessageSigner } from "../modules/single-signer-validation/signer.js";
import { nativeSMASigner } from "./nativeSMASigner.js";
import { modularAccountAbi } from "../abis/modularAccountAbi.js";
import { serializeModuleEntity } from "../actions/common/utils.js";

Expand Down Expand Up @@ -175,16 +177,25 @@ export async function createSMAV2Account(
})
);

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

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

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

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

const accountContract = getContract({
address: baseAccount.address,
address: _accountAddress,
abi: modularAccountAbi,
client,
});
Expand Down
71 changes: 69 additions & 2 deletions account-kit/smart-contracts/src/ma-v2/client/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { custom, parseEther, publicActions } from "viem";
import {
custom,
parseEther,
publicActions,
getContract,
hashMessage,
} from "viem";
import { LocalAccountSigner, type SmartAccountSigner } from "@aa-sdk/core";
import { createSMAV2AccountClient, type SMAV2AccountClient } from "./client.js";
import { createSMAV2AccountClient } from "./client.js";
import { local070Instance } from "~test/instances.js";
import { setBalance } from "viem/actions";
import { accounts } from "~test/constants.js";
import { getDefaultSingleSignerValidationModuleAddress } from "../modules/utils.js";
import { SingleSignerValidationModule } from "../modules/single-signer-validation/module.js";
import { installValidationActions } from "../actions/install-validation/installValidation.js";
import { semiModularAccountBytecodeAbi } from "../abis/semiModularAccountBytecodeAbi.js";

describe("MA v2 Tests", async () => {
const instance = local070Instance;
let client: ReturnType<typeof instance.getClient> &
ReturnType<typeof publicActions>;

const isValidSigSuccess = "0x1626ba7e";

beforeAll(async () => {
client = instance.getClient().extend(publicActions);
});
Expand Down Expand Up @@ -182,6 +191,64 @@ describe("MA v2 Tests", async () => {
).rejects.toThrowError();
});

it("successfully sign + validate a message, for native and single signer validation", async () => {
const provider = (await givenConnectedProvider({ signer })).extend(
installValidationActions
);

const accountContract = getContract({
address: provider.getAddress(),
abi: semiModularAccountBytecodeAbi,
client,
});

// UO deploys the account to test 1271 against
const result = await provider.installValidation({
validationConfig: {
moduleAddress: getDefaultSingleSignerValidationModuleAddress(
provider.chain
),
entityId: 1,
isGlobal: true,
isSignatureValidation: true,
isUserOpValidation: true,
},
selectors: [],
installData: SingleSignerValidationModule.encodeOnInstallData({
entityId: 1,
signer: await sessionKey.getAddress(),
}),
hooks: [],
});

let txnHash = provider.waitForUserOperationTransaction(result);
await expect(txnHash).resolves.not.toThrowError();

const message = "testmessage";

let signature = await provider.signMessage({ message });

await expect(
accountContract.read.isValidSignature([hashMessage(message), signature])
).resolves.toEqual(isValidSigSuccess);

// connect session key and send tx with session key
let sessionKeyClient = await createSMAV2AccountClient({
chain: instance.chain,
signer: sessionKey,
transport: custom(instance.getClient()),
accountAddress: provider.getAddress(),
entityId: 1,
isGlobalValidation: true,
});

signature = await sessionKeyClient.signMessage({ message });

await expect(
accountContract.read.isValidSignature([hashMessage(message), signature])
).resolves.toEqual(isValidSigSuccess);
});

const givenConnectedProvider = async ({
signer,
accountAddress,
Expand Down
Loading

0 comments on commit 4b79e47

Please sign in to comment.