Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-site
Submodule docs-site updated 689 files
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,68 @@ describe("Light Account Tests", () => {
);

it.each(versions)(
"should correctly encode batch transaction data",
"should correctly encode and decode a single call transaction",
async (version) => {
const provider = await givenConnectedProvider({ signerAccount, version });
const data = [
{
to: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
data: "0xdeadbeef",
},
] satisfies Call[];

const encoded = await provider.account.encodeCalls(data);

expect(encoded).toBe(
"0xb61d27f6000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000",
);

expect(provider.account.decodeCalls).toBeDefined();

const decoded = await provider.account.decodeCalls!(encoded);

expect(decoded.length).toEqual(data.length);
expect(decoded[0].to.toLowerCase()).toEqual(data[0].to.toLowerCase());
expect(decoded[0].value).toBe(0n);
expect(decoded[0].data?.toLowerCase()).toEqual(
data[0].data?.toLowerCase(),
);
},
);

it.each(versions)(
"should correctly encode and decode a single call transaction with value",
async (version) => {
const provider = await givenConnectedProvider({ signerAccount, version });
const data = [
{
to: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
data: "0xdeadbeef",
value: parseEther("1"),
},
] satisfies Call[];

const encoded = await provider.account.encodeCalls(data);

expect(encoded).toBe(
"0xb61d27f6000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000",
);

expect(provider.account.decodeCalls).toBeDefined();

const decoded = await provider.account.decodeCalls!(encoded);

expect(decoded.length).toEqual(data.length);
expect(decoded[0].to.toLowerCase()).toEqual(data[0].to.toLowerCase());
expect(decoded[0].value).toBe(parseEther("1"));
expect(decoded[0].data?.toLowerCase()).toEqual(
data[0].data?.toLowerCase(),
);
},
);

it.each(versions)(
"should correctly encode and decode batch transaction data",
async (version) => {
const provider = await givenConnectedProvider({ signerAccount, version });
const data = [
Expand All @@ -168,8 +229,26 @@ describe("Light Account Tests", () => {
},
] satisfies Call[];

expect(await provider.account.encodeCalls(data)).toBe(
"0x47e1da2a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba720000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004deadbeef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004cafebabe00000000000000000000000000000000000000000000000000000000",
const encoded = await provider.account.encodeCalls(data);

expect(encoded).toBe(
"0x18dfb3c7000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba720000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004deadbeef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004cafebabe00000000000000000000000000000000000000000000000000000000",
);

expect(provider.account.decodeCalls).toBeDefined();

const decoded = await provider.account.decodeCalls!(encoded);

expect(decoded.length).toEqual(data.length);
expect(decoded[0].to.toLowerCase()).toEqual(data[0].to.toLowerCase());
expect(decoded[0].value).toBeUndefined();
expect(decoded[0].data?.toLowerCase()).toEqual(
data[0].data?.toLowerCase(),
);
expect(decoded[1].to.toLowerCase()).toEqual(data[1].to.toLowerCase());
expect(decoded[1].value).toBeUndefined();
expect(decoded[1].data?.toLowerCase()).toEqual(
data[1].data?.toLowerCase(),
);
},
);
Expand Down
31 changes: 8 additions & 23 deletions packages/smart-accounts/src/light-account/accounts/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import type {
} from "../registry.js";
import { EIP1967_PROXY_IMPL_STORAGE_SLOT } from "../utils.js";
import { AccountVersionRegistry } from "../registry.js";
import {
encodeCallsLA as encodeCalls,
decodeCallsLA as decodeCalls,
} from "./calldataCodec.js";
import { BaseError, lowerAddress } from "@alchemy/common";
import type {
SignatureRequest,
Expand Down Expand Up @@ -230,30 +234,11 @@ export async function toLightAccountBase<
},

async encodeCalls(calls) {
if (calls.length === 1) {
const call = calls[0];
return encodeFunctionData({
abi,
functionName: "execute",
args: [call.to, call.value ?? 0n, call.data ?? "0x"],
});
}

const [targets, values, datas] = calls.reduce(
(accum, curr) => {
accum[0].push(curr.to);
accum[1].push(curr.value ?? 0n);
accum[2].push(curr.data ?? "0x");
return accum;
},
[[], [], []] as [Address[], bigint[], Hex[]],
);
return encodeCalls(calls);
},

return encodeFunctionData({
abi,
functionName: "executeBatch",
args: [targets, values, datas],
});
async decodeCalls(data) {
return decodeCalls(data, accountAddress);
},

async getStubSignature() {
Expand Down
101 changes: 101 additions & 0 deletions packages/smart-accounts/src/light-account/accounts/calldataCodec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
decodeFunctionData,
encodeFunctionData,
type Address,
type Call,
type Hex,
} from "viem";
import { LightAccountAbi_v1 } from "../abis/LightAccountAbi_v1.js";

// Conveniently, all variants of LA up to v2.0.0 use the same function signatures for `execute` and `executeBatch`.

export function encodeCallsLA(calls: readonly Call[]): Hex {
if (calls.length === 1) {
return encodeFunctionData({
abi: LightAccountAbi_v1,
functionName: "execute",
args: [calls[0].to, calls[0].value ?? 0n, calls[0].data ?? "0x"],
});
}

const [targets, values, datas, hasValue] = calls.reduce(
(accum, curr) => {
accum[0].push(curr.to);
accum[1].push(curr.value ?? 0n);
accum[2].push(curr.data ?? "0x");
accum[3] = accum[3] || (curr.value ?? 0n) !== 0n;
return accum;
},
[[], [], [], false] as [Address[], bigint[], Hex[], boolean],
);

return encodeFunctionData({
abi: LightAccountAbi_v1,
functionName: "executeBatch",
args: hasValue ? [targets, values, datas] : [targets, datas],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh interesting, never realized we had an overload for no values.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! This is specific to LightAccount and its variants.

});
}

export function decodeCallsLA(data: Hex, accountAddress: Address): Call[] {
const decoded = decodeFunctionData({
abi: LightAccountAbi_v1,
data,
});

if (decoded.functionName === "execute") {
return [
{
to: decoded.args[0],
data: decoded.args[2],
...(decoded.args[1] !== undefined ? { value: decoded.args[1] } : {}),
},
];
}

if (decoded.functionName === "executeBatch") {
// This function is overloaded - may or may not have an array for values.

if (isThreeTupleArray(decoded.args)) {
const [targets, values, datas] = decoded.args;

if (targets.length !== values.length || targets.length !== datas.length) {
throw new Error("Invalid arguments for executeBatch");
}

return targets.map((to, index) => ({
to,
value: values[index],
data: datas[index],
}));
}

// Two tuple array (no values)
const [targets, datas] = decoded.args;

if (targets.length !== datas.length) {
throw new Error("Invalid arguments for executeBatch");
}

return targets.map((to, index) => ({
to,
data: datas[index],
}));
}

// Otherwise, treat the call as a single call to the account itself.
return [
{
to: accountAddress,
data,
},
];
}

// Needed to narrow types for LAv1 `executeBatch` function.
function isThreeTupleArray(
input:
| readonly [readonly Address[], readonly Hex[]]
| readonly [readonly Address[], readonly bigint[], readonly Hex[]],
): input is readonly [readonly Address[], readonly bigint[], readonly Hex[]] {
return input.length === 3;
}
9 changes: 9 additions & 0 deletions packages/smart-accounts/src/light-account/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export type LightAccountVersion<TAccountType extends LightAccountType> =
export type GetLightAccountType<TAccount extends LightAccountBase> =
TAccount["source"] extends LightAccountType ? TAccount["source"] : never;

export type LightAccountAbi<
TAccountType extends LightAccountType,
TAccountVersion extends LightAccountVersion<TAccountType>,
> = TAccountVersion extends keyof (typeof AccountVersionRegistry)[TAccountType]
? (typeof AccountVersionRegistry)[TAccountType][TAccountVersion] extends StaticSmartAccountImplementation
? (typeof AccountVersionRegistry)[TAccountType][TAccountVersion]["accountAbi"]
: never
: never;

/**
* Infers the EntryPoint details as a type, given the account type and version.
*/
Expand Down
Loading
Loading