From 78d29afbfc6d0909d81ae2480206701329f4b491 Mon Sep 17 00:00:00 2001 From: moldy Date: Thu, 29 Jun 2023 17:42:04 -0400 Subject: [PATCH 1/2] fix: local account signers were not correctly handling hex inputs --- .../__tests__/kernel-account.test.ts | 6 +- .../__tests__/mocks/mock-signer-validator.ts | 6 +- .../__tests__/mocks/mock-signer.ts | 4 +- .../src/__tests__/private-key-signer.test.ts | 36 ---------- .../core/src/__tests__/simple-account.test.ts | 18 ++--- packages/core/src/signer/hd-account.ts | 40 ++--------- .../core/src/signer/local-account.test.ts | 72 +++++++++++++++++++ packages/core/src/signer/local-account.ts | 44 ++++++++++++ packages/core/src/signer/private-key.ts | 40 ++--------- 9 files changed, 143 insertions(+), 123 deletions(-) delete mode 100644 packages/core/src/__tests__/private-key-signer.test.ts create mode 100644 packages/core/src/signer/local-account.test.ts create mode 100644 packages/core/src/signer/local-account.ts diff --git a/packages/accounts/src/kernel-zerodev/__tests__/kernel-account.test.ts b/packages/accounts/src/kernel-zerodev/__tests__/kernel-account.test.ts index b50117f2dd..268d1d9080 100644 --- a/packages/accounts/src/kernel-zerodev/__tests__/kernel-account.test.ts +++ b/packages/accounts/src/kernel-zerodev/__tests__/kernel-account.test.ts @@ -5,6 +5,7 @@ import { type Address, type Hex, } from "viem"; +import { generatePrivateKey } from "viem/accounts"; import { polygonMumbai } from "viem/chains"; import { KernelSmartContractAccount, @@ -17,8 +18,7 @@ import { MockSigner } from "./mocks/mock-signer"; describe("Kernel Account Tests", () => { //any wallet should work const config = { - privateKey: process.env.OWNER_KEY as Hex, - ownerWallet: process.env.OWNER_WALLET, + privateKey: generatePrivateKey(), mockWallet: "0x48D4d3536cDe7A257087206870c6B6E76e3D4ff4", chain: polygonMumbai, rpcProvider: `${polygonMumbai.rpcUrls.alchemy.http[0]}/${[ @@ -52,7 +52,7 @@ describe("Kernel Account Tests", () => { ); function connect(index: bigint, owner = mockOwner) { - return provider.connect((provider) => account(index, owner)); + return provider.connect((_provider) => account(index, owner)); } function account(index: bigint, owner = mockOwner) { diff --git a/packages/accounts/src/kernel-zerodev/__tests__/mocks/mock-signer-validator.ts b/packages/accounts/src/kernel-zerodev/__tests__/mocks/mock-signer-validator.ts index b1f038f436..5421d2b9f9 100644 --- a/packages/accounts/src/kernel-zerodev/__tests__/mocks/mock-signer-validator.ts +++ b/packages/accounts/src/kernel-zerodev/__tests__/mocks/mock-signer-validator.ts @@ -1,11 +1,11 @@ -import { Address, Hex, SmartAccountSigner } from "@alchemy/aa-core"; +import type { Address, Hex, SmartAccountSigner } from "@alchemy/aa-core"; export class MockSignerValidator implements SmartAccountSigner { getAddress(): Promise
{ return Promise.resolve("0xabcfC3DB1e0f5023F5a4f40c03D149f316E6A5cc"); } - signMessage(msg: Uint8Array | Hex | string): Promise { + async signMessage(msg: Uint8Array | Hex | string): Promise { switch (msg) { case "0xabcfC3DB1e0f5023F5a4f40c03D149f316E6A5cc": return "0x64e29e4786b3740ceffc2c1a932124ee74c29b552957ea3bde8913753d1763af4f03362e387d2badb33932e8fc4f7b3411a0a5ade32a5b708aa48c171632a6211b"; @@ -13,6 +13,8 @@ export class MockSignerValidator implements SmartAccountSigner { return "0xabd26de022c2785a7d86c5c388f4adef5d93358b39fbb757463bc9edc78b7b86566cb1ab8c7ff3a52b10d98de6398aacc7b48aec92a3e280065a47b9698209541b"; case "0xbc7299170f076afcbafe11da04482e72e3beccabcd82de0cd2797500e81b76c4": return "0x6c21c7271c8403452c5a812c9ba776b33b12733953f154d36d989d379c92ec632b7a1997ca16203a7ef5fcd639bcaa3d5420b65e0774a8bca7fad6d1437024661c"; + default: + throw new Error("Invalid message"); } } } diff --git a/packages/accounts/src/kernel-zerodev/__tests__/mocks/mock-signer.ts b/packages/accounts/src/kernel-zerodev/__tests__/mocks/mock-signer.ts index 2adb0309d8..e7d75f64dd 100644 --- a/packages/accounts/src/kernel-zerodev/__tests__/mocks/mock-signer.ts +++ b/packages/accounts/src/kernel-zerodev/__tests__/mocks/mock-signer.ts @@ -1,11 +1,11 @@ -import { Address, Hex, SmartAccountSigner } from "@alchemy/aa-core"; +import type { Address, Hex, SmartAccountSigner } from "@alchemy/aa-core"; export class MockSigner implements SmartAccountSigner { getAddress(): Promise
{ return Promise.resolve("0x48D4d3536cDe7A257087206870c6B6E76e3D4ff4"); } - signMessage(msg: Uint8Array | Hex | string): Promise { + signMessage(_msg: Uint8Array | Hex | string): Promise { return Promise.resolve( "0x4d61c5c27fb64b207cbf3bcf60d78e725659cff5f93db9a1316162117dff72aa631761619d93d4d97dfb761ba00b61f9274c6a4a76e494df644d968dd84ddcdb1c" ); diff --git a/packages/core/src/__tests__/private-key-signer.test.ts b/packages/core/src/__tests__/private-key-signer.test.ts deleted file mode 100644 index 9d70d9619e..0000000000 --- a/packages/core/src/__tests__/private-key-signer.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { PrivateKeySigner } from "../signer/private-key"; - -describe("Private Key Account Signer Tests", () => { - const dummyPrivateKey = - "0x022430a80f723d8789f0d4fb346bdd013b546e4b96fcacf8aceca2b1a65a19dc"; - const dummyAddress = "0xabcfC3DB1e0f5023F5a4f40c03D149f316E6A5cc"; - - const signer: PrivateKeySigner = - PrivateKeySigner.privateKeyToAccountSigner(dummyPrivateKey); - - it("should sign a hex message properly", async () => { - expect(await signer.signMessage("0xab3430fgk78")).toMatchInlineSnapshot( - `"0xcb2094233752c6f66fa660f2118c60d0b7b73a097004514c01100bf986f3950f5ffddcd38035774d2878b72b2fd2d8ad1be2739f1b0cd8f34352d7301fe368e61b"` - ); - - expect( - await signer.signMessage("icanbreakthistestcase") - ).toMatchInlineSnapshot( - `"0xabd26de022c2785a7d86c5c388f4adef5d93358b39fbb757463bc9edc78b7b86566cb1ab8c7ff3a52b10d98de6398aacc7b48aec92a3e280065a47b9698209541b"` - ); - - expect( - await signer.signMessage("i will definately break this test case") - ).toMatchInlineSnapshot( - `"0x907004e990bb1bca76d9fed6bf4a6b614a8d11b6430657cb99ae83fae55c9bc60571876d8c01832e4661dd4312fa08b799b6f59c9b9abddd67e181abc6aef17e1c"` - ); - - expect(await signer.signMessage(dummyAddress)).toMatchInlineSnapshot( - `"0x64e29e4786b3740ceffc2c1a932124ee74c29b552957ea3bde8913753d1763af4f03362e387d2badb33932e8fc4f7b3411a0a5ade32a5b708aa48c171632a6211b"` - ); - }); - - it("should return wallet address", async () => { - expect(await signer.getAddress()).eql(dummyAddress); - }); -}); diff --git a/packages/core/src/__tests__/simple-account.test.ts b/packages/core/src/__tests__/simple-account.test.ts index 8e6261d0fe..83b22395f1 100644 --- a/packages/core/src/__tests__/simple-account.test.ts +++ b/packages/core/src/__tests__/simple-account.test.ts @@ -1,12 +1,12 @@ -import { isAddress, toHex } from "viem"; -import { generatePrivateKey, mnemonicToAccount } from "viem/accounts"; +import { isAddress } from "viem"; +import { generatePrivateKey } from "viem/accounts"; import { polygonMumbai } from "viem/chains"; import { SimpleSmartContractAccount, type SimpleSmartAccountOwner, } from "../account/simple.js"; import { SmartAccountProvider } from "../provider/base.js"; -import { PrivateKeySigner } from "../signer/private-key.js"; +import { LocalAccountSigner } from "../signer/local-account.js"; import type { BatchUserOperationCallData } from "../types.js"; const ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; @@ -16,14 +16,8 @@ const SIMPLE_ACCOUNT_FACTORY_ADDRESS = "0x9406Cc6185a346906296840746125a0E44976454"; describe("Simple Account Tests", () => { - const ownerAccount = mnemonicToAccount(OWNER_MNEMONIC); - const owner: SimpleSmartAccountOwner = { - signMessage: async (msg) => - ownerAccount.signMessage({ - message: { raw: toHex(msg) }, - }), - getAddress: async () => ownerAccount.address, - }; + const owner: SimpleSmartAccountOwner = + LocalAccountSigner.mnemonicToAccountSigner(OWNER_MNEMONIC); const chain = polygonMumbai; const signer = new SmartAccountProvider( `${chain.rpcUrls.alchemy.http[0]}/${API_KEY}`, @@ -111,7 +105,7 @@ describe("Simple Account Tests", () => { }); it("should get counterfactual for undeployed account", async () => { - const owner = PrivateKeySigner.privateKeyToAccountSigner( + const owner = LocalAccountSigner.privateKeyToAccountSigner( generatePrivateKey() ); const provider = new SmartAccountProvider( diff --git a/packages/core/src/signer/hd-account.ts b/packages/core/src/signer/hd-account.ts index a55c1cba10..1455785dfe 100644 --- a/packages/core/src/signer/hd-account.ts +++ b/packages/core/src/signer/hd-account.ts @@ -1,35 +1,7 @@ -import type { Address } from "abitype"; -import type { HDAccount, Hex } from "viem"; -import { mnemonicToAccount } from "viem/accounts"; -import type { SmartAccountSigner } from "./types"; +import type { HDAccount } from "viem"; +import { LocalAccountSigner } from "./local-account"; -export class HdAccountSigner implements SmartAccountSigner { - owner: HDAccount; - - constructor(owner: HDAccount) { - this.owner = owner; - } - - signMessage(msg: Uint8Array | Hex | string): Promise { - if (typeof msg === "string") { - return this.owner.signMessage({ - message: msg, - }); - } else { - return this.owner.signMessage({ - message: { - raw: msg, - }, - }); - } - } - - getAddress(): Promise
{ - return Promise.resolve(this.owner.address); - } - - static mnemonicToAccountSigner(key: Hex): HdAccountSigner { - const owner = mnemonicToAccount(key); - return new HdAccountSigner(owner); - } -} +/** + * @deprecated use LocalAccountSigner instead + */ +export class HdAccountSigner extends LocalAccountSigner {} diff --git a/packages/core/src/signer/local-account.test.ts b/packages/core/src/signer/local-account.test.ts new file mode 100644 index 0000000000..625a58a7e9 --- /dev/null +++ b/packages/core/src/signer/local-account.test.ts @@ -0,0 +1,72 @@ +import { LocalAccountSigner } from "./local-account"; + +describe("Local Account Signer Tests", () => { + describe("Using HD Account", () => { + const dummyMnemonic = + "test test test test test test test test test test test test"; + const signer = LocalAccountSigner.mnemonicToAccountSigner(dummyMnemonic); + + it("should sign a hex message properly", async () => { + expect( + await signer.signMessage("0xabcfC3DB1e0f5023F5a4f40c03D149f316E6A5cc") + ).toMatchInlineSnapshot( + '"0x35761512143ffd8da07c93c5a0136424fe935b48e77076f501a57745c16268bf0d9a5d6209b12d5f8b62f96f0991372e046092fd6b1e3bfa610eb51607a28f7e1b"' + ); + }); + + it("should sign a string message properly", async () => { + expect( + await signer.signMessage("icanbreakthistestcase") + ).toMatchInlineSnapshot( + '"0x6b2efa82d72558efad294b727741e1eb9ff4800e2d66ce5f0373454ebdd13573630985612c1d618f49f173f65cf83fc131f798cb65ae66f90f8f0b9d4fdbca881b"' + ); + + expect( + await signer.signMessage("i will definately break this test case") + ).toMatchInlineSnapshot( + '"0x1d9a20cfd63c179daab241d9e64a2549e1a47c1ca7c465df6f5b8c5720cee7266050b89b3874b384c88cd8325601572e79b3b626dc756a6aec5fff05d4ce5efc1b"' + ); + }); + + it("should sign a byte array correctly", async () => { + expect( + await signer.signMessage(new TextEncoder().encode("hello, I'm moldy")) + ).toEqual(await signer.signMessage("hello, I'm moldy")); + }); + }); + + describe("Using Private Key Account", () => { + const dummyPrivateKey = + "0x022430a80f723d8789f0d4fb346bdd013b546e4b96fcacf8aceca2b1a65a19dc"; + const signer = + LocalAccountSigner.privateKeyToAccountSigner(dummyPrivateKey); + + it("should sign a hex message properly", async () => { + expect( + await signer.signMessage("0xabcfC3DB1e0f5023F5a4f40c03D149f316E6A5cc") + ).toMatchInlineSnapshot( + '"0x91b6680c8f442f46ca71fee15cdd8c9e25693baeb4006d1908a453fd145315ce21a5e7f2ce9760fc993d65e8450fa5225d8dee12972886bdacbb989ca0b09c6c1b"' + ); + }); + + it("should sign a string message properly", async () => { + expect( + await signer.signMessage("icanbreakthistestcase") + ).toMatchInlineSnapshot( + '"0xabd26de022c2785a7d86c5c388f4adef5d93358b39fbb757463bc9edc78b7b86566cb1ab8c7ff3a52b10d98de6398aacc7b48aec92a3e280065a47b9698209541b"' + ); + + expect( + await signer.signMessage("i will definately break this test case") + ).toMatchInlineSnapshot( + '"0x907004e990bb1bca76d9fed6bf4a6b614a8d11b6430657cb99ae83fae55c9bc60571876d8c01832e4661dd4312fa08b799b6f59c9b9abddd67e181abc6aef17e1c"' + ); + }); + + it("should sign a byte array correctly", async () => { + expect( + await signer.signMessage(new TextEncoder().encode("hello, I'm moldy")) + ).toEqual(await signer.signMessage("hello, I'm moldy")); + }); + }); +}); diff --git a/packages/core/src/signer/local-account.ts b/packages/core/src/signer/local-account.ts new file mode 100644 index 0000000000..5cb46c44a0 --- /dev/null +++ b/packages/core/src/signer/local-account.ts @@ -0,0 +1,44 @@ +import { isHex, type HDAccount, type Hex, type PrivateKeyAccount } from "viem"; +import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts"; +import type { SmartAccountSigner } from "./types.js"; + +export class LocalAccountSigner + implements SmartAccountSigner +{ + private owner: T; + constructor(owner: T) { + this.owner = owner; + } + + readonly signMessage: (msg: string | Uint8Array) => Promise<`0x${string}`> = ( + msg + ) => { + if (typeof msg === "string" && !isHex(msg)) { + return this.owner.signMessage({ + message: msg, + }); + } else { + return this.owner.signMessage({ + message: { + raw: msg, + }, + }); + } + }; + + readonly getAddress: () => Promise<`0x${string}`> = async () => { + return this.owner.address; + }; + + static mnemonicToAccountSigner(key: string): LocalAccountSigner { + const owner = mnemonicToAccount(key); + return new LocalAccountSigner(owner); + } + + static privateKeyToAccountSigner( + key: Hex + ): LocalAccountSigner { + const owner = privateKeyToAccount(key); + return new LocalAccountSigner(owner); + } +} diff --git a/packages/core/src/signer/private-key.ts b/packages/core/src/signer/private-key.ts index 2de3401574..4dd24065c5 100644 --- a/packages/core/src/signer/private-key.ts +++ b/packages/core/src/signer/private-key.ts @@ -1,35 +1,7 @@ -import type { Address } from "abitype"; -import type { Hex, PrivateKeyAccount } from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import type { SmartAccountSigner } from "./types"; +import { type PrivateKeyAccount } from "viem"; +import { LocalAccountSigner } from "./local-account"; -export class PrivateKeySigner implements SmartAccountSigner { - owner: PrivateKeyAccount; - - constructor(owner: PrivateKeyAccount) { - this.owner = owner; - } - - signMessage(msg: Uint8Array | Hex | string): Promise { - if (typeof msg === "string") { - return this.owner.signMessage({ - message: msg, - }); - } else { - return this.owner.signMessage({ - message: { - raw: msg, - }, - }); - } - } - - getAddress(): Promise
{ - return Promise.resolve(this.owner.address); - } - - static privateKeyToAccountSigner(key: Hex): PrivateKeySigner { - const owner = privateKeyToAccount(key); - return new PrivateKeySigner(owner); - } -} +/** + * @deprecated use LocalAccountSigner instead + */ +export class PrivateKeySigner extends LocalAccountSigner {} From 69cfa70959f4ba01dcac4d31ea1e264297746c41 Mon Sep 17 00:00:00 2001 From: moldy Date: Fri, 30 Jun 2023 11:19:56 -0400 Subject: [PATCH 2/2] docs: update the readme to reflect the latest changes --- README.md | 32 ++++++++++---------------------- packages/core/src/index.ts | 1 + 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 6fd8be2211..3dadc0838b 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ import { SimpleSmartContractAccount, SmartAccountProvider, type SimpleSmartAccountOwner, + LocalAccountSigner, } from "@alchemy/aa-core"; import { mnemonicToAccount } from "viem/accounts"; import { polygonMumbai } from "viem/chains"; @@ -64,17 +65,10 @@ const SIMPLE_ACCOUNT_FACTORY_ADDRESS = "0x9406Cc6185a346906296840746125a0E44976454"; // 1. define the EOA owner of the Smart Account -// This is just one exapmle of how to interact with EOAs, feel free to use any other interface -const ownerAccount = mnemonicToAccount(MNEMONIC); -// All that is important for defining an owner is that it provides a `signMessage` and `getAddress` function -const owner: SimpleSmartAccountOwner = { - // this should sign a message according to ERC-191 - signMessage: async (msg) => - ownerAccount.signMessage({ - message: toHex(msg), - }), - getAddress: async () => ownerAccount.address, -}; +// this uses a utility method for creating an account signer using mnemonic +// we also have a utility for creating an account signer from a private key +const owner: SimpleSmartAccountOwner = + LocalAccountSigner.mnemonicToAccountSigner(MNEMONIC); // 2. initialize the provider and connect it to the account const provider = new SmartAccountProvider( @@ -109,6 +103,7 @@ const { hash } = provider.sendUserOperation({ import { SimpleSmartContractAccount, type SimpleSmartAccountOwner, + LocalAccountSigner, } from "@alchemy/aa-core"; import { toHex } from "viem"; import { mnemonicToAccount } from "viem/accounts"; @@ -119,17 +114,10 @@ const SIMPLE_ACCOUNT_FACTORY_ADDRESS = "0x9406Cc6185a346906296840746125a0E44976454"; // 1. define the EOA owner of the Smart Account -// This is just one exapmle of how to interact with EOAs, feel free to use any other interface -const ownerAccount = mnemonicToAccount(MNEMONIC); -// All that is important for defining an owner is that it provides a `signMessage` and `getAddress` function -const owner: SimpleSmartAccountOwner = { - // this should sign a message according to ERC-191 - signMessage: async (msg) => - ownerAccount.signMessage({ - message: toHex(msg), - }), - getAddress: async () => ownerAccount.address, -}; +// this uses a utility method for creating an account signer using mnemonic +// we also have a utility for creating an account signer from a private key +const owner: SimpleSmartAccountOwner = + LocalAccountSigner.mnemonicToAccountSigner(MNEMONIC); // 2. initialize the provider and connect it to the account let provider = new AlchemyProvider({ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 207f76f1ff..230554ffc1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -15,6 +15,7 @@ export type { } from "./account/simple.js"; export type * from "./account/types.js"; export { HdAccountSigner } from "./signer/hd-account.js"; +export { LocalAccountSigner } from "./signer/local-account.js"; export { PrivateKeySigner } from "./signer/private-key.js"; export type { SmartAccountSigner } from "./signer/types.js";