@cartridge/controller 0.13.5 → 0.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/utils.d.ts CHANGED
@@ -8,13 +8,15 @@ export declare function normalizeCalls(calls: Call | Call[]): {
8
8
  contractAddress: string;
9
9
  calldata: import('starknet').HexCalldata;
10
10
  }[];
11
+ export declare function getPresetSessionPolicies(config: Record<string, unknown>, chainId: string): SessionPolicies | undefined;
11
12
  export declare function toSessionPolicies(policies: Policies): SessionPolicies;
12
13
  /**
13
14
  * Converts parsed session policies to WASM-compatible Policy objects.
14
15
  *
15
- * IMPORTANT: Policies are sorted canonically before hashing. Without this,
16
- * Object.keys/entries reordering can cause identical policies to produce
17
- * different merkle roots, leading to "session/not-registered" errors.
16
+ * IMPORTANT: Policies are sorted canonically and addresses are normalized
17
+ * via getChecksumAddress before hashing. Without this, Object.keys/entries
18
+ * reordering or inconsistent address casing can cause identical policies to
19
+ * produce different merkle roots, leading to "session/not-registered" errors.
18
20
  * See: https://github.com/cartridge-gg/controller/issues/2357
19
21
  */
20
22
  export declare function toWasmPolicies(policies: ParsedSessionPolicies): Policy[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartridge/controller",
3
- "version": "0.13.5",
3
+ "version": "0.13.6",
4
4
  "description": "Cartridge Controller",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,7 +56,7 @@
56
56
  "vite-plugin-node-polyfills": "^0.23.0",
57
57
  "vite-plugin-top-level-await": "^1.4.4",
58
58
  "vite-plugin-wasm": "^3.4.1",
59
- "@cartridge/tsconfig": "0.13.5"
59
+ "@cartridge/tsconfig": "0.13.6"
60
60
  },
61
61
  "scripts": {
62
62
  "build:deps": "pnpm build",
@@ -1,16 +1,27 @@
1
+ import { getChecksumAddress } from "starknet";
1
2
  import { toWasmPolicies } from "../utils";
2
3
  import { ParsedSessionPolicies } from "../policies";
3
4
 
5
+ // Valid hex addresses for testing (short but valid for getChecksumAddress)
6
+ const ADDR_A = "0x0aaa";
7
+ const ADDR_B = "0x0bbb";
8
+ const ADDR_C = "0x0ccc";
9
+
10
+ // Pre-compute checksummed forms
11
+ const ADDR_A_CS = getChecksumAddress(ADDR_A);
12
+ const ADDR_B_CS = getChecksumAddress(ADDR_B);
13
+ const ADDR_C_CS = getChecksumAddress(ADDR_C);
14
+
4
15
  describe("toWasmPolicies", () => {
5
16
  describe("canonical ordering", () => {
6
17
  test("sorts contracts by address regardless of input order", () => {
7
18
  const policies1: ParsedSessionPolicies = {
8
19
  verified: false,
9
20
  contracts: {
10
- "0xAAA": {
21
+ [ADDR_A]: {
11
22
  methods: [{ entrypoint: "foo", authorized: true }],
12
23
  },
13
- "0xBBB": {
24
+ [ADDR_B]: {
14
25
  methods: [{ entrypoint: "bar", authorized: true }],
15
26
  },
16
27
  },
@@ -19,10 +30,10 @@ describe("toWasmPolicies", () => {
19
30
  const policies2: ParsedSessionPolicies = {
20
31
  verified: false,
21
32
  contracts: {
22
- "0xBBB": {
33
+ [ADDR_B]: {
23
34
  methods: [{ entrypoint: "bar", authorized: true }],
24
35
  },
25
- "0xAAA": {
36
+ [ADDR_A]: {
26
37
  methods: [{ entrypoint: "foo", authorized: true }],
27
38
  },
28
39
  },
@@ -32,16 +43,16 @@ describe("toWasmPolicies", () => {
32
43
  const result2 = toWasmPolicies(policies2);
33
44
 
34
45
  expect(result1).toEqual(result2);
35
- // First policy should be for 0xAAA (sorted alphabetically)
36
- expect(result1[0]).toHaveProperty("target", "0xAAA");
37
- expect(result1[1]).toHaveProperty("target", "0xBBB");
46
+ // First policy should be for ADDR_A (sorted alphabetically)
47
+ expect(result1[0]).toHaveProperty("target", ADDR_A_CS);
48
+ expect(result1[1]).toHaveProperty("target", ADDR_B_CS);
38
49
  });
39
50
 
40
51
  test("sorts methods within contracts by entrypoint", () => {
41
52
  const policies1: ParsedSessionPolicies = {
42
53
  verified: false,
43
54
  contracts: {
44
- "0xAAA": {
55
+ [ADDR_A]: {
45
56
  methods: [
46
57
  { entrypoint: "zebra", authorized: true },
47
58
  { entrypoint: "apple", authorized: true },
@@ -54,7 +65,7 @@ describe("toWasmPolicies", () => {
54
65
  const policies2: ParsedSessionPolicies = {
55
66
  verified: false,
56
67
  contracts: {
57
- "0xAAA": {
68
+ [ADDR_A]: {
58
69
  methods: [
59
70
  { entrypoint: "mango", authorized: true },
60
71
  { entrypoint: "zebra", authorized: true },
@@ -74,19 +85,19 @@ describe("toWasmPolicies", () => {
74
85
  const policies1: ParsedSessionPolicies = {
75
86
  verified: false,
76
87
  contracts: {
77
- "0xCCC": {
88
+ [ADDR_C]: {
78
89
  methods: [
79
90
  { entrypoint: "c_method", authorized: true },
80
91
  { entrypoint: "a_method", authorized: true },
81
92
  ],
82
93
  },
83
- "0xAAA": {
94
+ [ADDR_A]: {
84
95
  methods: [
85
96
  { entrypoint: "z_method", authorized: true },
86
97
  { entrypoint: "a_method", authorized: true },
87
98
  ],
88
99
  },
89
- "0xBBB": {
100
+ [ADDR_B]: {
90
101
  methods: [{ entrypoint: "b_method", authorized: true }],
91
102
  },
92
103
  },
@@ -96,16 +107,16 @@ describe("toWasmPolicies", () => {
96
107
  const policies2: ParsedSessionPolicies = {
97
108
  verified: false,
98
109
  contracts: {
99
- "0xBBB": {
110
+ [ADDR_B]: {
100
111
  methods: [{ entrypoint: "b_method", authorized: true }],
101
112
  },
102
- "0xAAA": {
113
+ [ADDR_A]: {
103
114
  methods: [
104
115
  { entrypoint: "a_method", authorized: true },
105
116
  { entrypoint: "z_method", authorized: true },
106
117
  ],
107
118
  },
108
- "0xCCC": {
119
+ [ADDR_C]: {
109
120
  methods: [
110
121
  { entrypoint: "a_method", authorized: true },
111
122
  { entrypoint: "c_method", authorized: true },
@@ -119,21 +130,21 @@ describe("toWasmPolicies", () => {
119
130
 
120
131
  expect(result1).toEqual(result2);
121
132
 
122
- // Verify order: 0xAAA first, then 0xBBB, then 0xCCC
123
- // Within 0xAAA: a_method before z_method
124
- expect(result1[0]).toHaveProperty("target", "0xAAA");
125
- expect(result1[2]).toHaveProperty("target", "0xBBB");
126
- expect(result1[3]).toHaveProperty("target", "0xCCC");
133
+ // Verify order: ADDR_A first, then ADDR_B, then ADDR_C
134
+ // Within ADDR_A: a_method before z_method
135
+ expect(result1[0]).toHaveProperty("target", ADDR_A_CS);
136
+ expect(result1[2]).toHaveProperty("target", ADDR_B_CS);
137
+ expect(result1[3]).toHaveProperty("target", ADDR_C_CS);
127
138
  });
128
139
 
129
140
  test("handles case-insensitive address sorting", () => {
130
141
  const policies1: ParsedSessionPolicies = {
131
142
  verified: false,
132
143
  contracts: {
133
- "0xaaa": {
144
+ "0x0aaa": {
134
145
  methods: [{ entrypoint: "foo", authorized: true }],
135
146
  },
136
- "0xAAB": {
147
+ "0x0AAB": {
137
148
  methods: [{ entrypoint: "bar", authorized: true }],
138
149
  },
139
150
  },
@@ -142,10 +153,10 @@ describe("toWasmPolicies", () => {
142
153
  const policies2: ParsedSessionPolicies = {
143
154
  verified: false,
144
155
  contracts: {
145
- "0xAAB": {
156
+ "0x0AAB": {
146
157
  methods: [{ entrypoint: "bar", authorized: true }],
147
158
  },
148
- "0xaaa": {
159
+ "0x0aaa": {
149
160
  methods: [{ entrypoint: "foo", authorized: true }],
150
161
  },
151
162
  },
@@ -157,6 +168,34 @@ describe("toWasmPolicies", () => {
157
168
  expect(result1).toEqual(result2);
158
169
  });
159
170
 
171
+ test("normalizes address casing via getChecksumAddress", () => {
172
+ const addr =
173
+ "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7";
174
+ const policies1: ParsedSessionPolicies = {
175
+ verified: false,
176
+ contracts: {
177
+ [addr.toLowerCase()]: {
178
+ methods: [{ entrypoint: "transfer", authorized: true }],
179
+ },
180
+ },
181
+ };
182
+
183
+ const policies2: ParsedSessionPolicies = {
184
+ verified: false,
185
+ contracts: {
186
+ [addr.toUpperCase().replace("0X", "0x")]: {
187
+ methods: [{ entrypoint: "transfer", authorized: true }],
188
+ },
189
+ },
190
+ };
191
+
192
+ const result1 = toWasmPolicies(policies1);
193
+ const result2 = toWasmPolicies(policies2);
194
+
195
+ expect(result1).toEqual(result2);
196
+ expect(result1[0]).toHaveProperty("target", getChecksumAddress(addr));
197
+ });
198
+
160
199
  test("handles empty policies", () => {
161
200
  const policies: ParsedSessionPolicies = {
162
201
  verified: false,
@@ -179,15 +218,21 @@ describe("toWasmPolicies", () => {
179
218
  });
180
219
 
181
220
  describe("ApprovalPolicy handling", () => {
221
+ const TOKEN =
222
+ "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7";
223
+ const TOKEN_CS = getChecksumAddress(TOKEN);
224
+ const SPENDER =
225
+ "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d";
226
+
182
227
  test("creates ApprovalPolicy for approve methods with spender and amount", () => {
183
228
  const policies: ParsedSessionPolicies = {
184
229
  verified: false,
185
230
  contracts: {
186
- "0xTOKEN": {
231
+ [TOKEN]: {
187
232
  methods: [
188
233
  {
189
234
  entrypoint: "approve",
190
- spender: "0xSPENDER",
235
+ spender: SPENDER,
191
236
  amount: "1000000000000000000",
192
237
  authorized: true,
193
238
  },
@@ -200,8 +245,8 @@ describe("toWasmPolicies", () => {
200
245
 
201
246
  expect(result).toHaveLength(1);
202
247
  expect(result[0]).toEqual({
203
- target: "0xTOKEN",
204
- spender: "0xSPENDER",
248
+ target: TOKEN_CS,
249
+ spender: SPENDER,
205
250
  amount: "1000000000000000000",
206
251
  });
207
252
  // Should NOT have method or authorized fields
@@ -213,11 +258,11 @@ describe("toWasmPolicies", () => {
213
258
  const policies: ParsedSessionPolicies = {
214
259
  verified: false,
215
260
  contracts: {
216
- "0xTOKEN": {
261
+ [TOKEN]: {
217
262
  methods: [
218
263
  {
219
264
  entrypoint: "approve",
220
- spender: "0xSPENDER",
265
+ spender: SPENDER,
221
266
  amount: 1000000000000000000,
222
267
  authorized: true,
223
268
  },
@@ -237,7 +282,7 @@ describe("toWasmPolicies", () => {
237
282
  const policies: ParsedSessionPolicies = {
238
283
  verified: false,
239
284
  contracts: {
240
- "0xTOKEN": {
285
+ [TOKEN]: {
241
286
  methods: [
242
287
  {
243
288
  entrypoint: "approve",
@@ -267,11 +312,11 @@ describe("toWasmPolicies", () => {
267
312
  const policies: ParsedSessionPolicies = {
268
313
  verified: false,
269
314
  contracts: {
270
- "0xTOKEN": {
315
+ [TOKEN]: {
271
316
  methods: [
272
317
  {
273
318
  entrypoint: "approve",
274
- spender: "0xSPENDER",
319
+ spender: SPENDER,
275
320
  authorized: true,
276
321
  },
277
322
  ],
@@ -290,10 +335,14 @@ describe("toWasmPolicies", () => {
290
335
  });
291
336
 
292
337
  test("creates CallPolicy for non-approve methods", () => {
338
+ const CONTRACT =
339
+ "0x0124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49";
340
+ const CONTRACT_CS = getChecksumAddress(CONTRACT);
341
+
293
342
  const policies: ParsedSessionPolicies = {
294
343
  verified: false,
295
344
  contracts: {
296
- "0xCONTRACT": {
345
+ [CONTRACT]: {
297
346
  methods: [
298
347
  {
299
348
  entrypoint: "transfer",
@@ -307,7 +356,7 @@ describe("toWasmPolicies", () => {
307
356
  const result = toWasmPolicies(policies);
308
357
 
309
358
  expect(result).toHaveLength(1);
310
- expect(result[0]).toHaveProperty("target", "0xCONTRACT");
359
+ expect(result[0]).toHaveProperty("target", CONTRACT_CS);
311
360
  expect(result[0]).toHaveProperty("method");
312
361
  expect(result[0]).toHaveProperty("authorized", true);
313
362
  });
@@ -316,11 +365,11 @@ describe("toWasmPolicies", () => {
316
365
  const policies: ParsedSessionPolicies = {
317
366
  verified: false,
318
367
  contracts: {
319
- "0xTOKEN": {
368
+ [TOKEN]: {
320
369
  methods: [
321
370
  {
322
371
  entrypoint: "approve",
323
- spender: "0xSPENDER",
372
+ spender: SPENDER,
324
373
  amount: "1000",
325
374
  authorized: true,
326
375
  },
@@ -339,7 +388,7 @@ describe("toWasmPolicies", () => {
339
388
 
340
389
  // First should be approve (sorted alphabetically)
341
390
  const approvePolicy = result[0];
342
- expect(approvePolicy).toHaveProperty("spender", "0xSPENDER");
391
+ expect(approvePolicy).toHaveProperty("spender", SPENDER);
343
392
  expect(approvePolicy).toHaveProperty("amount", "1000");
344
393
 
345
394
  // Second should be transfer
@@ -352,12 +401,12 @@ describe("toWasmPolicies", () => {
352
401
  const policies: ParsedSessionPolicies = {
353
402
  verified: false,
354
403
  contracts: {
355
- "0xTOKEN": {
404
+ [TOKEN]: {
356
405
  methods: [
357
406
  { entrypoint: "transfer", authorized: true },
358
407
  {
359
408
  entrypoint: "approve",
360
- spender: "0xSPENDER",
409
+ spender: SPENDER,
361
410
  amount: "1000",
362
411
  authorized: true,
363
412
  },
@@ -104,6 +104,13 @@ export class KeychainIFrame extends IFrame<Keychain> {
104
104
  );
105
105
  } else if (preset) {
106
106
  _url.searchParams.set("preset", preset);
107
+ if (policies) {
108
+ console.warn(
109
+ "[Controller] Both `preset` and `policies` provided to ControllerProvider. " +
110
+ "Policies are ignored when preset is set. " +
111
+ "Use `shouldOverridePresetPolicies: true` to override.",
112
+ );
113
+ }
107
114
  }
108
115
 
109
116
  // Add encrypted blob to URL fragment (hash) if present
@@ -4,14 +4,14 @@ import {
4
4
  signerToGuid,
5
5
  subscribeCreateSession,
6
6
  } from "@cartridge/controller-wasm";
7
- import { SessionPolicies } from "@cartridge/presets";
7
+ import { loadConfig, SessionPolicies } from "@cartridge/presets";
8
8
  import { AddStarknetChainParameters } from "@starknet-io/types-js";
9
9
  import { encode } from "starknet";
10
10
  import { API_URL, KEYCHAIN_URL } from "../constants";
11
- import { ParsedSessionPolicies } from "../policies";
11
+ import { parsePolicies, ParsedSessionPolicies } from "../policies";
12
12
  import BaseProvider from "../provider";
13
13
  import { AuthOptions } from "../types";
14
- import { toWasmPolicies } from "../utils";
14
+ import { getPresetSessionPolicies, toWasmPolicies } from "../utils";
15
15
  import SessionAccount from "./account";
16
16
 
17
17
  interface SessionRegistration {
@@ -29,7 +29,9 @@ interface SessionRegistration {
29
29
  export type SessionOptions = {
30
30
  rpc: string;
31
31
  chainId: string;
32
- policies: SessionPolicies;
32
+ policies?: SessionPolicies;
33
+ preset?: string;
34
+ shouldOverridePresetPolicies?: boolean;
33
35
  redirectUrl: string;
34
36
  disconnectRedirectUrl?: string;
35
37
  keychainUrl?: string;
@@ -47,10 +49,12 @@ export default class SessionProvider extends BaseProvider {
47
49
  protected _redirectUrl: string;
48
50
  protected _disconnectRedirectUrl?: string;
49
51
  protected _policies: ParsedSessionPolicies;
52
+ protected _preset?: string;
53
+ private _ready: Promise<void>;
50
54
  protected _keychainUrl: string;
51
55
  protected _apiUrl: string;
52
- protected _publicKey: string;
53
- protected _sessionKeyGuid: string;
56
+ protected _publicKey!: string;
57
+ protected _sessionKeyGuid!: string;
54
58
  protected _signupOptions?: AuthOptions;
55
59
  public reopenBrowser: boolean = true;
56
60
 
@@ -58,6 +62,8 @@ export default class SessionProvider extends BaseProvider {
58
62
  rpc,
59
63
  chainId,
60
64
  policies,
65
+ preset,
66
+ shouldOverridePresetPolicies,
61
67
  redirectUrl,
62
68
  disconnectRedirectUrl,
63
69
  keychainUrl,
@@ -66,27 +72,27 @@ export default class SessionProvider extends BaseProvider {
66
72
  }: SessionOptions) {
67
73
  super();
68
74
 
69
- this._policies = {
70
- verified: false,
71
- contracts: policies.contracts
72
- ? Object.fromEntries(
73
- Object.entries(policies.contracts).map(([address, contract]) => [
74
- address,
75
- {
76
- ...contract,
77
- methods: contract.methods.map((method) => ({
78
- ...method,
79
- authorized: true,
80
- })),
81
- },
82
- ]),
83
- )
84
- : undefined,
85
- messages: policies.messages?.map((message) => ({
86
- ...message,
87
- authorized: true,
88
- })),
89
- };
75
+ if (!policies && !preset) {
76
+ throw new Error("Either `policies` or `preset` must be provided");
77
+ }
78
+
79
+ // Policy precedence logic (matching ControllerProvider):
80
+ // 1. If shouldOverridePresetPolicies is true and policies are provided, use policies
81
+ // 2. Otherwise, if preset is defined, resolve policies from preset
82
+ // 3. Otherwise, use provided policies
83
+ if ((!preset || shouldOverridePresetPolicies) && policies) {
84
+ this._policies = parsePolicies(policies);
85
+ } else {
86
+ this._preset = preset;
87
+ if (policies) {
88
+ console.warn(
89
+ "[Controller] Both `preset` and `policies` provided to SessionProvider. " +
90
+ "Policies are ignored when preset is set. " +
91
+ "Use `shouldOverridePresetPolicies: true` to override.",
92
+ );
93
+ }
94
+ this._policies = { verified: false };
95
+ }
90
96
 
91
97
  this._rpcUrl = rpc;
92
98
  this._chainId = chainId;
@@ -96,6 +102,33 @@ export default class SessionProvider extends BaseProvider {
96
102
  this._apiUrl = apiUrl ?? API_URL;
97
103
  this._signupOptions = signupOptions;
98
104
 
105
+ // Eagerly start async init: resolve preset policies (if any),
106
+ // then try to restore an existing session from storage.
107
+ // All public async methods await this before proceeding.
108
+ this._ready = this._init();
109
+
110
+ if (typeof window !== "undefined") {
111
+ (window as any).starknet_controller_session = this;
112
+ }
113
+ }
114
+
115
+ private async _init(): Promise<void> {
116
+ if (this._preset) {
117
+ const config = await loadConfig(this._preset);
118
+ if (!config) {
119
+ throw new Error(`Failed to load preset: ${this._preset}`);
120
+ }
121
+
122
+ const sessionPolicies = getPresetSessionPolicies(config, this._chainId);
123
+ if (!sessionPolicies) {
124
+ throw new Error(
125
+ `No policies found for chain ${this._chainId} in preset ${this._preset}`,
126
+ );
127
+ }
128
+
129
+ this._policies = parsePolicies(sessionPolicies);
130
+ }
131
+
99
132
  const account = this.tryRetrieveFromQueryOrStorage();
100
133
  if (!account) {
101
134
  const pk = stark.randomAddress();
@@ -125,10 +158,6 @@ export default class SessionProvider extends BaseProvider {
125
158
  starknet: { privateKey: encode.addHexPrefix(jsonPk.privKey) },
126
159
  });
127
160
  }
128
-
129
- if (typeof window !== "undefined") {
130
- (window as any).starknet_controller_session = this;
131
- }
132
161
  }
133
162
 
134
163
  private validatePoliciesSubset(
@@ -218,25 +247,18 @@ export default class SessionProvider extends BaseProvider {
218
247
  }
219
248
 
220
249
  async username() {
221
- await this.tryRetrieveFromQueryOrStorage();
250
+ await this._ready;
222
251
  return this._username;
223
252
  }
224
253
 
225
254
  async probe(): Promise<WalletAccount | undefined> {
226
- if (this.account) {
227
- return this.account;
228
- }
229
-
230
- this.account = this.tryRetrieveFromQueryOrStorage();
255
+ await this._ready;
231
256
  return this.account;
232
257
  }
233
258
 
234
259
  async connect(): Promise<WalletAccount | undefined> {
235
- if (this.account) {
236
- return this.account;
237
- }
260
+ await this._ready;
238
261
 
239
- this.account = this.tryRetrieveFromQueryOrStorage();
240
262
  if (this.account) {
241
263
  return this.account;
242
264
  }
@@ -259,13 +281,18 @@ export default class SessionProvider extends BaseProvider {
259
281
  this._sessionKeyGuid = signerToGuid({
260
282
  starknet: { privateKey: encode.addHexPrefix(pk) },
261
283
  });
262
- let url = `${
263
- this._keychainUrl
264
- }/session?public_key=${this._publicKey}&redirect_uri=${
265
- this._redirectUrl
266
- }&redirect_query_name=startapp&policies=${JSON.stringify(
267
- this._policies,
268
- )}&rpc_url=${this._rpcUrl}`;
284
+ let url =
285
+ `${this._keychainUrl}` +
286
+ `/session?public_key=${this._publicKey}` +
287
+ `&redirect_uri=${this._redirectUrl}` +
288
+ `&redirect_query_name=startapp` +
289
+ `&rpc_url=${this._rpcUrl}`;
290
+
291
+ if (this._preset) {
292
+ url += `&preset=${encodeURIComponent(this._preset)}`;
293
+ } else {
294
+ url += `&policies=${encodeURIComponent(JSON.stringify(this._policies))}`;
295
+ }
269
296
 
270
297
  if (this._signupOptions) {
271
298
  url += `&signers=${encodeURIComponent(JSON.stringify(this._signupOptions))}`;
package/src/utils.ts CHANGED
@@ -48,6 +48,19 @@ export function normalizeCalls(calls: Call | Call[]) {
48
48
  });
49
49
  }
50
50
 
51
+ export function getPresetSessionPolicies(
52
+ config: Record<string, unknown>,
53
+ chainId: string,
54
+ ): SessionPolicies | undefined {
55
+ const decodedChainId = shortString.decodeShortString(chainId);
56
+ const chains = config.chains as
57
+ | Record<string, Record<string, unknown>>
58
+ | undefined;
59
+ const chainConfig = chains?.[decodedChainId];
60
+ if (!chainConfig?.policies) return undefined;
61
+ return toSessionPolicies(chainConfig.policies as Policies);
62
+ }
63
+
51
64
  export function toSessionPolicies(policies: Policies): SessionPolicies {
52
65
  return Array.isArray(policies)
53
66
  ? policies.reduce<SessionPolicies>(
@@ -92,9 +105,10 @@ export function toSessionPolicies(policies: Policies): SessionPolicies {
92
105
  /**
93
106
  * Converts parsed session policies to WASM-compatible Policy objects.
94
107
  *
95
- * IMPORTANT: Policies are sorted canonically before hashing. Without this,
96
- * Object.keys/entries reordering can cause identical policies to produce
97
- * different merkle roots, leading to "session/not-registered" errors.
108
+ * IMPORTANT: Policies are sorted canonically and addresses are normalized
109
+ * via getChecksumAddress before hashing. Without this, Object.keys/entries
110
+ * reordering or inconsistent address casing can cause identical policies to
111
+ * produce different merkle roots, leading to "session/not-registered" errors.
98
112
  * See: https://github.com/cartridge-gg/controller/issues/2357
99
113
  */
100
114
  export function toWasmPolicies(policies: ParsedSessionPolicies): Policy[] {
@@ -110,7 +124,7 @@ export function toWasmPolicies(policies: ParsedSessionPolicies): Policy[] {
110
124
  if (m.entrypoint === "approve") {
111
125
  if ("spender" in m && "amount" in m && m.spender && m.amount) {
112
126
  const approvalPolicy: ApprovalPolicy = {
113
- target,
127
+ target: getChecksumAddress(target),
114
128
  spender: m.spender,
115
129
  amount: String(m.amount),
116
130
  };
@@ -127,7 +141,7 @@ export function toWasmPolicies(policies: ParsedSessionPolicies): Policy[] {
127
141
 
128
142
  // For non-approve methods and legacy approve, create a regular CallPolicy
129
143
  return {
130
- target,
144
+ target: getChecksumAddress(target),
131
145
  method: hash.getSelectorFromName(m.entrypoint),
132
146
  authorized: !!m.authorized,
133
147
  };