From 294273e5d25cc8e4b295b6607f1ad84f4ec03925 Mon Sep 17 00:00:00 2001 From: adam Date: Wed, 27 Aug 2025 15:17:43 -0400 Subject: [PATCH] feat: allow overriding chainid in smart wallet client --- .../wallet-apis/src/actions/formatSign.ts | 8 +- .../src/actions/grantPermissions.ts | 15 +- .../wallet-apis/src/actions/prepareCalls.ts | 16 +- .../wallet-apis/src/actions/prepareSign.ts | 22 ++- .../src/actions/sendPreparedCalls.ts | 6 +- .../wallet-apis/src/chainIdOverrides.test.ts | 158 ++++++++++++++++++ packages/wallet-apis/src/types.ts | 4 +- 7 files changed, 201 insertions(+), 28 deletions(-) create mode 100644 packages/wallet-apis/src/chainIdOverrides.test.ts diff --git a/packages/wallet-apis/src/actions/formatSign.ts b/packages/wallet-apis/src/actions/formatSign.ts index f632b6fd9f..142d99ea3d 100644 --- a/packages/wallet-apis/src/actions/formatSign.ts +++ b/packages/wallet-apis/src/actions/formatSign.ts @@ -1,6 +1,6 @@ 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"; @@ -8,7 +8,7 @@ export type FormatSignParams< TAccount extends Address | undefined = Address | undefined, > = Prettify< Omit< - WithoutChainId< + OptionalChainId< Static< (typeof wallet_formatSign)["properties"]["Request"]["properties"]["params"] >[0] @@ -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) }, + ], }); } diff --git a/packages/wallet-apis/src/actions/grantPermissions.ts b/packages/wallet-apis/src/actions/grantPermissions.ts index 2acd9650cc..35a10a5483 100644 --- a/packages/wallet-apis/src/actions/grantPermissions.ts +++ b/packages/wallet-apis/src/actions/grantPermissions.ts @@ -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 extends true ? { account: Address } @@ -103,7 +106,7 @@ export async function grantPermissions< { ...params, account, - chainId: toHex(client.chain.id), + chainId: params.chainId ?? toHex(client.chain.id), }, ], }); diff --git a/packages/wallet-apis/src/actions/prepareCalls.ts b/packages/wallet-apis/src/actions/prepareCalls.ts index 7b54d0ef64..42f3dc3924 100644 --- a/packages/wallet-apis/src/actions/prepareCalls.ts +++ b/packages/wallet-apis/src/actions/prepareCalls.ts @@ -1,5 +1,5 @@ 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"; @@ -7,11 +7,13 @@ 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 extends true ? { from: Address } : { from?: never }) >; @@ -75,7 +77,7 @@ export async function prepareCalls< params: [ { ...params, - chainId: toHex(client.chain.id), + chainId: params.chainId ?? toHex(client.chain.id), from, capabilities, }, diff --git a/packages/wallet-apis/src/actions/prepareSign.ts b/packages/wallet-apis/src/actions/prepareSign.ts index 664c6c5851..187ae42328 100644 --- a/packages/wallet-apis/src/actions/prepareSign.ts +++ b/packages/wallet-apis/src/actions/prepareSign.ts @@ -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"; @@ -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 extends true ? { from: Address } : { from?: never }) >; @@ -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), + }, + ], }); } diff --git a/packages/wallet-apis/src/actions/sendPreparedCalls.ts b/packages/wallet-apis/src/actions/sendPreparedCalls.ts index 39ca58768a..8fefddde16 100644 --- a/packages/wallet-apis/src/actions/sendPreparedCalls.ts +++ b/packages/wallet-apis/src/actions/sendPreparedCalls.ts @@ -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] @@ -57,7 +57,7 @@ export async function sendPreparedCalls( ? params : { ...params, - chainId: toHex(client.chain.id), + chainId: params.chainId ?? toHex(client.chain.id), }, ], }); diff --git a/packages/wallet-apis/src/chainIdOverrides.test.ts b/packages/wallet-apis/src/chainIdOverrides.test.ts new file mode 100644 index 0000000000..6f3c5caf1b --- /dev/null +++ b/packages/wallet-apis/src/chainIdOverrides.test.ts @@ -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); + }); +}); diff --git a/packages/wallet-apis/src/types.ts b/packages/wallet-apis/src/types.ts index aaccc05fbc..ef6e7b583c 100644 --- a/packages/wallet-apis/src/types.ts +++ b/packages/wallet-apis/src/types.ts @@ -30,8 +30,8 @@ export type InnerWalletApiClient = BaseWalletClient<{ export type SignerClient = WalletClient; -export type WithoutChainId = T extends { chainId: Hex } - ? Omit +export type OptionalChainId = T extends { chainId: Hex } + ? Omit & { chainId?: Hex | undefined } : T; export type WithoutRawPayload = T extends { rawPayload: Hex }