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
8 changes: 5 additions & 3 deletions packages/wallet-apis/src/actions/formatSign.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { Static } from "@sinclair/typebox";
import type { wallet_formatSign } from "@alchemy/wallet-api-types/rpc";
import type { InnerWalletApiClient, WithoutChainId } from "../types.ts";
import type { InnerWalletApiClient, OptionalChainId } from "../types.ts";
import { toHex, type Address, type IsUndefined, type Prettify } from "viem";
import { AccountNotFoundError } from "@alchemy/common";

export type FormatSignParams<
TAccount extends Address | undefined = Address | undefined,
> = Prettify<
Omit<
WithoutChainId<
OptionalChainId<
Static<
(typeof wallet_formatSign)["properties"]["Request"]["properties"]["params"]
>[0]
Expand Down Expand Up @@ -54,6 +54,8 @@ export async function formatSign<

return client.request({
method: "wallet_formatSign",
params: [{ ...params, from, chainId: toHex(client.chain.id) }],
params: [
{ ...params, from, chainId: params.chainId ?? toHex(client.chain.id) },
],
});
}
15 changes: 9 additions & 6 deletions packages/wallet-apis/src/actions/grantPermissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ import type { Static } from "@sinclair/typebox";
import type { wallet_createSession } from "@alchemy/wallet-api-types/rpc";
import { signSignatureRequest } from "./signSignatureRequest.js";
import { AccountNotFoundError } from "@alchemy/common";
import type { OptionalChainId } from "../types.ts";

export type GrantPermissionsParams<
TAccount extends Address | undefined = Address | undefined,
> = Prettify<
Omit<
Static<
(typeof wallet_createSession)["properties"]["Request"]["properties"]["params"]
>[0],
"account" | "chainId"
OptionalChainId<
Omit<
Static<
(typeof wallet_createSession)["properties"]["Request"]["properties"]["params"]
>[0],
"account"
>
> &
(IsUndefined<TAccount> extends true
? { account: Address }
Expand Down Expand Up @@ -103,7 +106,7 @@ export async function grantPermissions<
{
...params,
account,
chainId: toHex(client.chain.id),
chainId: params.chainId ?? toHex(client.chain.id),
},
],
});
Expand Down
16 changes: 9 additions & 7 deletions packages/wallet-apis/src/actions/prepareCalls.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { toHex, type Address, type IsUndefined, type Prettify } from "viem";
import type { InnerWalletApiClient } from "../types.ts";
import type { InnerWalletApiClient, OptionalChainId } from "../types.ts";
import type { Static } from "@sinclair/typebox";
import type { wallet_prepareCalls } from "@alchemy/wallet-api-types/rpc";
import { AccountNotFoundError } from "@alchemy/common";

export type PrepareCallsParams<
TAccount extends Address | undefined = Address | undefined,
> = Prettify<
Omit<
Static<
(typeof wallet_prepareCalls)["properties"]["Request"]["properties"]["params"]
>[0],
"from" | "chainId"
OptionalChainId<
Omit<
Static<
(typeof wallet_prepareCalls)["properties"]["Request"]["properties"]["params"]
>[0],
"from"
>
> &
(IsUndefined<TAccount> extends true ? { from: Address } : { from?: never })
>;
Expand Down Expand Up @@ -75,7 +77,7 @@ export async function prepareCalls<
params: [
{
...params,
chainId: toHex(client.chain.id),
chainId: params.chainId ?? toHex(client.chain.id),
from,
capabilities,
},
Expand Down
22 changes: 15 additions & 7 deletions packages/wallet-apis/src/actions/prepareSign.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { InnerWalletApiClient } from "../types.ts";
import type { InnerWalletApiClient, OptionalChainId } from "../types.ts";
import { toHex, type Address, type IsUndefined, type Prettify } from "viem";
import type { Static } from "@sinclair/typebox";
import type { wallet_prepareSign } from "@alchemy/wallet-api-types/rpc";
Expand All @@ -7,11 +7,13 @@ import { AccountNotFoundError } from "@alchemy/common";
export type PrepareSignParams<
TAccount extends Address | undefined = Address | undefined,
> = Prettify<
Omit<
Static<
(typeof wallet_prepareSign)["properties"]["Request"]["properties"]["params"]
>[0],
"from" | "chainId"
OptionalChainId<
Omit<
Static<
(typeof wallet_prepareSign)["properties"]["Request"]["properties"]["params"]
>[0],
"from"
>
> &
(IsUndefined<TAccount> extends true ? { from: Address } : { from?: never })
>;
Expand Down Expand Up @@ -50,6 +52,12 @@ export async function prepareSign<

return client.request({
method: "wallet_prepareSign",
params: [{ ...params, from, chainId: toHex(client.chain.id) }],
params: [
{
...params,
from,
chainId: params.chainId ?? toHex(client.chain.id),
},
],
});
}
6 changes: 3 additions & 3 deletions packages/wallet-apis/src/actions/sendPreparedCalls.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Static } from "@sinclair/typebox";
import { toHex, type Prettify } from "viem";
import type { wallet_sendPreparedCalls } from "@alchemy/wallet-api-types/rpc";
import type { InnerWalletApiClient, WithoutChainId } from "../types.ts";
import type { InnerWalletApiClient, OptionalChainId } from "../types.ts";

export type SendPreparedCallsParams = Prettify<
WithoutChainId<
OptionalChainId<
Static<
(typeof wallet_sendPreparedCalls)["properties"]["Request"]["properties"]["params"]
>[0]
Expand Down Expand Up @@ -57,7 +57,7 @@ export async function sendPreparedCalls(
? params
: {
...params,
chainId: toHex(client.chain.id),
chainId: params.chainId ?? toHex(client.chain.id),
},
],
});
Expand Down
158 changes: 158 additions & 0 deletions packages/wallet-apis/src/chainIdOverrides.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { zeroAddress } from "viem";
import { type AlchemyTransport } from "@alchemy/common";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrumSepolia } from "viem/chains";
import { createSmartWalletClient } from "./client.js";
import { custom } from "viem";

describe("chainId overrides", () => {
const signer = privateKeyToAccount(
"0xd7b061ef04d29cf68b3c89356678eccec9988de8d5ed892c19461c4a9d65925d",
);

// Track captured requests for verification
let capturedRequests: Array<{ method: string; params: any[] }> = [];

const mockTransport = custom({
async request({ method, params }) {
// Capture the request for assertion
capturedRequests.push({ method, params });

// Return mock responses based on the method
switch (method) {
case "wallet_prepareCalls":
return {
userOperation: {
sender: "0x1234567890123456789012345678901234567890",
nonce: "0x0",
initCode: "0x",
callData: "0x",
callGasLimit: "0x0",
verificationGasLimit: "0x0",
preVerificationGas: "0x0",
maxFeePerGas: "0x0",
maxPriorityFeePerGas: "0x0",
paymasterAndData: "0x",
signature: "0x",
},
signatureRequest: {
type: "personal_sign",
data: "0x",
},
};
case "wallet_sendPreparedCalls":
return {
preparedCallIds: ["test-call-id"],
};
case "wallet_prepareSign":
return {
signatureRequest: {
type: "personal_sign",
data: "0x",
},
};
case "wallet_formatSign":
return {
signature: "0x1234567890abcdef",
};
case "wallet_createSession":
return {
sessionId: "0x1234567890abcdef",
signatureRequest: {
type: "personal_sign",
data: "0x",
},
};
default:
throw new Error(`Unhandled method: ${method}`);
}
},
});

beforeEach(() => {
capturedRequests = [];
});

it("should allow overriding the chainId in prepareCalls", async () => {
const client = createSmartWalletClient({
transport: mockTransport as unknown as AlchemyTransport,
chain: arbitrumSepolia,
signer,
});

const overrideChainId = "0x1"; // Ethereum mainnet

await client.prepareCalls({
calls: [{ to: zeroAddress, value: "0x0" }],
from: "0x1234567890123456789012345678901234567890",
chainId: overrideChainId,
});

// Verify the request was captured with the overridden chainId
expect(capturedRequests).toHaveLength(1);
expect(capturedRequests[0].method).toBe("wallet_prepareCalls");
expect(capturedRequests[0].params[0].chainId).toBe(overrideChainId);
});

it("should allow overriding the chainId in sendPreparedCalls", async () => {
const client = createSmartWalletClient({
transport: mockTransport as unknown as AlchemyTransport,
chain: arbitrumSepolia,
signer,
});

const overrideChainId = "0x1"; // Ethereum mainnet

await client.sendPreparedCalls({
type: "user-operation-v060",
data: {
sender: "0x1234567890123456789012345678901234567890",
nonce: "0x0",
initCode: "0x",
callData: "0x",
callGasLimit: "0x0",
verificationGasLimit: "0x0",
preVerificationGas: "0x0",
maxFeePerGas: "0x0",
maxPriorityFeePerGas: "0x0",
paymasterAndData: "0x",
},
signature: {
type: "secp256k1",
data: "0x1234567890abcdef",
},
chainId: overrideChainId,
});

// Verify the request was captured with the overridden chainId
expect(capturedRequests).toHaveLength(1);
expect(capturedRequests[0].method).toBe("wallet_sendPreparedCalls");
expect(capturedRequests[0].params[0].chainId).toBe(overrideChainId);
});

it("should allow overriding the chainId in grantPermissions", async () => {
const client = createSmartWalletClient({
transport: mockTransport as unknown as AlchemyTransport,
chain: arbitrumSepolia,
signer,
});

const overrideChainId = "0x1"; // Ethereum mainnet

await client.grantPermissions({
account: "0x1234567890123456789012345678901234567890",
expirySec: Math.floor(Date.now() / 1000) + 60 * 60,
key: {
publicKey: "0x1234567890123456789012345678901234567890",
type: "secp256k1",
},
permissions: [{ type: "root" }],
chainId: overrideChainId,
});

// Verify the request was captured with the overridden chainId
expect(capturedRequests).toHaveLength(1);
expect(capturedRequests[0].method).toBe("wallet_createSession");
expect(capturedRequests[0].params[0].chainId).toBe(overrideChainId);
});
});
4 changes: 2 additions & 2 deletions packages/wallet-apis/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export type InnerWalletApiClient = BaseWalletClient<{

export type SignerClient = WalletClient<Transport, Chain, Account>;

export type WithoutChainId<T> = T extends { chainId: Hex }
? Omit<T, "chainId">
export type OptionalChainId<T> = T extends { chainId: Hex }
? Omit<T, "chainId"> & { chainId?: Hex | undefined }
: T;

export type WithoutRawPayload<T> = T extends { rawPayload: Hex }
Expand Down
Loading