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 55bb274 commit 25c6795
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 25 deletions.
111 changes: 111 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,111 @@
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
) => {
const apply712MessageWrap = async (digest: Hex): Promise<Hex> => {
return hashTypedData({
domain: {
chainId: Number(chain.id),
verifyingContract: accountAddress,
},
types: {
ReplaySafeHash: [{ name: "digest", type: "bytes32" }],
},
message: {
digest,
},
primaryType: "ReplaySafeHash",
});
};

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 digest = await apply712MessageWrap(hashMessage(message));

return pack1271Signature({
validationSignature: await signer.signMessage({ raw: digest }),
entityId,
});
},

// 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;
const digest = isDeferredAction
? hashTypedData(typedDataDefinition)
: await apply712MessageWrap(hashTypedData(typedDataDefinition));
return signer.signMessage({ raw: digest });
},
};
};
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
58 changes: 57 additions & 1 deletion account-kit/smart-contracts/src/ma-v2/client/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { custom, parseEther, publicActions } from "viem";
import {
custom,
parseEther,
publicActions,
getContract,
keccak256,
toHex,
} from "viem";
import { LocalAccountSigner, type SmartAccountSigner } from "@aa-sdk/core";
import { createSMAV2AccountClient, type SMAV2AccountClient } from "./client.js";

Check warning on line 10 in account-kit/smart-contracts/src/ma-v2/client/client.test.ts

View workflow job for this annotation

GitHub Actions / Lint

'SMAV2AccountClient' is defined but never used
import { local070Instance } from "~test/instances.js";
Expand All @@ -7,6 +14,7 @@ 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;
Expand Down Expand Up @@ -182,6 +190,54 @@ describe("MA v2 Tests", async () => {
).rejects.toThrowError();
});

it("successfully sign + validate a message for native signer", 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
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: [],
});

const message = keccak256(toHex("testmessage"));

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

console.log(await provider.account.isAccountDeployed());

console.log(
await accountContract.read.isValidSignature([message, signature])

Check failure on line 230 in account-kit/smart-contracts/src/ma-v2/client/client.test.ts

View workflow job for this annotation

GitHub Actions / Build and Test

src/ma-v2/client/client.test.ts > MA v2 Tests > successfully sign + validate a message for native signer

ContractFunctionExecutionError: The contract function "isValidSignature" reverted with the following reason: Attempted to convert to an invalid type. Contract Call: address: 0x65b5DE0251f553B2208762a4724303E750294D96 function: isValidSignature(bytes32 hash, bytes signature) args: (0x1c37b8e52b87629910dcc67dc45e49972920b01bf8828ac1410cf66b80471ce4, 0x0000000000FF3317c1406ca1a4edabfc1607c805ade9b7faf27a80077eb14e7b0aa1fbcd5ab361ae0170d86a2fe4e0f4a2832def9240bf9f93af02e6b630f6b67bbda4f790501c) Docs: https://viem.sh/docs/contract/readContract Version: 2.20.0 ❯ getContractError ../../node_modules/viem/utils/errors/getContractError.ts:72:10 ❯ readContract ../../node_modules/viem/actions/public/readContract.ts:136:11 ❯ src/ma-v2/client/client.test.ts:230:7 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { details: undefined, docsPath: '/docs/contract/readContract', metaMessages: [ 'Contract Call:', ' address: 0x65b5DE0251f553B2208762a4724303E750294D96\n function: isValidSignature(bytes32 hash, bytes signature)\n args: (0x1c37b8e52b87629910dcc67dc45e49972920b01bf8828ac1410cf66b80471ce4, 0x0000000000FF3317c1406ca1a4edabfc1607c805ade9b7faf27a80077eb14e7b0aa1fbcd5ab361ae0170d86a2fe4e0f4a2832def9240bf9f93af02e6b630f6b67bbda4f790501c)' ], shortMessage: 'The contract function "isValidSignature" reverted with the following reason:\nAttempted to convert to an invalid type.', version: '2.20.0', abi: [ { type: 'constructor', inputs: [ { name: 'entryPoint', type: 'address', internalType: 'contract IEntryPoint' }, { name: 'executionInstallDelegate', type: 'address', internalType: 'contract ExecutionInstallDelegate' } ], stateMutability: 'nonpayable' }, { type: 'fallback', stateMutability: 'payable' }, { type: 'receive', stateMutability: 'payable' }, { type: 'function', name: 'accountId', inputs: [], outputs: [ { name: '', type: 'string', internalType: 'string' } ], stateMutability: 'pure' }, { type: 'function', name: 'entryPoint', inputs: [], outputs: [ { name: '', type: 'address', internalType: 'contract IEntryPoint' } ], stateMutability: 'view' }, { type: 'function', name: 'execute', inputs: [ { name: 'target', type: 'address', internalType: 'address' }, { name: 'value', type: 'uint256', internalType: 'uint256' }, { name: 'data', type: 'bytes', internalType: 'bytes' } ], outputs: [ { name: 'result', type: 'bytes', internalType: 'bytes' } ], stateMutability: 'payable' }, { type: 'function', name: 'executeBatch', inputs: [ { name: 'calls', type: 'tuple[]', internalType: 'struct Call[]', components: [ { name: 'target', type: 'address', internalType: 'address' }, { name: 'value', type: 'uint256', internalType: 'uint256' }, { name: 'data', type: 'bytes', internalType: 'bytes' } ] } ], outputs: [ { name: 'results', type: 'bytes[]', internalType: 'bytes[]' } ], stateMutability: 'payable' }, { type: 'function', name: 'executeUserOp', inputs: [ { name: 'userOp', type: 'tuple', internalType: 'struct PackedUserOperation', components: [ { name: 'sender', type: 'address', internalType: 'address' }, { name: 'nonce', type: 'uint256', internalType: 'uint256' }, { name: 'initCode', type: 'bytes', internalType: 'bytes' }, { name: 'callData', type: 'bytes', internalType: 'bytes' }, { name: 'accountGasLimits', type: 'bytes32', internalType: 'bytes32' }, { name: 'preVerificationGas', type: 'uint256', internalType: 'uint256' }, { name: 'gasFees', type: 'bytes32', internalType: 'bytes32' }, { name: 'paymasterAndData', type: 'bytes', internalType: 'bytes' }, { name: 'signature', type: 'bytes', internalType: 'bytes' } ] }, { name: '', type: 'bytes32', internalType: 'bytes32' } ], outputs: [], stateMutability: 'nonpayable' }, { type: 'function', name: 'executeWithRuntimeValidation', inputs: [ { name: 'data', type: 'bytes', internalType: 'bytes' }, { name: 'authorization', type: 'bytes', internalType: 'bytes' } ], outputs: [ { name: '', type: 'bytes', internalType: 'bytes' } ], stateMutability: 'payable' }, { type: 'function', name: 'getExecutionData', inputs: [ { name: 'selector', type: 'bytes4', inte
);

// await expect(
// accountContract.read.isValidSignature({
// message,
// signature,
// })
// ).resolves.toBeTruthy();
});

const givenConnectedProvider = async ({
signer,
accountAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,69 +6,105 @@ import {
type SignableMessage,
type TypedData,
type TypedDataDefinition,
type Chain,
type Address,
} from "viem";
import { getDefaultSingleSignerValidationModuleAddress } from "../utils.js";

import { packSignature } from "../../utils.js";
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
* @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 the signer to use for signing operations
* @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 singleSignerMessageSigner = <TSigner extends SmartAccountSigner>(
signer: TSigner
signer: TSigner,
chain: Chain,
accountAddress: Address,
entityId: number
) => {
const apply712MessageWrap = async (digest: Hex): Promise<Hex> => {
return hashTypedData({
domain: {
chainId: Number(chain.id),
salt: accountAddress,
verifyingContract: getDefaultSingleSignerValidationModuleAddress(chain),
},
types: {
ReplaySafeHash: [{ name: "digest", type: "bytes32" }],
},
message: {
digest,
},
primaryType: "ReplaySafeHash",
});
};

return {
getDummySignature: (): Hex => {
const dummyEcdsaSignature =
"0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";

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

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

// TODO: we can't implement these methods yet, because the RI at `alpha.0` doesn't have a wrapping type,
// and viem doesn't support raw signing, only via EIP-191 or EIP-712.
// When we do implement this, we need to prefix the data with the validation module address & entityId.

signMessage({
// we apply the expected 1271 packing here since the account contract will expect it
async signMessage({
message,
}: {
message: SignableMessage;
}): Promise<`0x${string}`> {
return signer.signMessage({ raw: hashMessage(message) });
const digest = await apply712MessageWrap(hashMessage(message));

return pack1271Signature({
validationSignature: await signer.signMessage({ raw: digest }),
entityId,
});
},

signTypedData: <
// 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> => {
return signer.signMessage({ raw: hashTypedData(typedDataDefinition) });
const digest = await apply712MessageWrap(
hashTypedData(typedDataDefinition)
);

return signer.signMessage({ raw: digest });
},
};
};
Loading

0 comments on commit 25c6795

Please sign in to comment.