Skip to content

Commit

Permalink
feat: kernel batch transactions and gas estimation fixes (#39)
Browse files Browse the repository at this point in the history
* feat: `ISmartAccountProvider` accepts `BaseSmartContractAccount` as generic

* fix: fixed type export for `SmartAccountSinger`

* feat: added signMessage method in `SmartAccountProvider`

* feat: fixed lint errors

* feat: exported `alchemyPaymasterAndDataMiddleware` in alchemy package

* feat: fixed core package build issues

* feat: fixed estimation mismatch and implemented signMessage method for `KernelSmartAccountProvider`

* feat: fixed estimation mismatch and implemented signMessage method for `KernelSmartAccountProvider`

* feat: added support for batch transfers

* feat: added support for batch transfers

* feat: added support for batch transfers

* fix: lint errors fixes

* fix: lint errors fixes

* fix: minor code enhancements

* fix: few cleanup items and nits

* fix: few cleanup items and nits

* fix: few cleanup items and nits

* fix: few cleanup items and nits
  • Loading branch information
whatmanathinks authored Jul 5, 2023
1 parent 383fd27 commit f2a3d3d
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 84 deletions.
4 changes: 4 additions & 0 deletions packages/accounts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export {
KernelBaseValidator,
} from "./kernel-zerodev/validator/base";
export type { KernelBaseValidatorParams } from "./kernel-zerodev/validator/base";
export type {
KernelBatchUserOperationCallData,
KernelUserOperationCallData,
} from "./kernel-zerodev/types";
export type { KernelSmartAccountParams } from "./kernel-zerodev/account";
export { KernelSmartContractAccount } from "./kernel-zerodev/account";
export { KernelAccountProvider } from "./kernel-zerodev/provider";
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { PrivateKeySigner } from "@alchemy/aa-core";
import {
type BatchUserOperationCallData,
PrivateKeySigner,
} from "@alchemy/aa-core";
import {
encodeAbiParameters,
parseAbiParameters,
Expand All @@ -14,16 +17,15 @@ import {
import { KernelAccountProvider } from "../provider";
import { KernelBaseValidator, ValidatorMode } from "../validator/base";
import { MockSigner } from "./mocks/mock-signer";
import type { KernelUserOperationCallData } from "../types";

describe("Kernel Account Tests", () => {
//any wallet should work
const config = {
privateKey: generatePrivateKey(),
mockWallet: "0x48D4d3536cDe7A257087206870c6B6E76e3D4ff4",
chain: polygonMumbai,
rpcProvider: `${polygonMumbai.rpcUrls.alchemy.http[0]}/${[
process.env.API_KEY,
]}`,
rpcProvider: `${polygonMumbai.rpcUrls.alchemy.http[0]}/demo`,
validatorAddress: "0x180D6465F921C7E0DEA0040107D342c87455fFF5" as Address,
accountFactoryAddress:
"0x5D006d3880645ec6e254E18C1F879DAC9Dd71A39" as Address,
Expand Down Expand Up @@ -167,53 +169,57 @@ describe("Kernel Account Tests", () => {
);
});

//NOTE - this test case will only work if your alchemy endpoint has beta access

// it("sendUserOperation should fail to execute if gas fee not present", async () => {
// let signerWithProvider = connect(1000n)
//
//
// const result = signerWithProvider.sendUserOperation({
// target: await signerWithProvider.getAddress(),
// data: "0x",
// });
//
// await expect(result).rejects.toThrowError(/sender balance and deposit together is 0/);
// });

//NOTE - this test case will only work if your alchemy endpoint has beta access
// and you have deposited some matic balance for counterfactual address at entrypoint

// it("sendUserOperation should execute properly", async () => {
// //
// let signerWithProvider = connect(0n,owner)
//
// //to fix bug in old versions
// await signerWithProvider.account.getInitCode()
// const result = signerWithProvider.sendUserOperation({
// target: await signerWithProvider.getAddress(),
// data: "0x",
// value: 0n
// });
// await expect(result).resolves.not.toThrowError();
// });
it("sendUserOperation should fail to execute if gas fee not present", async () => {
let signerWithProvider = connect(1000n);

const result = signerWithProvider.sendUserOperation({
target: await signerWithProvider.getAddress(),
data: "0x",
});

await expect(result).rejects.toThrowError(
/sender balance and deposit together is 0/
);
});

// Only work if you have deposited some matic balance for counterfactual address at entrypoint
it("sendUserOperation should execute properly", async () => {
//
let signerWithProvider = connect(0n, owner);

const result = signerWithProvider.sendUserOperation({
target: await signerWithProvider.getAddress(),
data: "0x",
value: 0n,
});
await expect(result).resolves.not.toThrowError();
}, 10000);

it("sendUserOperation batch should execute properly", async () => {
let signerWithProvider = connect(0n, owner);
const request: KernelUserOperationCallData = {
target: await signerWithProvider.getAddress(),
data: "0x",
value: 100000000n,
};
const request2: KernelUserOperationCallData = {
target: await signerWithProvider.getAddress(),
data: "0x",
value: 200000000n,
};
const requests: BatchUserOperationCallData = [request, request2];
const result = signerWithProvider.sendUserOperation(requests);
await expect(result).resolves.not.toThrowError();
}, 20000);

//non core functions
it("should correctly identify whether account is deployed", async () => {
//contract already deployed
const signer = account(0n);
expect(await signer.isAccountDeployed()).eql(true);

//contract already deployed
const signer2 = account(3n);
expect(await signer2.isAccountDeployed()).eql(true);

//contract not deployed
const signer3 = account(4n);
expect(await signer3.isAccountDeployed()).eql(false);

//contract not deployed
const signer4 = account(5n);
expect(await signer4.isAccountDeployed()).eql(false);
});
});
14 changes: 14 additions & 0 deletions packages/accounts/src/kernel-zerodev/abis/MultiSendAbi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const MultiSendAbi = [
{
inputs: [],
stateMutability: "nonpayable",
type: "constructor",
},
{
inputs: [{ internalType: "bytes", name: "transactions", type: "bytes" }],
name: "multiSend",
outputs: [],
stateMutability: "payable",
type: "function",
},
] as const;
18 changes: 17 additions & 1 deletion packages/accounts/src/kernel-zerodev/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
BaseSmartContractAccount,
type SmartAccountSigner,
} from "@alchemy/aa-core";
import type { BatchUserOperationCallData } from "@alchemy/aa-core/src";
import { MultiSendAbi } from "./abis/MultiSendAbi";
import { encodeCall } from "./utils";

export interface KernelSmartAccountParams<
TTransport extends Transport | FallbackTransport = Transport
Expand Down Expand Up @@ -48,7 +51,7 @@ export class KernelSmartContractAccount<
}

getDummySignature(): Hex {
return "0x4046ab7d9c387d7a5ef5ca0777eded29767fd9863048946d35b3042d2f7458ff7c62ade2903503e15973a63a296313eab15b964a18d79f4b06c8c01c7028143c1c";
return "0x00000000b650d28e51cf39d5c0bb7db6d81cce5f0a77baba8bf8de587c0bc83fa70e374f3bfef2afb697dc5627c669de7dc13e96c85697e0f6aae2f2ebe227552d00cb181c";
}

async encodeExecute(target: Hex, value: bigint, data: Hex): Promise<Hex> {
Expand All @@ -67,6 +70,19 @@ export class KernelSmartContractAccount<
return this.encodeExecuteAction(target, value, data, 1);
}

override async encodeBatchExecute(
_txs: BatchUserOperationCallData
): Promise<`0x${string}`> {
const multiSendData: `0x${string}` = concatHex(
_txs.map((tx) => encodeCall(tx))
);
return encodeFunctionData({
abi: MultiSendAbi,
functionName: "multiSend",
args: [multiSendData],
});
}

async signWithEip6492(msg: string | Uint8Array): Promise<Hex> {
try {
const formattedMessage = typeof msg === "string" ? toBytes(msg) : msg;
Expand Down
48 changes: 7 additions & 41 deletions packages/accounts/src/kernel-zerodev/provider.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,12 @@
import type { HttpTransport } from "viem";
import {
type AccountMiddlewareFn,
deepHexlify,
resolveProperties,
SmartAccountProvider,
} from "@alchemy/aa-core";
import type { Hash, HttpTransport } from "viem";
import { SmartAccountProvider } from "@alchemy/aa-core";
import { KernelSmartContractAccount } from "./account";

export class KernelAccountProvider extends SmartAccountProvider<HttpTransport> {
gasEstimator: AccountMiddlewareFn = async (struct) => {
const request = deepHexlify(await resolveProperties(struct));
const estimates = await this.rpcClient.estimateUserOperationGas(
request,
this.entryPointAddress
);

estimates.verificationGasLimit =
(BigInt(estimates.verificationGasLimit) * 130n) / 100n;

return {
...struct,
...estimates,
};
};

request: (args: { method: string; params?: any[] }) => Promise<any> = async (
args
) => {
const { method, params } = args;
if (method === "personal_sign") {
if (!this.account) {
throw new Error("account not connected!");
}
const [data, address] = params!;
if (address !== (await this.getAddress())) {
throw new Error(
"cannot sign for address that is not the current account"
);
}
// @ts-ignore
return this.account.signWithEip6492(data);
} else {
return super.request(args);
signMessage = async (msg: string | Uint8Array): Promise<Hash> => {
if (!this.account) {
throw new Error("account not connected!");
}
return (this.account as KernelSmartContractAccount).signWithEip6492(msg);
};
}
7 changes: 7 additions & 0 deletions packages/accounts/src/kernel-zerodev/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { UserOperationCallData } from "@alchemy/aa-core";

export interface KernelUserOperationCallData extends UserOperationCallData {
delegateCall?: boolean;
}

export type KernelBatchUserOperationCallData = KernelUserOperationCallData[];
17 changes: 17 additions & 0 deletions packages/accounts/src/kernel-zerodev/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { encodePacked, toBytes } from "viem";
import type { Hex } from "viem";
import type { KernelUserOperationCallData } from "./types";

export const encodeCall = (call: KernelUserOperationCallData): Hex => {
const data = toBytes(call.data);
return encodePacked(
["uint8", "address", "uint256", "uint256", "bytes"],
[
call.delegateCall ? 1 : 0,
call.target,
call.value ?? BigInt(0),
BigInt(data.length),
call.data,
]
);
};

0 comments on commit f2a3d3d

Please sign in to comment.