@cartridge/controller 0.12.2 → 0.13.3
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/.turbo/turbo-build$colon$deps.log +17 -17
- package/.turbo/turbo-build.log +17 -17
- package/dist/iframe/keychain.d.ts +4 -1
- package/dist/index.js +786 -761
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +25 -8
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +2 -2
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.js +25 -8
- package/dist/node/index.js.map +1 -1
- package/dist/policies.d.ts +2 -2
- package/dist/{provider-B8OiOgBt.js → provider-bC9cKItb.js} +143 -128
- package/dist/provider-bC9cKItb.js.map +1 -0
- package/dist/session.js +2 -2
- package/dist/stats.html +1 -1
- package/dist/toast/types.d.ts +7 -3
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +8 -0
- package/package.json +30 -25
- package/src/__tests__/controllerDefaults.test.ts +8 -8
- package/src/__tests__/parseChainId.test.ts +12 -4
- package/src/__tests__/toWasmPolicies.test.ts +379 -0
- package/src/controller.ts +8 -15
- package/src/iframe/keychain.ts +32 -0
- package/src/policies.ts +2 -1
- package/src/toast/types.ts +7 -3
- package/src/toast/variants/marketplace.ts +6 -3
- package/src/types.ts +2 -0
- package/src/utils.ts +68 -33
- package/LICENSE +0 -16
- package/dist/provider-B8OiOgBt.js.map +0 -1
package/dist/toast/types.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export type ToastPosition = "top-left" | "top-right" | "top-center" | "bottom-le
|
|
|
2
2
|
export interface BaseToastOptions {
|
|
3
3
|
duration?: number;
|
|
4
4
|
position?: ToastPosition;
|
|
5
|
+
preset?: string;
|
|
6
|
+
safeToClose?: boolean;
|
|
5
7
|
onClick?: () => void;
|
|
6
8
|
}
|
|
7
9
|
export interface ErrorToastOptions extends BaseToastOptions {
|
|
@@ -24,6 +26,7 @@ export interface AchievementToastOptions extends BaseToastOptions {
|
|
|
24
26
|
title: string;
|
|
25
27
|
subtitle?: string;
|
|
26
28
|
xpAmount: number;
|
|
29
|
+
progress: number;
|
|
27
30
|
isDraft?: boolean;
|
|
28
31
|
}
|
|
29
32
|
export interface QuestToastOptions extends BaseToastOptions {
|
|
@@ -33,8 +36,9 @@ export interface QuestToastOptions extends BaseToastOptions {
|
|
|
33
36
|
}
|
|
34
37
|
export interface MarketplaceToastOptions extends BaseToastOptions {
|
|
35
38
|
variant: "marketplace";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
itemNames: string[];
|
|
40
|
+
itemImages: string[];
|
|
41
|
+
collectionName: string;
|
|
42
|
+
action: "purchased" | "sold" | "sent" | "listed" | "unlisted";
|
|
39
43
|
}
|
|
40
44
|
export type ToastOptions = ErrorToastOptions | TransactionToastOptions | NetworkSwitchToastOptions | AchievementToastOptions | QuestToastOptions | MarketplaceToastOptions;
|
package/dist/types.d.ts
CHANGED
|
@@ -169,5 +169,7 @@ export type OpenOptions = {
|
|
|
169
169
|
export type StarterpackOptions = {
|
|
170
170
|
/** The preimage to use */
|
|
171
171
|
preimage?: string;
|
|
172
|
+
/** Callback fired after the Play button closes the starterpack modal */
|
|
173
|
+
onPurchaseComplete?: () => void;
|
|
172
174
|
};
|
|
173
175
|
export {};
|
package/dist/utils.d.ts
CHANGED
|
@@ -9,6 +9,14 @@ 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
|
+
/**
|
|
13
|
+
* Converts parsed session policies to WASM-compatible Policy objects.
|
|
14
|
+
*
|
|
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.
|
|
18
|
+
* See: https://github.com/cartridge-gg/controller/issues/2357
|
|
19
|
+
*/
|
|
12
20
|
export declare function toWasmPolicies(policies: ParsedSessionPolicies): Policy[];
|
|
13
21
|
export declare function toArray<T>(val: T | T[]): T[];
|
|
14
22
|
export declare function humanizeString(str: string): string;
|
package/package.json
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cartridge/controller",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.3",
|
|
4
4
|
"description": "Cartridge Controller",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/cartridge-gg/controller.git",
|
|
8
|
+
"directory": "packages/controller"
|
|
9
|
+
},
|
|
5
10
|
"module": "dist/index.js",
|
|
6
11
|
"types": "dist/index.d.ts",
|
|
7
12
|
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build:deps": "pnpm build",
|
|
15
|
+
"dev": "vite build --watch",
|
|
16
|
+
"build:browser": "vite build",
|
|
17
|
+
"build:node": "tsup --config tsup.node.config.ts",
|
|
18
|
+
"build": "pnpm build:browser && pnpm build:node",
|
|
19
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
20
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
21
|
+
"test": "jest",
|
|
22
|
+
"version": "pnpm pkg get version"
|
|
23
|
+
},
|
|
8
24
|
"exports": {
|
|
9
25
|
".": {
|
|
10
26
|
"types": "./dist/index.d.ts",
|
|
@@ -21,8 +37,8 @@
|
|
|
21
37
|
}
|
|
22
38
|
},
|
|
23
39
|
"dependencies": {
|
|
24
|
-
"@cartridge/controller-wasm": "
|
|
25
|
-
"@cartridge/penpal": "
|
|
40
|
+
"@cartridge/controller-wasm": "catalog:",
|
|
41
|
+
"@cartridge/penpal": "catalog:",
|
|
26
42
|
"micro-sol-signer": "^0.5.0",
|
|
27
43
|
"bs58": "^6.0.0",
|
|
28
44
|
"ethers": "^6.13.5",
|
|
@@ -36,31 +52,20 @@
|
|
|
36
52
|
"@walletconnect/ethereum-provider": "^2.20.0"
|
|
37
53
|
},
|
|
38
54
|
"devDependencies": {
|
|
39
|
-
"@
|
|
40
|
-
"@types/
|
|
41
|
-
"@types/
|
|
55
|
+
"@cartridge/tsconfig": "workspace:*",
|
|
56
|
+
"@types/jest": "catalog:",
|
|
57
|
+
"@types/mocha": "catalog:",
|
|
58
|
+
"@types/node": "catalog:",
|
|
42
59
|
"jest": "^29.7.0",
|
|
43
|
-
"prettier": "
|
|
60
|
+
"prettier": "catalog:",
|
|
44
61
|
"rollup-plugin-visualizer": "^5.12.0",
|
|
45
62
|
"ts-jest": "^29.2.5",
|
|
46
|
-
"tsup": "
|
|
47
|
-
"typescript": "
|
|
48
|
-
"vite": "
|
|
63
|
+
"tsup": "catalog:",
|
|
64
|
+
"typescript": "catalog:",
|
|
65
|
+
"vite": "catalog:",
|
|
49
66
|
"vite-plugin-dts": "^4.5.3",
|
|
50
67
|
"vite-plugin-node-polyfills": "^0.23.0",
|
|
51
|
-
"vite-plugin-top-level-await": "
|
|
52
|
-
"vite-plugin-wasm": "
|
|
53
|
-
"@cartridge/tsconfig": "0.12.2"
|
|
54
|
-
},
|
|
55
|
-
"scripts": {
|
|
56
|
-
"build:deps": "pnpm build",
|
|
57
|
-
"dev": "vite build --watch",
|
|
58
|
-
"build:browser": "vite build",
|
|
59
|
-
"build:node": "tsup --config tsup.node.config.ts",
|
|
60
|
-
"build": "pnpm build:browser && pnpm build:node",
|
|
61
|
-
"format": "prettier --write \"src/**/*.ts\"",
|
|
62
|
-
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
63
|
-
"test": "jest",
|
|
64
|
-
"version": "pnpm pkg get version"
|
|
68
|
+
"vite-plugin-top-level-await": "catalog:",
|
|
69
|
+
"vite-plugin-wasm": "catalog:"
|
|
65
70
|
}
|
|
66
|
-
}
|
|
71
|
+
}
|
|
@@ -41,30 +41,30 @@ describe("ControllerProvider defaults", () => {
|
|
|
41
41
|
);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
test("should
|
|
45
|
-
const
|
|
44
|
+
test("should allow non-Cartridge RPC for mainnet", async () => {
|
|
45
|
+
const customChains = [
|
|
46
46
|
{ rpcUrl: "https://some-other-provider.com/starknet/mainnet/rpc/v0_9" },
|
|
47
47
|
];
|
|
48
48
|
|
|
49
49
|
expect(() => {
|
|
50
50
|
new ControllerProvider({
|
|
51
|
-
chains:
|
|
51
|
+
chains: customChains,
|
|
52
52
|
defaultChainId: constants.StarknetChainId.SN_MAIN,
|
|
53
53
|
});
|
|
54
|
-
}).toThrow(
|
|
54
|
+
}).not.toThrow();
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
test("should
|
|
58
|
-
const
|
|
57
|
+
test("should allow non-Cartridge RPC for sepolia", async () => {
|
|
58
|
+
const customChains = [
|
|
59
59
|
{ rpcUrl: "https://some-other-provider.com/starknet/sepolia/rpc/v0_9" },
|
|
60
60
|
];
|
|
61
61
|
|
|
62
62
|
expect(() => {
|
|
63
63
|
new ControllerProvider({
|
|
64
|
-
chains:
|
|
64
|
+
chains: customChains,
|
|
65
65
|
defaultChainId: constants.StarknetChainId.SN_SEPOLIA,
|
|
66
66
|
});
|
|
67
|
-
}).toThrow(
|
|
67
|
+
}).not.toThrow();
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
test("should allow non-Cartridge RPC for custom chains", () => {
|
|
@@ -42,17 +42,25 @@ describe("parseChainId", () => {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
describe("Non-Cartridge hosts", () => {
|
|
46
|
+
test("returns placeholder chainId in Node", () => {
|
|
47
|
+
expect(parseChainId(new URL("http://dl:123123"))).toBe(
|
|
48
|
+
shortString.encodeShortString("LOCALHOST"),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
45
53
|
describe("Error cases", () => {
|
|
46
54
|
test("throws error for unsupported URL format", () => {
|
|
47
55
|
expect(() =>
|
|
48
|
-
parseChainId(new URL("https://api.
|
|
49
|
-
).toThrow("Chain https://api.
|
|
56
|
+
parseChainId(new URL("https://api.cartridge.gg/unsupported")),
|
|
57
|
+
).toThrow("Chain https://api.cartridge.gg/unsupported not supported");
|
|
50
58
|
});
|
|
51
59
|
|
|
52
60
|
test("throws error for URLs without proper chain identifiers", () => {
|
|
53
61
|
expect(() =>
|
|
54
|
-
parseChainId(new URL("https://api.
|
|
55
|
-
).toThrow("Chain https://api.
|
|
62
|
+
parseChainId(new URL("https://api.cartridge.gg/v1/starknet")),
|
|
63
|
+
).toThrow("Chain https://api.cartridge.gg/v1/starknet not supported");
|
|
56
64
|
});
|
|
57
65
|
});
|
|
58
66
|
});
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { toWasmPolicies } from "../utils";
|
|
2
|
+
import { ParsedSessionPolicies } from "../policies";
|
|
3
|
+
|
|
4
|
+
describe("toWasmPolicies", () => {
|
|
5
|
+
describe("canonical ordering", () => {
|
|
6
|
+
test("sorts contracts by address regardless of input order", () => {
|
|
7
|
+
const policies1: ParsedSessionPolicies = {
|
|
8
|
+
verified: false,
|
|
9
|
+
contracts: {
|
|
10
|
+
"0xAAA": {
|
|
11
|
+
methods: [{ entrypoint: "foo", authorized: true }],
|
|
12
|
+
},
|
|
13
|
+
"0xBBB": {
|
|
14
|
+
methods: [{ entrypoint: "bar", authorized: true }],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const policies2: ParsedSessionPolicies = {
|
|
20
|
+
verified: false,
|
|
21
|
+
contracts: {
|
|
22
|
+
"0xBBB": {
|
|
23
|
+
methods: [{ entrypoint: "bar", authorized: true }],
|
|
24
|
+
},
|
|
25
|
+
"0xAAA": {
|
|
26
|
+
methods: [{ entrypoint: "foo", authorized: true }],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const result1 = toWasmPolicies(policies1);
|
|
32
|
+
const result2 = toWasmPolicies(policies2);
|
|
33
|
+
|
|
34
|
+
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");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("sorts methods within contracts by entrypoint", () => {
|
|
41
|
+
const policies1: ParsedSessionPolicies = {
|
|
42
|
+
verified: false,
|
|
43
|
+
contracts: {
|
|
44
|
+
"0xAAA": {
|
|
45
|
+
methods: [
|
|
46
|
+
{ entrypoint: "zebra", authorized: true },
|
|
47
|
+
{ entrypoint: "apple", authorized: true },
|
|
48
|
+
{ entrypoint: "mango", authorized: true },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const policies2: ParsedSessionPolicies = {
|
|
55
|
+
verified: false,
|
|
56
|
+
contracts: {
|
|
57
|
+
"0xAAA": {
|
|
58
|
+
methods: [
|
|
59
|
+
{ entrypoint: "mango", authorized: true },
|
|
60
|
+
{ entrypoint: "zebra", authorized: true },
|
|
61
|
+
{ entrypoint: "apple", authorized: true },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const result1 = toWasmPolicies(policies1);
|
|
68
|
+
const result2 = toWasmPolicies(policies2);
|
|
69
|
+
|
|
70
|
+
expect(result1).toEqual(result2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("produces consistent output for complex policies", () => {
|
|
74
|
+
const policies1: ParsedSessionPolicies = {
|
|
75
|
+
verified: false,
|
|
76
|
+
contracts: {
|
|
77
|
+
"0xCCC": {
|
|
78
|
+
methods: [
|
|
79
|
+
{ entrypoint: "c_method", authorized: true },
|
|
80
|
+
{ entrypoint: "a_method", authorized: true },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
"0xAAA": {
|
|
84
|
+
methods: [
|
|
85
|
+
{ entrypoint: "z_method", authorized: true },
|
|
86
|
+
{ entrypoint: "a_method", authorized: true },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
"0xBBB": {
|
|
90
|
+
methods: [{ entrypoint: "b_method", authorized: true }],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Same policies in different order
|
|
96
|
+
const policies2: ParsedSessionPolicies = {
|
|
97
|
+
verified: false,
|
|
98
|
+
contracts: {
|
|
99
|
+
"0xBBB": {
|
|
100
|
+
methods: [{ entrypoint: "b_method", authorized: true }],
|
|
101
|
+
},
|
|
102
|
+
"0xAAA": {
|
|
103
|
+
methods: [
|
|
104
|
+
{ entrypoint: "a_method", authorized: true },
|
|
105
|
+
{ entrypoint: "z_method", authorized: true },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
"0xCCC": {
|
|
109
|
+
methods: [
|
|
110
|
+
{ entrypoint: "a_method", authorized: true },
|
|
111
|
+
{ entrypoint: "c_method", authorized: true },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const result1 = toWasmPolicies(policies1);
|
|
118
|
+
const result2 = toWasmPolicies(policies2);
|
|
119
|
+
|
|
120
|
+
expect(result1).toEqual(result2);
|
|
121
|
+
|
|
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");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("handles case-insensitive address sorting", () => {
|
|
130
|
+
const policies1: ParsedSessionPolicies = {
|
|
131
|
+
verified: false,
|
|
132
|
+
contracts: {
|
|
133
|
+
"0xaaa": {
|
|
134
|
+
methods: [{ entrypoint: "foo", authorized: true }],
|
|
135
|
+
},
|
|
136
|
+
"0xAAB": {
|
|
137
|
+
methods: [{ entrypoint: "bar", authorized: true }],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const policies2: ParsedSessionPolicies = {
|
|
143
|
+
verified: false,
|
|
144
|
+
contracts: {
|
|
145
|
+
"0xAAB": {
|
|
146
|
+
methods: [{ entrypoint: "bar", authorized: true }],
|
|
147
|
+
},
|
|
148
|
+
"0xaaa": {
|
|
149
|
+
methods: [{ entrypoint: "foo", authorized: true }],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result1 = toWasmPolicies(policies1);
|
|
155
|
+
const result2 = toWasmPolicies(policies2);
|
|
156
|
+
|
|
157
|
+
expect(result1).toEqual(result2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("handles empty policies", () => {
|
|
161
|
+
const policies: ParsedSessionPolicies = {
|
|
162
|
+
verified: false,
|
|
163
|
+
contracts: {},
|
|
164
|
+
messages: [],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const result = toWasmPolicies(policies);
|
|
168
|
+
expect(result).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("handles undefined contracts and messages", () => {
|
|
172
|
+
const policies: ParsedSessionPolicies = {
|
|
173
|
+
verified: false,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const result = toWasmPolicies(policies);
|
|
177
|
+
expect(result).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("ApprovalPolicy handling", () => {
|
|
182
|
+
test("creates ApprovalPolicy for approve methods with spender and amount", () => {
|
|
183
|
+
const policies: ParsedSessionPolicies = {
|
|
184
|
+
verified: false,
|
|
185
|
+
contracts: {
|
|
186
|
+
"0xTOKEN": {
|
|
187
|
+
methods: [
|
|
188
|
+
{
|
|
189
|
+
entrypoint: "approve",
|
|
190
|
+
spender: "0xSPENDER",
|
|
191
|
+
amount: "1000000000000000000",
|
|
192
|
+
authorized: true,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const result = toWasmPolicies(policies);
|
|
200
|
+
|
|
201
|
+
expect(result).toHaveLength(1);
|
|
202
|
+
expect(result[0]).toEqual({
|
|
203
|
+
target: "0xTOKEN",
|
|
204
|
+
spender: "0xSPENDER",
|
|
205
|
+
amount: "1000000000000000000",
|
|
206
|
+
});
|
|
207
|
+
// Should NOT have method or authorized fields
|
|
208
|
+
expect(result[0]).not.toHaveProperty("method");
|
|
209
|
+
expect(result[0]).not.toHaveProperty("authorized");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("converts numeric amount to string in ApprovalPolicy", () => {
|
|
213
|
+
const policies: ParsedSessionPolicies = {
|
|
214
|
+
verified: false,
|
|
215
|
+
contracts: {
|
|
216
|
+
"0xTOKEN": {
|
|
217
|
+
methods: [
|
|
218
|
+
{
|
|
219
|
+
entrypoint: "approve",
|
|
220
|
+
spender: "0xSPENDER",
|
|
221
|
+
amount: 1000000000000000000,
|
|
222
|
+
authorized: true,
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const result = toWasmPolicies(policies);
|
|
230
|
+
|
|
231
|
+
expect(result[0]).toHaveProperty("amount", "1000000000000000000");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("falls back to CallPolicy for approve without spender", () => {
|
|
235
|
+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
236
|
+
|
|
237
|
+
const policies: ParsedSessionPolicies = {
|
|
238
|
+
verified: false,
|
|
239
|
+
contracts: {
|
|
240
|
+
"0xTOKEN": {
|
|
241
|
+
methods: [
|
|
242
|
+
{
|
|
243
|
+
entrypoint: "approve",
|
|
244
|
+
authorized: true,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const result = toWasmPolicies(policies);
|
|
252
|
+
|
|
253
|
+
expect(result).toHaveLength(1);
|
|
254
|
+
expect(result[0]).toHaveProperty("method");
|
|
255
|
+
expect(result[0]).toHaveProperty("authorized", true);
|
|
256
|
+
expect(result[0]).not.toHaveProperty("spender");
|
|
257
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
258
|
+
expect.stringContaining("[DEPRECATED]"),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
warnSpy.mockRestore();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("falls back to CallPolicy for approve without amount", () => {
|
|
265
|
+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
266
|
+
|
|
267
|
+
const policies: ParsedSessionPolicies = {
|
|
268
|
+
verified: false,
|
|
269
|
+
contracts: {
|
|
270
|
+
"0xTOKEN": {
|
|
271
|
+
methods: [
|
|
272
|
+
{
|
|
273
|
+
entrypoint: "approve",
|
|
274
|
+
spender: "0xSPENDER",
|
|
275
|
+
authorized: true,
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const result = toWasmPolicies(policies);
|
|
283
|
+
|
|
284
|
+
expect(result).toHaveLength(1);
|
|
285
|
+
expect(result[0]).toHaveProperty("method");
|
|
286
|
+
expect(result[0]).not.toHaveProperty("spender");
|
|
287
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
288
|
+
|
|
289
|
+
warnSpy.mockRestore();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("creates CallPolicy for non-approve methods", () => {
|
|
293
|
+
const policies: ParsedSessionPolicies = {
|
|
294
|
+
verified: false,
|
|
295
|
+
contracts: {
|
|
296
|
+
"0xCONTRACT": {
|
|
297
|
+
methods: [
|
|
298
|
+
{
|
|
299
|
+
entrypoint: "transfer",
|
|
300
|
+
authorized: true,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const result = toWasmPolicies(policies);
|
|
308
|
+
|
|
309
|
+
expect(result).toHaveLength(1);
|
|
310
|
+
expect(result[0]).toHaveProperty("target", "0xCONTRACT");
|
|
311
|
+
expect(result[0]).toHaveProperty("method");
|
|
312
|
+
expect(result[0]).toHaveProperty("authorized", true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("handles mixed approve and non-approve methods", () => {
|
|
316
|
+
const policies: ParsedSessionPolicies = {
|
|
317
|
+
verified: false,
|
|
318
|
+
contracts: {
|
|
319
|
+
"0xTOKEN": {
|
|
320
|
+
methods: [
|
|
321
|
+
{
|
|
322
|
+
entrypoint: "approve",
|
|
323
|
+
spender: "0xSPENDER",
|
|
324
|
+
amount: "1000",
|
|
325
|
+
authorized: true,
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
entrypoint: "transfer",
|
|
329
|
+
authorized: true,
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const result = toWasmPolicies(policies);
|
|
337
|
+
|
|
338
|
+
expect(result).toHaveLength(2);
|
|
339
|
+
|
|
340
|
+
// First should be approve (sorted alphabetically)
|
|
341
|
+
const approvePolicy = result[0];
|
|
342
|
+
expect(approvePolicy).toHaveProperty("spender", "0xSPENDER");
|
|
343
|
+
expect(approvePolicy).toHaveProperty("amount", "1000");
|
|
344
|
+
|
|
345
|
+
// Second should be transfer
|
|
346
|
+
const transferPolicy = result[1];
|
|
347
|
+
expect(transferPolicy).toHaveProperty("method");
|
|
348
|
+
expect(transferPolicy).toHaveProperty("authorized", true);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("sorts approve policies correctly among other methods", () => {
|
|
352
|
+
const policies: ParsedSessionPolicies = {
|
|
353
|
+
verified: false,
|
|
354
|
+
contracts: {
|
|
355
|
+
"0xTOKEN": {
|
|
356
|
+
methods: [
|
|
357
|
+
{ entrypoint: "transfer", authorized: true },
|
|
358
|
+
{
|
|
359
|
+
entrypoint: "approve",
|
|
360
|
+
spender: "0xSPENDER",
|
|
361
|
+
amount: "1000",
|
|
362
|
+
authorized: true,
|
|
363
|
+
},
|
|
364
|
+
{ entrypoint: "balance_of", authorized: true },
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const result = toWasmPolicies(policies);
|
|
371
|
+
|
|
372
|
+
expect(result).toHaveLength(3);
|
|
373
|
+
// Sorted order: approve, balance_of, transfer
|
|
374
|
+
expect(result[0]).toHaveProperty("spender"); // approve -> ApprovalPolicy
|
|
375
|
+
expect(result[1]).toHaveProperty("method"); // balance_of -> CallPolicy
|
|
376
|
+
expect(result[2]).toHaveProperty("method"); // transfer -> CallPolicy
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
package/src/controller.ts
CHANGED
|
@@ -459,7 +459,14 @@ export default class ControllerProvider extends BaseProvider {
|
|
|
459
459
|
return;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
|
|
462
|
+
const { onPurchaseComplete, ...starterpackOptions } = options ?? {};
|
|
463
|
+
this.iframes.keychain.setOnStarterpackPlay(onPurchaseComplete);
|
|
464
|
+
const sanitizedOptions =
|
|
465
|
+
Object.keys(starterpackOptions).length > 0
|
|
466
|
+
? (starterpackOptions as Omit<StarterpackOptions, "onPurchaseComplete">)
|
|
467
|
+
: undefined;
|
|
468
|
+
|
|
469
|
+
await this.keychain.openStarterPack(id, sanitizedOptions);
|
|
463
470
|
this.iframes.keychain?.open();
|
|
464
471
|
}
|
|
465
472
|
|
|
@@ -570,20 +577,6 @@ export default class ControllerProvider extends BaseProvider {
|
|
|
570
577
|
const url = new URL(chain.rpcUrl);
|
|
571
578
|
const chainId = parseChainId(url);
|
|
572
579
|
|
|
573
|
-
// Validate that mainnet and sepolia must use Cartridge RPC
|
|
574
|
-
const isMainnet = chainId === constants.StarknetChainId.SN_MAIN;
|
|
575
|
-
const isSepolia = chainId === constants.StarknetChainId.SN_SEPOLIA;
|
|
576
|
-
const isCartridgeRpc = url.hostname === "api.cartridge.gg";
|
|
577
|
-
const isLocalhost =
|
|
578
|
-
url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
579
|
-
|
|
580
|
-
if ((isMainnet || isSepolia) && !(isCartridgeRpc || isLocalhost)) {
|
|
581
|
-
throw new Error(
|
|
582
|
-
`Only Cartridge RPC providers are allowed for ${isMainnet ? "mainnet" : "sepolia"}. ` +
|
|
583
|
-
`Please use: https://api.cartridge.gg/x/starknet/${isMainnet ? "mainnet" : "sepolia"}/rpc/v0_9`,
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
580
|
this.chains.set(chainId, chain);
|
|
588
581
|
} catch (error) {
|
|
589
582
|
console.error(`Failed to parse chainId for ${chain.rpcUrl}:`, error);
|
package/src/iframe/keychain.ts
CHANGED
|
@@ -11,11 +11,15 @@ type KeychainIframeOptions = IFrameOptions<Keychain> &
|
|
|
11
11
|
needsSessionCreation?: boolean;
|
|
12
12
|
username?: string;
|
|
13
13
|
onSessionCreated?: () => void;
|
|
14
|
+
onStarterpackPlay?: () => void;
|
|
14
15
|
encryptedBlob?: string;
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
const STARTERPACK_PLAY_CALLBACK_DELAY_MS = 200;
|
|
19
|
+
|
|
17
20
|
export class KeychainIFrame extends IFrame<Keychain> {
|
|
18
21
|
private walletBridge: WalletBridge;
|
|
22
|
+
private onStarterpackPlay?: () => void;
|
|
19
23
|
|
|
20
24
|
constructor({
|
|
21
25
|
url,
|
|
@@ -32,11 +36,13 @@ export class KeychainIFrame extends IFrame<Keychain> {
|
|
|
32
36
|
needsSessionCreation,
|
|
33
37
|
username,
|
|
34
38
|
onSessionCreated,
|
|
39
|
+
onStarterpackPlay,
|
|
35
40
|
encryptedBlob,
|
|
36
41
|
propagateSessionErrors,
|
|
37
42
|
errorDisplayMode,
|
|
38
43
|
...iframeOptions
|
|
39
44
|
}: KeychainIframeOptions) {
|
|
45
|
+
let onStarterpackPlayHandler: (() => Promise<void>) | undefined;
|
|
40
46
|
const _url = new URL(url ?? KEYCHAIN_URL);
|
|
41
47
|
const walletBridge = new WalletBridge();
|
|
42
48
|
|
|
@@ -118,10 +124,32 @@ export class KeychainIFrame extends IFrame<Keychain> {
|
|
|
118
124
|
onSessionCreated();
|
|
119
125
|
}
|
|
120
126
|
},
|
|
127
|
+
onStarterpackPlay: (_origin: string) => async () => {
|
|
128
|
+
if (onStarterpackPlayHandler) {
|
|
129
|
+
await onStarterpackPlayHandler();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
121
132
|
},
|
|
122
133
|
});
|
|
123
134
|
|
|
124
135
|
this.walletBridge = walletBridge;
|
|
136
|
+
this.onStarterpackPlay = onStarterpackPlay;
|
|
137
|
+
onStarterpackPlayHandler = async () => {
|
|
138
|
+
this.close();
|
|
139
|
+
const callback = this.onStarterpackPlay;
|
|
140
|
+
this.onStarterpackPlay = undefined;
|
|
141
|
+
if (!callback) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await new Promise((resolve) =>
|
|
145
|
+
setTimeout(resolve, STARTERPACK_PLAY_CALLBACK_DELAY_MS),
|
|
146
|
+
);
|
|
147
|
+
try {
|
|
148
|
+
callback();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error("Failed to run starterpack play callback:", error);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
125
153
|
|
|
126
154
|
// Expose the wallet bridge instance globally for WASM interop
|
|
127
155
|
if (typeof window !== "undefined") {
|
|
@@ -132,4 +160,8 @@ export class KeychainIFrame extends IFrame<Keychain> {
|
|
|
132
160
|
getWalletBridge(): WalletBridge {
|
|
133
161
|
return this.walletBridge;
|
|
134
162
|
}
|
|
163
|
+
|
|
164
|
+
setOnStarterpackPlay(callback?: () => void) {
|
|
165
|
+
this.onStarterpackPlay = callback;
|
|
166
|
+
}
|
|
135
167
|
}
|