Skip to content
Draft
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
20 changes: 14 additions & 6 deletions packages/wallet-apis/src/actions/prepareCalls.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Address, Prettify } from "viem";
import type { Narrow } from "abitype";
import type { Address, Call, Calls, Prettify } from "viem";
import type { DistributiveOmit, InnerWalletApiClient } from "../types.ts";
import { LOGGER } from "../logger.js";
import {
Expand All @@ -8,6 +9,7 @@ import {
type PrepareCallsCapabilities,
type WithCapabilities,
} from "../utils/capabilities.js";
import { encodeCalls } from "../utils/encodeCalls.js";
import { resolveAddress, type AccountParam } from "../utils/resolve.js";
import { wallet_prepareCalls as MethodSchema } from "@alchemy/wallet-api-types/rpc";
import {
Expand All @@ -22,11 +24,14 @@ const schema = methodSchema(MethodSchema);
type BasePrepareCallsParams = MethodParams<typeof MethodSchema>;
type PrepareCallsResponse = MethodResponse<typeof MethodSchema>;

export type PrepareCallsParams = Prettify<
export type PrepareCallsParams<
calls extends readonly unknown[] = readonly unknown[],
> = Prettify<
WithCapabilities<
DistributiveOmit<BasePrepareCallsParams, "from" | "chainId"> & {
DistributiveOmit<BasePrepareCallsParams, "from" | "chainId" | "calls"> & {
account?: AccountParam;
chainId?: number;
calls: Calls<Narrow<calls>>;
}
>
>;
Expand Down Expand Up @@ -79,9 +84,9 @@ export type PrepareCallsResult =
* });
* ```
*/
export async function prepareCalls(
export async function prepareCalls<const calls extends readonly unknown[]>(
client: InnerWalletApiClient,
params: PrepareCallsParams,
params: PrepareCallsParams<calls>,
): Promise<PrepareCallsResult> {
const from = params.account
? resolveAddress(params.account)
Expand All @@ -96,9 +101,12 @@ export async function prepareCalls(
hasCapabilities: !!params.capabilities,
});

const { account: _, chainId: __, ...rest } = params;
const encodedCalls = encodeCalls(params.calls as readonly Call[]);

const { account: _, chainId: __, calls: ___, ...rest } = params;
const rpcParams = encode(schema.request, {
...rest,
calls: encodedCalls,
chainId,
from,
capabilities: toRpcCapabilities(capabilities),
Expand Down
22 changes: 12 additions & 10 deletions packages/wallet-apis/src/actions/sendCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { LOGGER } from "../logger.js";
import { signSignatureRequest } from "./signSignatureRequest.js";
import { extractCapabilitiesForSending } from "../utils/capabilities.js";

export type SendCallsParams = Prettify<
DistributiveOmit<PrepareCallsParams, "chainId"> & {
export type SendCallsParams<
calls extends readonly unknown[] = readonly unknown[],
> = Prettify<
DistributiveOmit<PrepareCallsParams<calls>, "chainId"> & {
chain?: Pick<Chain, "id">;
}
>;
Expand Down Expand Up @@ -53,39 +55,39 @@ export type SendCallsResult = Prettify<SendPreparedCallsResult>;
* from the user. It is recommended to use the `prepareCalls` action instead to manually handle the permit signature.
* </Note>
*/
export async function sendCalls(
export async function sendCalls<const calls extends readonly unknown[]>(
client: InnerWalletApiClient,
params: SendCallsParams,
params: SendCallsParams<calls>,
): Promise<SendCallsResult> {
LOGGER.info("sendCalls:start", {
calls: params.calls?.length,
hasCapabilities: !!params.capabilities,
});
const { chain, ...prepareCallsParams } = params;
let calls = await prepareCalls(client, {
let prepared = await prepareCalls(client, {
...prepareCallsParams,
...(chain != null ? { chainId: chain.id } : {}),
});

if (calls.type === "paymaster-permit") {
if (prepared.type === "paymaster-permit") {
const signature = await signSignatureRequest(
client,
calls.signatureRequest,
prepared.signatureRequest,
);

const secondCallParams = {
...calls.modifiedRequest,
...prepared.modifiedRequest,
// WebAuthn signatures are not supported for paymaster permits (throws above).
paymasterPermitSignature: signature as Exclude<
typeof signature,
{ type: "webauthn-p256" }
>,
};

calls = await prepareCalls(client, secondCallParams);
prepared = await prepareCalls(client, secondCallParams);
}

const signedCalls = await signPreparedCalls(client, calls);
const signedCalls = await signPreparedCalls(client, prepared);

const sendPreparedCallsCapabilities = extractCapabilitiesForSending(
params.capabilities,
Expand Down
8 changes: 6 additions & 2 deletions packages/wallet-apis/src/decorators/smartWalletActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,15 @@ export type SmartWalletActions = {
requestAccount: (
params?: RequestAccountParams,
) => Promise<RequestAccountResult>;
prepareCalls: (params: PrepareCallsParams) => Promise<PrepareCallsResult>;
prepareCalls: <const calls extends readonly unknown[]>(
params: PrepareCallsParams<calls>,
) => Promise<PrepareCallsResult>;
sendPreparedCalls: (
params: SendPreparedCallsParams,
) => Promise<SendPreparedCallsResult>;
sendCalls: (params: SendCallsParams) => Promise<SendCallsResult>;
sendCalls: <const calls extends readonly unknown[]>(
params: SendCallsParams<calls>,
) => Promise<SendCallsResult>;
listAccounts: (params: ListAccountsParams) => Promise<ListAccountsResult>;
signSignatureRequest: (
params: SignSignatureRequestParams,
Expand Down
76 changes: 76 additions & 0 deletions packages/wallet-apis/src/utils/encodeCalls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { encodeFunctionData, erc20Abi } from "viem";
import { encodeCalls } from "./encodeCalls.js";

describe("encodeCalls", () => {
it("passes through an encoded call unchanged", () => {
const result = encodeCalls([
{ to: "0x1234567890abcdef1234567890abcdef12345678", data: "0xdeadbeef" },
]);

expect(result).toEqual([
{ to: "0x1234567890abcdef1234567890abcdef12345678", data: "0xdeadbeef" },
]);
});

it("encodes an abi-style call", () => {
const to = "0x1234567890abcdef1234567890abcdef12345678" as const;
const args = ["0x000000000000000000000000000000000000dead", 1000n] as const;

const result = encodeCalls([
{ to, abi: erc20Abi, functionName: "transfer", args },
]);

const expectedData = encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args,
});

expect(result).toEqual([{ to, data: expectedData }]);
});

it("handles a mixed array of encoded and abi-style calls", () => {
const to = "0x1234567890abcdef1234567890abcdef12345678" as const;

const result = encodeCalls([
{ to, data: "0xdeadbeef" },
{
to,
abi: erc20Abi,
functionName: "transfer",
args: ["0x000000000000000000000000000000000000dead", 500n],
},
]);

expect(result).toHaveLength(2);
expect(result[0]).toEqual({ to, data: "0xdeadbeef" });
expect(result[1]!.data).toBeDefined();
expect(result[1]!.to).toBe(to);
});

it("preserves value on both call types", () => {
const to = "0x1234567890abcdef1234567890abcdef12345678" as const;

const result = encodeCalls([
{ to, data: "0xdeadbeef", value: 100n },
{
to,
abi: erc20Abi,
functionName: "transfer",
args: ["0x000000000000000000000000000000000000dead", 500n],
value: 200n,
},
]);

expect(result[0]!.value).toBe(100n);
expect(result[1]!.value).toBe(200n);
});

it("handles a call with only to (no data or abi)", () => {
const to = "0x1234567890abcdef1234567890abcdef12345678" as const;

const result = encodeCalls([{ to, value: 1000n }]);

expect(result).toEqual([{ to, value: 1000n }]);
});
});
35 changes: 35 additions & 0 deletions packages/wallet-apis/src/utils/encodeCalls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { encodeFunctionData } from "viem";
import type { Abi, Address } from "abitype";
import type { Call } from "viem";
import type { Hex } from "viem";

type EncodedCall = {
to: Address;
data?: Hex;
value?: bigint;
};

/**
* Encodes an array of calls, converting any abi-style calls to encoded data.
* Calls that already have encoded `data` are passed through unchanged.
*
* @param {ReadonlyArray<Call>} calls - Array of calls, either encoded or abi-style
* @returns {Array<EncodedCall>} Array of calls with encoded data
*/
export function encodeCalls(calls: readonly Call[]): EncodedCall[] {
return calls.map((call) => {
const data = call.abi
? encodeFunctionData({
abi: call.abi as Abi,
functionName: call.functionName!,
args: call.args as readonly unknown[],
})
: call.data;

return {
to: call.to,
...(data != null ? { data } : {}),
...(call.value != null ? { value: call.value } : {}),
};
});
}
Loading