@cartridge/controller 0.7.14-alpha.3 → 0.9.0

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.
Files changed (58) hide show
  1. package/.turbo/turbo-build$colon$deps.log +51 -49
  2. package/.turbo/turbo-build.log +47 -45
  3. package/dist/__tests__/setup.d.ts +0 -0
  4. package/dist/account.d.ts +2 -3
  5. package/dist/controller.d.ts +7 -5
  6. package/dist/iframe/base.d.ts +1 -0
  7. package/dist/iframe/keychain.d.ts +4 -2
  8. package/dist/iframe/profile.d.ts +1 -1
  9. package/dist/index.js +885 -4955
  10. package/dist/index.js.map +1 -1
  11. package/dist/node/index.cjs +3 -3
  12. package/dist/node/index.cjs.map +1 -1
  13. package/dist/node/index.d.cts +2 -2
  14. package/dist/node/index.d.ts +2 -2
  15. package/dist/node/index.js +3 -3
  16. package/dist/node/index.js.map +1 -1
  17. package/dist/{provider-sFJ4sl5V.js → provider-B9Ikz5hr.js} +98 -75
  18. package/dist/provider-B9Ikz5hr.js.map +1 -0
  19. package/dist/provider.d.ts +1 -1
  20. package/dist/session/account.d.ts +2 -2
  21. package/dist/session.js +10 -10
  22. package/dist/session.js.map +1 -1
  23. package/dist/stats.html +1 -1
  24. package/dist/types.d.ts +6 -5
  25. package/dist/utils.d.ts +3 -3
  26. package/dist/wallets/bridge.d.ts +2 -2
  27. package/dist/wallets/index.d.ts +0 -2
  28. package/dist/wallets/metamask/index.d.ts +2 -2
  29. package/dist/wallets/rabby/index.d.ts +1 -1
  30. package/dist/wallets/types.d.ts +2 -2
  31. package/jest.config.ts +2 -0
  32. package/package.json +8 -8
  33. package/src/__tests__/controllerDefaults.test.ts +80 -0
  34. package/src/__tests__/parseChainId.test.ts +0 -6
  35. package/src/__tests__/setup.ts +29 -0
  36. package/src/account.ts +5 -8
  37. package/src/controller.ts +98 -22
  38. package/src/iframe/base.ts +19 -1
  39. package/src/iframe/keychain.ts +14 -2
  40. package/src/iframe/profile.ts +0 -11
  41. package/src/node/account.ts +3 -3
  42. package/src/provider.ts +9 -9
  43. package/src/session/account.ts +3 -4
  44. package/src/types.ts +7 -5
  45. package/src/utils.ts +49 -13
  46. package/src/wallets/bridge.ts +8 -19
  47. package/src/wallets/index.ts +0 -2
  48. package/src/wallets/metamask/index.ts +18 -13
  49. package/src/wallets/rabby/index.ts +14 -10
  50. package/src/wallets/types.ts +5 -2
  51. package/tsconfig.json +3 -2
  52. package/.turbo/turbo-format$colon$check.log +0 -7
  53. package/.turbo/turbo-format.log +0 -42
  54. package/dist/provider-sFJ4sl5V.js.map +0 -1
  55. package/dist/wallets/turnkey/index.d.ts +0 -20
  56. package/dist/wallets/wallet-connect/index.d.ts +0 -19
  57. package/src/wallets/turnkey/index.ts +0 -195
  58. package/src/wallets/wallet-connect/index.ts +0 -206
package/dist/types.d.ts CHANGED
@@ -122,8 +122,8 @@ export type Chain = {
122
122
  rpcUrl: string;
123
123
  };
124
124
  export type ProviderOptions = {
125
- defaultChainId: ChainId;
126
- chains: Chain[];
125
+ defaultChainId?: ChainId;
126
+ chains?: Chain[];
127
127
  };
128
128
  export type KeychainOptions = IFrameOptions & {
129
129
  policies?: SessionPolicies;
@@ -137,6 +137,8 @@ export type KeychainOptions = IFrameOptions & {
137
137
  feeSource?: FeeSource;
138
138
  /** Signup options (the order of the options is reflected in the UI. It's recommended to group socials and wallets together ) */
139
139
  signupOptions?: AuthOptions;
140
+ /** When true, manually provided policies will override preset policies. Default is false. */
141
+ shouldOverridePresetPolicies?: boolean;
140
142
  };
141
143
  export type ProfileOptions = IFrameOptions & {
142
144
  /** The URL of profile. Mainly for internal development purpose */
@@ -147,11 +149,10 @@ export type ProfileOptions = IFrameOptions & {
147
149
  namespace?: string;
148
150
  /** The tokens to be listed on Inventory modal */
149
151
  tokens?: Tokens;
150
- /** The policies to use for the profile */
151
- policies?: SessionPolicies;
152
152
  };
153
153
  export type ProfileContextTypeVariant = "inventory" | "trophies" | "achievements" | "leaderboard" | "activity";
154
+ export type Token = "eth" | "strk" | "lords" | "usdc" | "usdt";
154
155
  export type Tokens = {
155
- erc20?: string[];
156
+ erc20?: Token[];
156
157
  };
157
158
  export {};
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Call } from 'starknet';
2
- import { default as wasm } from '@cartridge/account-wasm/controller';
2
+ import { Policy } from '@cartridge/controller-wasm/controller';
3
3
  import { Policies, SessionPolicies } from '@cartridge/presets';
4
4
  import { ChainId } from '@starknet-io/types-js';
5
5
  import { ParsedSessionPolicies } from './policies';
@@ -9,7 +9,7 @@ export declare function normalizeCalls(calls: Call | Call[]): {
9
9
  calldata: import('starknet').HexCalldata;
10
10
  }[];
11
11
  export declare function toSessionPolicies(policies: Policies): SessionPolicies;
12
- export declare function toWasmPolicies(policies: ParsedSessionPolicies): wasm.Policy[];
12
+ export declare function toWasmPolicies(policies: ParsedSessionPolicies): Policy[];
13
13
  export declare function toArray<T>(val: T | T[]): T[];
14
14
  export declare function humanizeString(str: string): string;
15
- export declare function parseChainId(url: URL): Promise<ChainId>;
15
+ export declare function parseChainId(url: URL): ChainId;
@@ -4,7 +4,7 @@ export declare class WalletBridge {
4
4
  constructor();
5
5
  getIFrameMethods(): {
6
6
  externalDetectWallets: (_origin: string) => () => Promise<ExternalWallet[]>;
7
- externalConnectWallet: (_origin: string) => (type: ExternalWalletType, address?: string) => Promise<ExternalWalletResponse<unknown>>;
7
+ externalConnectWallet: (_origin: string) => (type: ExternalWalletType) => Promise<ExternalWalletResponse<unknown>>;
8
8
  externalSignMessage: (_origin: string) => (identifier: ExternalWalletType | string, message: string) => Promise<ExternalWalletResponse<unknown>>;
9
9
  externalSignTypedData: (_origin: string) => (identifier: ExternalWalletType | string, data: any) => Promise<ExternalWalletResponse<unknown>>;
10
10
  externalSendTransaction: (_origin: string) => (identifier: ExternalWalletType | string, txn: any) => Promise<ExternalWalletResponse<unknown>>;
@@ -13,7 +13,7 @@ export declare class WalletBridge {
13
13
  detectWallets(): Promise<ExternalWallet[]>;
14
14
  private getWalletAdapterByType;
15
15
  private handleError;
16
- connectWallet(type: ExternalWalletType, address?: string): Promise<ExternalWalletResponse>;
16
+ connectWallet(type: ExternalWalletType): Promise<ExternalWalletResponse>;
17
17
  private getConnectedWalletAdapter;
18
18
  signMessage(identifier: ExternalWalletType | string, message: string): Promise<ExternalWalletResponse>;
19
19
  signTypedData(identifier: ExternalWalletType | string, data: any): Promise<ExternalWalletResponse>;
@@ -3,6 +3,4 @@ export * from './bridge';
3
3
  export * from './metamask';
4
4
  export * from './phantom';
5
5
  export * from './rabby';
6
- export * from './turnkey';
7
6
  export * from './types';
8
- export * from './wallet-connect';
@@ -9,10 +9,10 @@ export declare class MetaMaskWallet implements WalletAdapter {
9
9
  constructor();
10
10
  isAvailable(): boolean;
11
11
  getInfo(): ExternalWallet;
12
- connect(address?: string): Promise<ExternalWalletResponse<any>>;
12
+ connect(): Promise<ExternalWalletResponse<any>>;
13
13
  getConnectedAccounts(): string[];
14
14
  signTransaction(transaction: any): Promise<ExternalWalletResponse<any>>;
15
- signMessage(message: string): Promise<ExternalWalletResponse<any>>;
15
+ signMessage(message: string, address?: string): Promise<ExternalWalletResponse<any>>;
16
16
  signTypedData(data: any): Promise<ExternalWalletResponse<any>>;
17
17
  sendTransaction(_txn: any): Promise<ExternalWalletResponse<any>>;
18
18
  switchChain(chainId: string): Promise<boolean>;
@@ -12,7 +12,7 @@ export declare class RabbyWallet implements WalletAdapter {
12
12
  connect(address?: string): Promise<ExternalWalletResponse<any>>;
13
13
  getConnectedAccounts(): string[];
14
14
  signTransaction(transaction: any): Promise<ExternalWalletResponse<any>>;
15
- signMessage(message: `0x${string}`): Promise<ExternalWalletResponse<any>>;
15
+ signMessage(message: `0x${string}`, address?: string): Promise<ExternalWalletResponse<any>>;
16
16
  signTypedData(data: any): Promise<ExternalWalletResponse<any>>;
17
17
  sendTransaction(_txn: any): Promise<ExternalWalletResponse<any>>;
18
18
  switchChain(chainId: string): Promise<boolean>;
@@ -22,8 +22,8 @@ export interface WalletAdapter {
22
22
  isAvailable(): boolean;
23
23
  getInfo(): ExternalWallet;
24
24
  getConnectedAccounts(): string[];
25
- connect(address?: string): Promise<ExternalWalletResponse<any>>;
26
- signMessage?(message: string): Promise<ExternalWalletResponse<any>>;
25
+ connect(): Promise<ExternalWalletResponse<any>>;
26
+ signMessage?(message: string, address?: string): Promise<ExternalWalletResponse<any>>;
27
27
  signTypedData?(data: any): Promise<ExternalWalletResponse<any>>;
28
28
  sendTransaction(tx: any): Promise<ExternalWalletResponse<any>>;
29
29
  getBalance(tokenAddress?: string): Promise<ExternalWalletResponse<any>>;
package/jest.config.ts CHANGED
@@ -8,6 +8,8 @@ const config: Config = {
8
8
  transform: {
9
9
  '^.+\\.tsx?$': 'ts-jest',
10
10
  },
11
+ setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts'],
12
+ testTimeout: 10000,
11
13
  };
12
14
 
13
15
  export default config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartridge/controller",
3
- "version": "0.7.14-alpha.3",
3
+ "version": "0.9.0",
4
4
  "description": "Cartridge Controller",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,23 +24,23 @@
24
24
  "@metamask/sdk": "^0.32.1",
25
25
  "@solana/web3.js": "^1.98.0",
26
26
  "open": "^10.1.0",
27
- "starknet": "^6.21.0",
27
+ "starknet": "^7.6.2",
28
28
  "starknetkit": "^2.6.1"
29
29
  },
30
30
  "dependencies": {
31
+ "@cartridge/controller-wasm": "0.2.1",
31
32
  "@cartridge/penpal": "^6.2.4",
32
33
  "ethers": "^6.13.5",
33
- "@starknet-io/types-js": "^0.7.7",
34
+ "@starknet-io/types-js": "^0.8.4",
34
35
  "@telegram-apps/sdk": "^2.4.0",
35
36
  "@turnkey/sdk-browser": "^4.0.0",
36
37
  "cbor-x": "^1.5.0",
37
38
  "mipd": "^0.0.7",
38
- "@walletconnect/ethereum-provider": "^2.20.0",
39
- "@cartridge/ui": "0.7.14-alpha.3",
40
- "@cartridge/account-wasm": "0.7.14-alpha.3"
39
+ "@walletconnect/ethereum-provider": "^2.20.0"
41
40
  },
42
41
  "devDependencies": {
43
42
  "@types/jest": "^29.5.14",
43
+ "@types/mocha": "^10.0.10",
44
44
  "@types/node": "^18.0.6",
45
45
  "jest": "^29.7.0",
46
46
  "prettier": "^3.4.2",
@@ -50,10 +50,10 @@
50
50
  "typescript": "^5.7.3",
51
51
  "vite": "^6.0.0",
52
52
  "vite-plugin-dts": "^4.5.3",
53
- "vite-plugin-node-polyfills": "^0.22.0",
53
+ "vite-plugin-node-polyfills": "^0.23.0",
54
54
  "vite-plugin-top-level-await": "^1.4.4",
55
55
  "vite-plugin-wasm": "^3.4.1",
56
- "@cartridge/tsconfig": "0.7.14-alpha.3"
56
+ "@cartridge/tsconfig": "0.9.0"
57
57
  },
58
58
  "scripts": {
59
59
  "build:deps": "pnpm build",
@@ -0,0 +1,80 @@
1
+ import { constants } from "starknet";
2
+ import ControllerProvider from "../controller";
3
+
4
+ describe("ControllerProvider defaults", () => {
5
+ let originalConsoleError: any;
6
+ let originalConsoleWarn: any;
7
+
8
+ beforeEach(() => {
9
+ // Mock console methods to suppress expected errors/warnings
10
+ originalConsoleError = console.error;
11
+ originalConsoleWarn = console.warn;
12
+ console.error = jest.fn();
13
+ console.warn = jest.fn();
14
+ });
15
+
16
+ afterEach(() => {
17
+ console.error = originalConsoleError;
18
+ console.warn = originalConsoleWarn;
19
+ });
20
+
21
+ test("should use default chains and chainId when not provided", () => {
22
+ const controller = new ControllerProvider({});
23
+
24
+ expect(controller.rpcUrl()).toBe(
25
+ "https://api.cartridge.gg/x/starknet/mainnet",
26
+ );
27
+ });
28
+
29
+ test("should use custom chains when provided", () => {
30
+ const customChains = [
31
+ { rpcUrl: "https://api.cartridge.gg/x/starknet/sepolia" },
32
+ ];
33
+
34
+ const controller = new ControllerProvider({
35
+ chains: customChains,
36
+ defaultChainId: constants.StarknetChainId.SN_SEPOLIA,
37
+ });
38
+
39
+ expect(controller.rpcUrl()).toBe(
40
+ "https://api.cartridge.gg/x/starknet/sepolia",
41
+ );
42
+ });
43
+
44
+ test("should throw error when using non-Cartridge RPC for mainnet", async () => {
45
+ const invalidChains = [
46
+ { rpcUrl: "https://some-other-provider.com/starknet/mainnet" },
47
+ ];
48
+
49
+ expect(() => {
50
+ new ControllerProvider({
51
+ chains: invalidChains,
52
+ defaultChainId: constants.StarknetChainId.SN_MAIN,
53
+ });
54
+ }).toThrow("Only Cartridge RPC providers are allowed for mainnet");
55
+ });
56
+
57
+ test("should throw error when using non-Cartridge RPC for sepolia", async () => {
58
+ const invalidChains = [
59
+ { rpcUrl: "https://some-other-provider.com/starknet/sepolia" },
60
+ ];
61
+
62
+ expect(() => {
63
+ new ControllerProvider({
64
+ chains: invalidChains,
65
+ defaultChainId: constants.StarknetChainId.SN_SEPOLIA,
66
+ });
67
+ }).toThrow("Only Cartridge RPC providers are allowed for sepolia");
68
+ });
69
+
70
+ test("should allow non-Cartridge RPC for custom chains", () => {
71
+ const customChains = [{ rpcUrl: "http://localhost:5050" }];
72
+
73
+ // This should not throw
74
+ expect(() => {
75
+ new ControllerProvider({
76
+ chains: customChains,
77
+ });
78
+ }).not.toThrow();
79
+ });
80
+ });
@@ -23,12 +23,6 @@ describe("parseChainId", () => {
23
23
  ).toBe(shortString.encodeShortString("WP_SLOT"));
24
24
  });
25
25
 
26
- test("identifies slot chain on localhost", () => {
27
- expect(parseChainId(new URL("http://localhost:8001/x/slot/katana"))).toBe(
28
- shortString.encodeShortString("WP_SLOT"),
29
- );
30
- });
31
-
32
26
  test("identifies slot chain with hyphenated name", () => {
33
27
  expect(
34
28
  parseChainId(
@@ -0,0 +1,29 @@
1
+ // Jest setup file to handle cleanup of timers and open handles
2
+
3
+ // Mock starknetkit to prevent it from creating timers
4
+ jest.mock("starknetkit", () => ({
5
+ connect: jest.fn(),
6
+ StarknetWindowObject: {},
7
+ }));
8
+
9
+ jest.mock("starknetkit/injected", () => ({
10
+ InjectedConnector: jest.fn(),
11
+ }));
12
+
13
+ // Clean up any remaining timers after each test
14
+ afterEach(() => {
15
+ jest.clearAllTimers();
16
+ jest.useRealTimers();
17
+ });
18
+
19
+ // Force cleanup after all tests
20
+ afterAll(() => {
21
+ // Clear any remaining timers
22
+ jest.clearAllTimers();
23
+ jest.useRealTimers();
24
+
25
+ // Force garbage collection if available
26
+ if (global.gc) {
27
+ global.gc();
28
+ }
29
+ });
package/src/account.ts CHANGED
@@ -6,8 +6,6 @@ import {
6
6
  AllowArray,
7
7
  } from "starknet";
8
8
 
9
- import { SPEC } from "@starknet-io/types-js";
10
-
11
9
  import {
12
10
  ConnectError,
13
11
  Keychain,
@@ -18,9 +16,9 @@ import {
18
16
  import { AsyncMethodReturns } from "@cartridge/penpal";
19
17
  import BaseProvider from "./provider";
20
18
  import { toArray } from "./utils";
19
+ import { SIGNATURE } from "@starknet-io/types-js";
21
20
 
22
21
  class ControllerAccount extends WalletAccount {
23
- address: string;
24
22
  private keychain: AsyncMethodReturns<Keychain>;
25
23
  private modal: Modal;
26
24
  private options?: KeychainOptions;
@@ -33,9 +31,8 @@ class ControllerAccount extends WalletAccount {
33
31
  options: KeychainOptions,
34
32
  modal: Modal,
35
33
  ) {
36
- super({ nodeUrl: rpcUrl }, provider);
34
+ super({ nodeUrl: rpcUrl }, provider, address);
37
35
 
38
- this.address = address;
39
36
  this.keychain = keychain;
40
37
  this.options = options;
41
38
  this.modal = modal;
@@ -111,13 +108,13 @@ class ControllerAccount extends WalletAccount {
111
108
  * @returns the signature of the JSON object
112
109
  * @throws {Error} if the JSON object is not a valid JSON
113
110
  */
114
- async signMessage(typedData: TypedData): Promise<SPEC.SIGNATURE> {
111
+ async signMessage(typedData: TypedData): Promise<SIGNATURE> {
115
112
  return new Promise(async (resolve, reject) => {
116
113
  const sessionSign = await this.keychain.signMessage(typedData, "", true);
117
114
 
118
115
  // Session sign succeeded
119
116
  if (!("code" in sessionSign)) {
120
- resolve(sessionSign as SPEC.SIGNATURE);
117
+ resolve(sessionSign as SIGNATURE);
121
118
  return;
122
119
  }
123
120
 
@@ -126,7 +123,7 @@ class ControllerAccount extends WalletAccount {
126
123
  const manualSign = await this.keychain.signMessage(typedData, "", false);
127
124
 
128
125
  if (!("code" in manualSign)) {
129
- resolve(manualSign as SPEC.SIGNATURE);
126
+ resolve(manualSign as SIGNATURE);
130
127
  } else {
131
128
  reject((manualSign as ConnectError).error);
132
129
  }
package/src/controller.ts CHANGED
@@ -1,28 +1,29 @@
1
1
  import { AsyncMethodReturns } from "@cartridge/penpal";
2
2
 
3
+ import { Policy } from "@cartridge/presets";
4
+ import {
5
+ AddInvokeTransactionResult,
6
+ AddStarknetChainParameters,
7
+ ChainId,
8
+ } from "@starknet-io/types-js";
9
+ import { constants, shortString, WalletAccount } from "starknet";
10
+ import { version } from "../package.json";
3
11
  import ControllerAccount from "./account";
4
- import { KeychainIFrame, ProfileIFrame } from "./iframe";
5
12
  import { NotReadyToConnect } from "./errors";
13
+ import { KeychainIFrame, ProfileIFrame } from "./iframe";
14
+ import BaseProvider from "./provider";
6
15
  import {
7
- Keychain,
8
- ResponseCodes,
16
+ Chain,
17
+ ConnectError,
9
18
  ConnectReply,
10
- ProbeReply,
11
19
  ControllerOptions,
12
- ConnectError,
13
- Profile,
14
20
  IFrames,
21
+ Keychain,
22
+ ProbeReply,
23
+ Profile,
15
24
  ProfileContextTypeVariant,
16
- Chain,
25
+ ResponseCodes,
17
26
  } from "./types";
18
- import BaseProvider from "./provider";
19
- import { shortString, WalletAccount } from "starknet";
20
- import { Policy } from "@cartridge/presets";
21
- import {
22
- AddInvokeTransactionResult,
23
- AddStarknetChainParameters,
24
- ChainId,
25
- } from "@starknet-io/types-js";
26
27
  import { parseChainId } from "./utils";
27
28
 
28
29
  export default class ControllerProvider extends BaseProvider {
@@ -33,10 +34,26 @@ export default class ControllerProvider extends BaseProvider {
33
34
  private selectedChain: ChainId;
34
35
  private chains: Map<ChainId, Chain>;
35
36
 
36
- constructor(options: ControllerOptions) {
37
+ isReady(): boolean {
38
+ return !!this.keychain;
39
+ }
40
+
41
+ constructor(options: ControllerOptions = {}) {
37
42
  super();
38
43
 
39
- this.selectedChain = options.defaultChainId;
44
+ // Default Cartridge chains that are always available
45
+ const cartridgeChains: Chain[] = [
46
+ { rpcUrl: "https://api.cartridge.gg/x/starknet/sepolia" },
47
+ { rpcUrl: "https://api.cartridge.gg/x/starknet/mainnet" },
48
+ ];
49
+
50
+ // Merge user chains with default chains
51
+ // User chains take precedence if they specify the same network
52
+ const chains = [...cartridgeChains, ...(options.chains || [])];
53
+ const defaultChainId =
54
+ options.defaultChainId || constants.StarknetChainId.SN_MAIN;
55
+
56
+ this.selectedChain = defaultChainId;
40
57
  this.chains = new Map<ChainId, Chain>();
41
58
 
42
59
  this.iframes = {
@@ -46,18 +63,52 @@ export default class ControllerProvider extends BaseProvider {
46
63
  onConnect: (keychain) => {
47
64
  this.keychain = keychain;
48
65
  },
66
+ version: version,
49
67
  }),
50
68
  };
51
69
 
52
- this.options = options;
70
+ this.options = { ...options, chains, defaultChainId };
53
71
 
54
- this.validateChains(options.chains);
72
+ this.initializeChains(chains);
55
73
 
56
74
  if (typeof window !== "undefined") {
57
75
  (window as any).starknet_controller = this;
58
76
  }
59
77
  }
60
78
 
79
+ async logout() {
80
+ if (!this.keychain) {
81
+ console.error(new NotReadyToConnect().message);
82
+ return;
83
+ }
84
+
85
+ try {
86
+ // Disconnect the controller/keychain first
87
+ await this.disconnect();
88
+
89
+ // Close all controller iframes
90
+ const iframes = document.querySelectorAll('iframe[id^="controller-"]');
91
+ iframes.forEach((iframe) => {
92
+ const container = iframe.parentElement;
93
+ if (container) {
94
+ container.style.visibility = "hidden";
95
+ container.style.opacity = "0";
96
+ }
97
+ });
98
+
99
+ // Reset body overflow
100
+ if (document.body) {
101
+ document.body.style.overflow = "auto";
102
+ }
103
+
104
+ // Reload the page to complete logout
105
+ window.location.reload();
106
+ } catch (err) {
107
+ console.error("Logout failed:", err);
108
+ throw err;
109
+ }
110
+ }
111
+
61
112
  async probe(): Promise<WalletAccount | undefined> {
62
113
  try {
63
114
  await this.waitForKeychain();
@@ -96,6 +147,7 @@ export default class ControllerProvider extends BaseProvider {
96
147
  openSettings: () => this.openSettings.bind(this),
97
148
  openPurchaseCredits: () => this.openPurchaseCredits.bind(this),
98
149
  openExecute: () => this.openExecute.bind(this),
150
+ logout: () => this.logout.bind(this),
99
151
  },
100
152
  rpcUrl: this.rpcUrl(),
101
153
  username,
@@ -127,7 +179,15 @@ export default class ControllerProvider extends BaseProvider {
127
179
 
128
180
  try {
129
181
  let response = await this.keychain.connect(
130
- this.options.policies || {},
182
+ // Policy precedence logic:
183
+ // 1. If shouldOverridePresetPolicies is true and policies are provided, use policies
184
+ // 2. Otherwise, if preset is defined, use empty object (let preset take precedence)
185
+ // 3. Otherwise, use provided policies or empty object
186
+ this.options.shouldOverridePresetPolicies && this.options.policies
187
+ ? this.options.policies
188
+ : this.options.preset
189
+ ? {}
190
+ : this.options.policies || {},
131
191
  this.rpcUrl(),
132
192
  this.options.signupOptions,
133
193
  );
@@ -368,14 +428,30 @@ export default class ControllerProvider extends BaseProvider {
368
428
  return await this.keychain.delegateAccount();
369
429
  }
370
430
 
371
- private async validateChains(chains: Chain[]) {
431
+ private initializeChains(chains: Chain[]) {
372
432
  for (const chain of chains) {
373
433
  try {
374
434
  const url = new URL(chain.rpcUrl);
375
- const chainId = await parseChainId(url);
435
+ const chainId = parseChainId(url);
436
+
437
+ // Validate that mainnet and sepolia must use Cartridge RPC
438
+ const isMainnet = chainId === constants.StarknetChainId.SN_MAIN;
439
+ const isSepolia = chainId === constants.StarknetChainId.SN_SEPOLIA;
440
+ const isCartridgeRpc = url.hostname === "api.cartridge.gg";
441
+ const isLocalhost =
442
+ url.hostname === "localhost" || url.hostname === "127.0.0.1";
443
+
444
+ if ((isMainnet || isSepolia) && !(isCartridgeRpc || isLocalhost)) {
445
+ throw new Error(
446
+ `Only Cartridge RPC providers are allowed for ${isMainnet ? "mainnet" : "sepolia"}. ` +
447
+ `Please use: https://api.cartridge.gg/x/starknet/${isMainnet ? "mainnet" : "sepolia"}`,
448
+ );
449
+ }
450
+
376
451
  this.chains.set(chainId, chain);
377
452
  } catch (error) {
378
453
  console.error(`Failed to parse chainId for ${chain.rpcUrl}:`, error);
454
+ throw error; // Re-throw to ensure invalid chains fail fast
379
455
  }
380
456
  }
381
457
 
@@ -14,6 +14,7 @@ export class IFrame<CallSender extends {}> implements Modal {
14
14
  private iframe?: HTMLIFrameElement;
15
15
  private container?: HTMLDivElement;
16
16
  private onClose?: () => void;
17
+ private child?: AsyncMethodReturns<CallSender>;
17
18
 
18
19
  constructor({
19
20
  id,
@@ -72,6 +73,20 @@ export class IFrame<CallSender extends {}> implements Modal {
72
73
  container.style.pointerEvents = "auto";
73
74
  container.appendChild(iframe);
74
75
 
76
+ // Add click event listener to close iframe when clicking outside
77
+ container.addEventListener("click", (e) => {
78
+ if (e.target === container) {
79
+ // Attempting to reset(clear context) for keychain iframe (identified by ID)
80
+ if (id === "controller-keychain" && this.child) {
81
+ // Type assertion for keychain child only
82
+ (this.child as any)
83
+ .reset?.()
84
+ .catch((e: any) => console.error("Error resetting context:", e));
85
+ }
86
+ this.close();
87
+ }
88
+ });
89
+
75
90
  this.iframe = iframe;
76
91
  this.container = container;
77
92
 
@@ -98,7 +113,10 @@ export class IFrame<CallSender extends {}> implements Modal {
98
113
  reload: (_origin: string) => () => window.location.reload(),
99
114
  ...methods,
100
115
  },
101
- }).promise.then(onConnect);
116
+ }).promise.then((child) => {
117
+ this.child = child;
118
+ onConnect(child);
119
+ });
102
120
 
103
121
  this.resize();
104
122
  window.addEventListener("resize", () => this.resize());
@@ -3,12 +3,20 @@ import { Keychain, KeychainOptions } from "../types";
3
3
  import { WalletBridge } from "../wallets/bridge";
4
4
  import { IFrame, IFrameOptions } from "./base";
5
5
 
6
- type KeychainIframeOptions = IFrameOptions<Keychain> & KeychainOptions;
6
+ type KeychainIframeOptions = IFrameOptions<Keychain> &
7
+ KeychainOptions & {
8
+ version?: string;
9
+ };
7
10
 
8
11
  export class KeychainIFrame extends IFrame<Keychain> {
9
12
  private walletBridge: WalletBridge;
10
13
 
11
- constructor({ url, policies, ...iframeOptions }: KeychainIframeOptions) {
14
+ constructor({
15
+ url,
16
+ policies,
17
+ version,
18
+ ...iframeOptions
19
+ }: KeychainIframeOptions) {
12
20
  const _url = new URL(url ?? KEYCHAIN_URL);
13
21
  const walletBridge = new WalletBridge();
14
22
 
@@ -19,6 +27,10 @@ export class KeychainIFrame extends IFrame<Keychain> {
19
27
  );
20
28
  }
21
29
 
30
+ if (version) {
31
+ _url.searchParams.set("v", encodeURIComponent(version));
32
+ }
33
+
22
34
  super({
23
35
  ...iframeOptions,
24
36
  id: "controller-keychain",
@@ -20,7 +20,6 @@ export class ProfileIFrame extends IFrame<Profile> {
20
20
  slot,
21
21
  namespace,
22
22
  tokens,
23
- policies,
24
23
  ...iframeOptions
25
24
  }: ProfileIFrameOptions) {
26
25
  const _profileUrl = (profileUrl || PROFILE_URL).replace(/\/$/, "");
@@ -51,16 +50,6 @@ export class ProfileIFrame extends IFrame<Profile> {
51
50
  );
52
51
  }
53
52
 
54
- if (policies?.contracts) {
55
- const methods = Object.values(policies.contracts).flatMap(
56
- (contract) => contract.methods,
57
- );
58
- _url.searchParams.set(
59
- "methods",
60
- encodeURIComponent(JSON.stringify(methods)),
61
- );
62
- }
63
-
64
53
  super({
65
54
  ...iframeOptions,
66
55
  id: "controller-profile",
@@ -1,5 +1,5 @@
1
- import { Policy } from "@cartridge/account-wasm";
2
- import { CartridgeSessionAccount } from "@cartridge/account-wasm/session";
1
+ import { Policy } from "@cartridge/controller-wasm";
2
+ import { CartridgeSessionAccount } from "@cartridge/controller-wasm/session";
3
3
  import { Call, InvokeFunctionResponse, WalletAccount } from "starknet";
4
4
 
5
5
  import { normalizeCalls } from "../utils";
@@ -37,7 +37,7 @@ export default class SessionAccount extends WalletAccount {
37
37
  sessionKeyGuid: string;
38
38
  },
39
39
  ) {
40
- super({ nodeUrl: rpcUrl }, provider);
40
+ super({ nodeUrl: rpcUrl }, provider, address);
41
41
 
42
42
  this.address = address;
43
43
  this.controller = CartridgeSessionAccount.newAsRegistered(