Skip to content

Commit f1fb04b

Browse files
plusminushalfclaude
andcommitted
Fix ~80s consumer type-checks of client aliases (pimlicolabs#500)
Checking a client created by createSmartAccountClient, createPimlicoClient, or createPasskeyServerClient against its bare alias (e.g. useQuery<SmartAccountClient>) forced TypeScript to structurally expand the full action surface on every call site: ~82s / 481k type instantiations in the issue's reproduction. The client aliases now use an inline mapped-type body over a private *Inner alias, with variance annotations on the type parameters (the same pattern viem ships in SimulateContractReturnType, wevm/viem#2557). Both sides of a comparison then carry the alias's identity, so tsc compares instantiations argument-by-argument instead of expanding them: the issue's repro drops to 0.44s / 149k instantiations. rpcSchema is deliberately left unannotated so a pairwise mismatch falls back to the structural check rather than rejecting assignments that compile today. Return-type precision is unchanged. The mapped types must stay inline: extracting them into a shared helper (e.g. Flatten<T>) re-attaches the helper's alias identity instead and restores the slowdown (measured: 444k instantiations, no improvement). The type-consumer fixture now guards all of this against the packed tarball: Equal-assertions that account/chain/client stay exactly inferred (what PR pimlicolabs#511 would have widened), bare-alias assignability (which fails loudly if the variance fast path ever hard-rejects), and custom-rpcSchema fallback for all three clients. Closes pimlicolabs#500. Supersedes pimlicolabs#511. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f90ee52 commit f1fb04b

7 files changed

Lines changed: 236 additions & 53 deletions

File tree

.changeset/heavy-planes-shave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"permissionless": patch
3+
---
4+
5+
Fixed extreme TypeScript type-checking slowness (~80s per call site, issue #500) when a client created by `createSmartAccountClient`, `createPimlicoClient`, or `createPasskeyServerClient` is checked against the bare `SmartAccountClient` / `PimlicoClient` / `PasskeyServerClient` alias (e.g. `useQuery<SmartAccountClient>`). The client type aliases now use an inline mapped-type body with variance annotations (the same pattern viem ships in `SimulateContractReturnType`, wevm/viem#2557) so TypeScript compares instantiations argument-by-argument instead of expanding the full action surface structurally. Return-type precision is unchanged.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Guards for issue #500: createSmartAccountClient's precise return type must
2+
// stay assignable to the bare SmartAccountClient alias cheaply (variance fast
3+
// path) WITHOUT the fix widening the return type. The Equal assertions fail if
4+
// the return type ever collapses to the loose defaults (or any) — which is
5+
// what the rejected fix in PR #511 would have done — and the bare-alias
6+
// assignments fail if the variance restructure ever rejects something the old
7+
// structural check accepted.
8+
import {
9+
type SmartAccountClient,
10+
createSmartAccountClient
11+
} from "permissionless"
12+
import {
13+
type ToSimpleSmartAccountReturnType,
14+
toSimpleSmartAccount
15+
} from "permissionless/accounts"
16+
import {
17+
type PasskeyServerClient,
18+
createPasskeyServerClient
19+
} from "permissionless/clients/passkeyServer"
20+
import {
21+
type PimlicoClient,
22+
createPimlicoClient
23+
} from "permissionless/clients/pimlico"
24+
import {
25+
http,
26+
type Transport,
27+
type createClient,
28+
createPublicClient
29+
} from "viem"
30+
import { entryPoint07Address } from "viem/account-abstraction"
31+
import { privateKeyToAccount } from "viem/accounts"
32+
import { sepolia } from "viem/chains"
33+
34+
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
35+
? 1
36+
: 2
37+
? true
38+
: false
39+
type Expect<T extends true> = T
40+
41+
const publicClient = createPublicClient({
42+
chain: sepolia,
43+
transport: http("https://rpc.invalid")
44+
})
45+
46+
const pimlicoClient = createPimlicoClient({
47+
transport: http("https://bundler.invalid"),
48+
entryPoint: { address: entryPoint07Address, version: "0.7" }
49+
})
50+
51+
export async function makeClient() {
52+
const account = await toSimpleSmartAccount({
53+
client: publicClient,
54+
owner: privateKeyToAccount(`0x${"11".repeat(32)}` as `0x${string}`),
55+
entryPoint: { address: entryPoint07Address, version: "0.7" }
56+
})
57+
58+
return createSmartAccountClient({
59+
account,
60+
chain: sepolia,
61+
bundlerTransport: http("https://bundler.invalid"),
62+
paymaster: pimlicoClient,
63+
userOperation: {
64+
estimateFeesPerGas: async () =>
65+
(await pimlicoClient.getUserOperationGasPrice()).fast
66+
}
67+
})
68+
}
69+
70+
type PreciseClient = Awaited<ReturnType<typeof makeClient>>
71+
declare const precise: PreciseClient
72+
73+
// The #500 hot path: precise instantiation → bare alias.
74+
export const bare: SmartAccountClient = precise
75+
76+
// Return-type precision is unchanged — everything PR #511 would have widened.
77+
export type AccountIsExact = Expect<
78+
Equal<PreciseClient["account"], ToSimpleSmartAccountReturnType<"0.7">>
79+
>
80+
export type ChainIsExact = Expect<Equal<PreciseClient["chain"], typeof sepolia>>
81+
export type ClientSlotIsExact = Expect<
82+
Equal<PreciseClient["client"], undefined>
83+
>
84+
85+
// A custom rpcSchema must still assign to the bare alias. Its type argument
86+
// differs from the bare default (`undefined`), so the variance fast path
87+
// cannot decide this pairwise — it must fall back to the structural check.
88+
// This line breaks loudly if `rpcSchema` ever gets a variance annotation (or
89+
// a future TypeScript starts measuring it) in a way that hard-rejects first.
90+
type CustomRpcSchema = [
91+
{ Method: "custom_method"; Parameters: [value: string]; ReturnType: string }
92+
]
93+
declare const withCustomSchema: SmartAccountClient<
94+
Transport,
95+
typeof sepolia,
96+
ToSimpleSmartAccountReturnType<"0.7">,
97+
undefined,
98+
CustomRpcSchema
99+
>
100+
export const bareFromCustomSchema: SmartAccountClient = withCustomSchema
101+
102+
// Same guarantees for PimlicoClient (fixed alongside, identical pathology).
103+
export const barePimlico: PimlicoClient = pimlicoClient
104+
export type PimlicoChainIsExact = Expect<
105+
Equal<(typeof pimlicoClient)["chain"], undefined>
106+
>
107+
108+
// Same guarantees for PasskeyServerClient (same inline-mapped restructure).
109+
const passkeyClient = createPasskeyServerClient({
110+
transport: http("https://passkeys.invalid")
111+
})
112+
export const barePasskey: PasskeyServerClient = passkeyClient
113+
declare const passkeyWithCustomSchema: PasskeyServerClient<CustomRpcSchema>
114+
export const barePasskeyFromCustomSchema: PasskeyServerClient =
115+
passkeyWithCustomSchema
116+
117+
// The precise client still satisfies structural consumers that were never
118+
// spelled as the alias (no aliasSymbol on either side → structural path).
119+
declare const plainViemClient: ReturnType<typeof createClient>
120+
export const clientSlotAccepts: SmartAccountClient["client"] = plainViemClient

.github/fixtures/type-consumer/tsconfig.bundler.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616
// root), so unrelated workspace typings leak into this fixture.
1717
"types": []
1818
},
19-
"files": ["consumer.ts"]
19+
"files": ["consumer.ts", "issue-500.ts"]
2020
}

.github/fixtures/type-consumer/tsconfig.node10.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,9 @@
1818
// root), so unrelated workspace typings leak into this fixture.
1919
"types": []
2020
},
21+
// issue-500.ts is checked only by tsconfig.bundler.json: its direct viem
22+
// value imports (viem/accounts, viem/chains) make node10 resolution pull
23+
// raw .ts sources out of viem's nested ox install — a fixture dependency
24+
// quirk unrelated to what either file guards.
2125
"files": ["consumer.ts"]
2226
}

packages/permissionless/clients/createSmartAccountClient.ts

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,33 +29,58 @@ import {
2929
* - Add docs
3030
* - Fix typing, 'accounts' is required to signMessage, signTypedData, signTransaction, but not needed here, since account is embedded in the client
3131
*/
32-
export type SmartAccountClient<
33-
transport extends Transport = Transport,
34-
chain extends Chain | undefined = Chain | undefined,
35-
account extends SmartAccount | undefined = SmartAccount | undefined,
36-
client extends Client | undefined = Client | undefined,
37-
rpcSchema extends RpcSchema | undefined = undefined
38-
> = Prettify<
39-
Client<
40-
transport,
41-
chain extends Chain
42-
? chain
43-
: client extends Client<any, infer chain>
44-
? chain
45-
: undefined,
46-
account,
47-
rpcSchema extends RpcSchema
48-
? [...BundlerRpcSchema, ...rpcSchema]
49-
: BundlerRpcSchema,
50-
BundlerActions<account> & SmartAccountActions<chain, account>
51-
>
32+
type SmartAccountClientInner<
33+
transport extends Transport,
34+
chain extends Chain | undefined,
35+
account extends SmartAccount | undefined,
36+
client extends Client | undefined,
37+
rpcSchema extends RpcSchema | undefined
38+
> = Client<
39+
transport,
40+
chain extends Chain
41+
? chain
42+
: // biome-ignore lint/suspicious/noExplicitAny: We need any to infer the chain type
43+
client extends Client<any, infer chain>
44+
? chain
45+
: undefined,
46+
account,
47+
rpcSchema extends RpcSchema
48+
? [...BundlerRpcSchema, ...rpcSchema]
49+
: BundlerRpcSchema,
50+
BundlerActions<account> & SmartAccountActions<chain, account>
5251
> & {
5352
client: client
5453
paymaster: BundlerClientConfig["paymaster"] | undefined
5554
paymasterContext: BundlerClientConfig["paymasterContext"] | undefined
5655
userOperation: BundlerClientConfig["userOperation"] | undefined
5756
}
5857

58+
// Variance annotations referred from viem:
59+
// https://github.com/wevm/viem/blob/main/src/actions/public/simulateContract.ts#L129
60+
export type SmartAccountClient<
61+
out transport extends Transport = Transport,
62+
/** @ts-expect-error cast variance */
63+
out chain extends Chain | undefined = Chain | undefined,
64+
/** @ts-expect-error cast variance */
65+
out account extends SmartAccount | undefined = SmartAccount | undefined,
66+
out client extends Client | undefined = Client | undefined,
67+
rpcSchema extends RpcSchema | undefined = undefined
68+
> = {
69+
[key in keyof SmartAccountClientInner<
70+
transport,
71+
chain,
72+
account,
73+
client,
74+
rpcSchema
75+
>]: SmartAccountClientInner<
76+
transport,
77+
chain,
78+
account,
79+
client,
80+
rpcSchema
81+
>[key]
82+
}
83+
5984
export type SmartAccountClientConfig<
6085
transport extends Transport = Transport,
6186
chain extends Chain | undefined = Chain | undefined,

packages/permissionless/clients/passkeyServer/index.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,21 @@ import {
1414
passkeyServerActions
1515
} from "../decorators/passkeyServer.js"
1616

17+
type PasskeyServerClientInner<rpcSchema extends RpcSchema | undefined> = Client<
18+
Transport,
19+
Chain | undefined,
20+
Account | undefined,
21+
rpcSchema extends RpcSchema
22+
? [...PasskeyServerRpcSchema, ...rpcSchema]
23+
: [...PasskeyServerRpcSchema],
24+
PasskeyServerActions
25+
>
26+
1727
export type PasskeyServerClient<
1828
rpcSchema extends RpcSchema | undefined = undefined
19-
> = Prettify<
20-
Client<
21-
Transport,
22-
Chain | undefined,
23-
Account | undefined,
24-
rpcSchema extends RpcSchema
25-
? [...PasskeyServerRpcSchema, ...rpcSchema]
26-
: [...PasskeyServerRpcSchema],
27-
PasskeyServerActions
28-
>
29-
>
29+
> = {
30+
[key in keyof PasskeyServerClientInner<rpcSchema>]: PasskeyServerClientInner<rpcSchema>[key]
31+
}
3032

3133
export type PasskeyServerClientConfig<
3234
rpcSchema extends RpcSchema | undefined = undefined

packages/permissionless/clients/pimlico/index.ts

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,58 @@ import {
2121
import type { PimlicoRpcSchema } from "../../types/pimlico.js"
2222
import { type PimlicoActions, pimlicoActions } from "../decorators/pimlico.js"
2323

24+
type PimlicoClientInner<
25+
entryPointVersion extends EntryPointVersion,
26+
transport extends Transport,
27+
chain extends Chain | undefined,
28+
account extends SmartAccount | undefined,
29+
client extends Client | undefined,
30+
rpcSchema extends RpcSchema | undefined
31+
> = Client<
32+
transport,
33+
chain extends Chain
34+
? chain
35+
: // biome-ignore lint/suspicious/noExplicitAny: We need any to infer the chain type
36+
client extends Client<any, infer chain>
37+
? chain
38+
: undefined,
39+
account,
40+
rpcSchema extends RpcSchema
41+
? [...BundlerRpcSchema, ...PimlicoRpcSchema, ...rpcSchema]
42+
: [...BundlerRpcSchema, ...PimlicoRpcSchema],
43+
BundlerActions<account> &
44+
PaymasterActions &
45+
PimlicoActions<chain, entryPointVersion>
46+
>
47+
48+
// Variance annotations referred from viem:
49+
// https://github.com/wevm/viem/blob/main/src/actions/public/simulateContract.ts#L129
2450
export type PimlicoClient<
25-
entryPointVersion extends EntryPointVersion = EntryPointVersion,
26-
transport extends Transport = Transport,
27-
chain extends Chain | undefined = Chain | undefined,
28-
account extends SmartAccount | undefined = SmartAccount | undefined,
29-
client extends Client | undefined = Client | undefined,
51+
/** @ts-expect-error cast variance */
52+
out entryPointVersion extends EntryPointVersion = EntryPointVersion,
53+
out transport extends Transport = Transport,
54+
/** @ts-expect-error cast variance */
55+
out chain extends Chain | undefined = Chain | undefined,
56+
out account extends SmartAccount | undefined = SmartAccount | undefined,
57+
out client extends Client | undefined = Client | undefined,
3058
rpcSchema extends RpcSchema | undefined = undefined
31-
> = Prettify<
32-
Client<
59+
> = {
60+
[key in keyof PimlicoClientInner<
61+
entryPointVersion,
3362
transport,
34-
chain extends Chain
35-
? chain
36-
: // biome-ignore lint/suspicious/noExplicitAny: We need any to infer the chain type
37-
client extends Client<any, infer chain>
38-
? chain
39-
: undefined,
63+
chain,
4064
account,
41-
rpcSchema extends RpcSchema
42-
? [...BundlerRpcSchema, ...PimlicoRpcSchema, ...rpcSchema]
43-
: [...BundlerRpcSchema, ...PimlicoRpcSchema],
44-
BundlerActions<account> &
45-
PaymasterActions &
46-
PimlicoActions<chain, entryPointVersion>
47-
>
48-
>
65+
client,
66+
rpcSchema
67+
>]: PimlicoClientInner<
68+
entryPointVersion,
69+
transport,
70+
chain,
71+
account,
72+
client,
73+
rpcSchema
74+
>[key]
75+
}
4976

5077
export type PimlicoClientConfig<
5178
entryPointVersion extends EntryPointVersion = EntryPointVersion,

0 commit comments

Comments
 (0)