@provable-games/metagame-sdk 0.1.2 → 0.1.4

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/merkle.js ADDED
@@ -0,0 +1,200 @@
1
+ import { StandardMerkleTree } from '@ericnordelo/strk-merkle-tree';
2
+ import { hash } from 'starknet';
3
+
4
+ // src/merkle/merkleTree.ts
5
+ function computeLeafValue(address, count) {
6
+ const intermediate = hash.computePedersenHash("0x0", address);
7
+ return hash.computePedersenHash(intermediate, "0x" + count.toString(16));
8
+ }
9
+ function buildMerkleTree(entries) {
10
+ if (entries.length === 0) {
11
+ throw new Error("Cannot build merkle tree with no entries");
12
+ }
13
+ const leaves = entries.map((e) => [computeLeafValue(e.address, e.count)]);
14
+ const tree = StandardMerkleTree.of(leaves, ["felt252"], {
15
+ sortLeaves: true
16
+ });
17
+ return {
18
+ version: 2,
19
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
20
+ entries,
21
+ root: tree.root,
22
+ tree: tree.dump()
23
+ };
24
+ }
25
+ function getProof(treeData, address, count) {
26
+ const tree = StandardMerkleTree.load(treeData.tree);
27
+ const leafValue = computeLeafValue(address, count);
28
+ for (const [index, leaf] of tree.entries()) {
29
+ if (BigInt(leaf[0]) === BigInt(leafValue)) {
30
+ return tree.getProof(index);
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+ function verifyProof(treeData, address, count, proof) {
36
+ const leafValue = computeLeafValue(address, count);
37
+ return StandardMerkleTree.verify(
38
+ treeData.root,
39
+ ["felt252"],
40
+ [leafValue],
41
+ proof
42
+ );
43
+ }
44
+ function buildQualification(count, proof) {
45
+ return ["0x" + count.toString(16), ...proof];
46
+ }
47
+ function findEntry(treeData, address) {
48
+ return treeData.entries.find(
49
+ (e) => e.address.toLowerCase() === address.toLowerCase() || BigInt(e.address) === BigInt(address)
50
+ ) || null;
51
+ }
52
+
53
+ // src/merkle/client.ts
54
+ var DEFAULT_MERKLE_API_URLS = {
55
+ SN_SEPOLIA: "https://merkle-api-sepolia.up.railway.app",
56
+ SN_MAIN: "https://merkle-api.up.railway.app"
57
+ };
58
+ var MerkleClient = class {
59
+ apiUrl;
60
+ constructor(config) {
61
+ this.apiUrl = config.apiUrl ?? DEFAULT_MERKLE_API_URLS[config.chainId] ?? DEFAULT_MERKLE_API_URLS.SN_MAIN;
62
+ }
63
+ /** Fetch paginated list of merkle trees. */
64
+ async getTrees(options) {
65
+ const empty = {
66
+ data: [],
67
+ total: 0,
68
+ page: 1,
69
+ limit: 20,
70
+ totalPages: 0
71
+ };
72
+ try {
73
+ const params = new URLSearchParams();
74
+ if (options?.page) params.set("page", String(options.page));
75
+ if (options?.limit) params.set("limit", String(options.limit));
76
+ const qs = params.toString();
77
+ const res = await fetch(`${this.apiUrl}/trees${qs ? `?${qs}` : ""}`);
78
+ if (!res.ok) return empty;
79
+ const json = await res.json();
80
+ return {
81
+ data: json.data ?? [],
82
+ total: json.total ?? 0,
83
+ page: json.page ?? 1,
84
+ limit: json.limit ?? 20,
85
+ totalPages: json.totalPages ?? 0
86
+ };
87
+ } catch {
88
+ return empty;
89
+ }
90
+ }
91
+ /** Fetch entries for a specific merkle tree. */
92
+ async getTreeEntries(treeId, options) {
93
+ const empty = {
94
+ data: [],
95
+ total: 0,
96
+ page: 1,
97
+ limit: 50,
98
+ totalPages: 0
99
+ };
100
+ try {
101
+ const params = new URLSearchParams();
102
+ if (options?.page) params.set("page", String(options.page));
103
+ if (options?.limit) params.set("limit", String(options.limit));
104
+ const qs = params.toString();
105
+ const res = await fetch(
106
+ `${this.apiUrl}/trees/${treeId}/entries${qs ? `?${qs}` : ""}`
107
+ );
108
+ if (!res.ok) return empty;
109
+ const json = await res.json();
110
+ return {
111
+ data: json.data ?? [],
112
+ total: json.total ?? 0,
113
+ page: json.page ?? 1,
114
+ limit: json.limit ?? 50,
115
+ totalPages: json.totalPages ?? 0
116
+ };
117
+ } catch {
118
+ return empty;
119
+ }
120
+ }
121
+ /** Fetch a merkle proof for a player address. */
122
+ async getProof(treeId, playerAddress) {
123
+ try {
124
+ const res = await fetch(
125
+ `${this.apiUrl}/trees/${treeId}/proof/${playerAddress.toLowerCase()}`
126
+ );
127
+ if (!res.ok) return null;
128
+ return await res.json();
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+ /** Store a merkle tree in the API (after on-chain registration). */
134
+ async storeTree(request) {
135
+ const res = await fetch(`${this.apiUrl}/trees`, {
136
+ method: "POST",
137
+ headers: { "Content-Type": "application/json" },
138
+ body: JSON.stringify(request)
139
+ });
140
+ if (!res.ok) {
141
+ const text = await res.text();
142
+ throw new Error(`Failed to create merkle tree: ${text}`);
143
+ }
144
+ return await res.json();
145
+ }
146
+ /**
147
+ * Build a merkle tree locally and return the calldata needed
148
+ * to register it on-chain via the validator contract.
149
+ *
150
+ * The consumer is responsible for executing the call with their
151
+ * own account/signer.
152
+ */
153
+ buildTreeCalldata(entries, validatorAddress) {
154
+ const tree = buildMerkleTree(entries);
155
+ return {
156
+ tree,
157
+ call: {
158
+ contractAddress: validatorAddress,
159
+ entrypoint: "create_tree",
160
+ calldata: [tree.root]
161
+ }
162
+ };
163
+ }
164
+ /**
165
+ * Parse the tree ID from transaction receipt events.
166
+ * Returns null if the event cannot be found.
167
+ */
168
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
169
+ parseTreeIdFromEvents(events, validatorAddress) {
170
+ const validatorBigInt = BigInt(validatorAddress);
171
+ const treeEvent = events.find((e) => {
172
+ try {
173
+ return BigInt(e.from_address) === validatorBigInt;
174
+ } catch {
175
+ return false;
176
+ }
177
+ });
178
+ if (!treeEvent?.keys?.[1]) return null;
179
+ return Number(BigInt(treeEvent.keys[1]));
180
+ }
181
+ /**
182
+ * Store the tree in the API after on-chain registration.
183
+ * Pass the tree ID from the on-chain event along with the original entries.
184
+ */
185
+ async createTree(params) {
186
+ return this.storeTree({
187
+ id: params.treeId,
188
+ name: params.name,
189
+ description: params.description,
190
+ entries: params.entries
191
+ });
192
+ }
193
+ };
194
+ function createMerkleClient(config) {
195
+ return new MerkleClient(config);
196
+ }
197
+
198
+ export { MerkleClient, buildMerkleTree, buildQualification, computeLeafValue, createMerkleClient, findEntry, getProof, verifyProof };
199
+ //# sourceMappingURL=merkle.js.map
200
+ //# sourceMappingURL=merkle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/merkle/merkleTree.ts","../src/merkle/client.ts"],"names":[],"mappings":"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","file":"merkle.js","sourcesContent":["import { StandardMerkleTree } from \"@ericnordelo/strk-merkle-tree\";\nimport { hash } from \"starknet\";\n\nexport interface MerkleEntry {\n address: string;\n count: number;\n}\n\nexport interface MerkleTreeData {\n version: 2;\n createdAt: string;\n entries: MerkleEntry[];\n root: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n tree: any; // StandardMerkleTree dump format\n}\n\n/**\n * Compute the leaf value that gets passed to StandardMerkleTree.\n * This matches the Cairo contract's leaf computation:\n * PedersenTrait::new(0).update(address).update(count).finalize()\n *\n * The StandardMerkleTree then applies its own leaf hashing on top:\n * H(0, value, 1) using PedersenCHasher\n */\nexport function computeLeafValue(address: string, count: number): string {\n const intermediate = hash.computePedersenHash(\"0x0\", address);\n return hash.computePedersenHash(intermediate, \"0x\" + count.toString(16));\n}\n\n/**\n * Build a StandardMerkleTree from entries.\n * Each leaf is a single felt252 value (the pre-hashed leaf value).\n */\nexport function buildMerkleTree(entries: MerkleEntry[]): MerkleTreeData {\n if (entries.length === 0) {\n throw new Error(\"Cannot build merkle tree with no entries\");\n }\n\n const leaves = entries.map((e) => [computeLeafValue(e.address, e.count)]);\n\n const tree = StandardMerkleTree.of(leaves, [\"felt252\"], {\n sortLeaves: true,\n });\n\n return {\n version: 2,\n createdAt: new Date().toISOString(),\n entries,\n root: tree.root,\n tree: tree.dump(),\n };\n}\n\n/**\n * Load a tree from saved data and get the proof for an address.\n */\nexport function getProof(\n treeData: MerkleTreeData,\n address: string,\n count: number,\n): string[] | null {\n const tree = StandardMerkleTree.load(treeData.tree);\n const leafValue = computeLeafValue(address, count);\n\n for (const [index, leaf] of tree.entries()) {\n if (BigInt(leaf[0] as string) === BigInt(leafValue)) {\n return tree.getProof(index);\n }\n }\n\n return null;\n}\n\n/**\n * Verify a proof against a root (client-side).\n */\nexport function verifyProof(\n treeData: MerkleTreeData,\n address: string,\n count: number,\n proof: string[],\n): boolean {\n const leafValue = computeLeafValue(address, count);\n return StandardMerkleTree.verify(\n treeData.root,\n [\"felt252\"],\n [leafValue],\n proof,\n );\n}\n\n/**\n * Build qualification array for contract calls: [count, ...proof]\n */\nexport function buildQualification(count: number, proof: string[]): string[] {\n return [\"0x\" + count.toString(16), ...proof];\n}\n\n/**\n * Find an entry in the tree by address.\n */\nexport function findEntry(\n treeData: MerkleTreeData,\n address: string,\n): MerkleEntry | null {\n return (\n treeData.entries.find(\n (e) =>\n e.address.toLowerCase() === address.toLowerCase() ||\n BigInt(e.address) === BigInt(address),\n ) || null\n );\n}\n","/**\n * MerkleClient — chainId-aware client for the merkle API service.\n *\n * Resolves the API URL once at construction time so callers never\n * need to pass chainId on every request.\n */\n\nimport { buildMerkleTree } from \"./merkleTree\";\nimport type { MerkleEntry } from \"./merkleTree\";\n\n// ── Default API URLs per chain ──────────────────────────────────────────────\n\nconst DEFAULT_MERKLE_API_URLS: Record<string, string> = {\n SN_SEPOLIA: \"https://merkle-api-sepolia.up.railway.app\",\n SN_MAIN: \"https://merkle-api.up.railway.app\",\n};\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\nexport interface MerkleClientConfig {\n /** Starknet chain ID, e.g. \"SN_MAIN\" or \"SN_SEPOLIA\" */\n chainId: string;\n /** Override the default API URL for this chain */\n apiUrl?: string;\n}\n\nexport interface MerkleTree {\n id: number;\n name: string;\n description: string;\n root: string;\n entryCount: number;\n createdAt: string;\n}\n\nexport interface MerkleTreesResponse {\n data: MerkleTree[];\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n}\n\nexport interface MerkleProofResponse {\n address: string;\n count: number;\n proof: string[];\n qualification: string[];\n}\n\nexport interface MerkleTreeEntry {\n address: string;\n count: number;\n}\n\nexport interface MerkleTreeEntriesResponse {\n data: MerkleTreeEntry[];\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n}\n\nexport interface CreateMerkleTreeRequest {\n id: number;\n name: string;\n description: string;\n entries: MerkleEntry[];\n}\n\nexport interface CreateMerkleTreeResponse {\n id: number;\n root: string;\n entryCount: number;\n}\n\nexport interface TreeCalldata {\n tree: ReturnType<typeof buildMerkleTree>;\n call: {\n contractAddress: string;\n entrypoint: string;\n calldata: string[];\n };\n}\n\n// ── Client ──────────────────────────────────────────────────────────────────\n\nexport class MerkleClient {\n readonly apiUrl: string;\n\n constructor(config: MerkleClientConfig) {\n this.apiUrl =\n config.apiUrl ??\n DEFAULT_MERKLE_API_URLS[config.chainId] ??\n DEFAULT_MERKLE_API_URLS.SN_MAIN;\n }\n\n /** Fetch paginated list of merkle trees. */\n async getTrees(options?: {\n page?: number;\n limit?: number;\n }): Promise<MerkleTreesResponse> {\n const empty: MerkleTreesResponse = {\n data: [],\n total: 0,\n page: 1,\n limit: 20,\n totalPages: 0,\n };\n try {\n const params = new URLSearchParams();\n if (options?.page) params.set(\"page\", String(options.page));\n if (options?.limit) params.set(\"limit\", String(options.limit));\n const qs = params.toString();\n const res = await fetch(`${this.apiUrl}/trees${qs ? `?${qs}` : \"\"}`);\n if (!res.ok) return empty;\n const json = await res.json();\n return {\n data: json.data ?? [],\n total: json.total ?? 0,\n page: json.page ?? 1,\n limit: json.limit ?? 20,\n totalPages: json.totalPages ?? 0,\n };\n } catch {\n return empty;\n }\n }\n\n /** Fetch entries for a specific merkle tree. */\n async getTreeEntries(\n treeId: string | number,\n options?: { page?: number; limit?: number },\n ): Promise<MerkleTreeEntriesResponse> {\n const empty: MerkleTreeEntriesResponse = {\n data: [],\n total: 0,\n page: 1,\n limit: 50,\n totalPages: 0,\n };\n try {\n const params = new URLSearchParams();\n if (options?.page) params.set(\"page\", String(options.page));\n if (options?.limit) params.set(\"limit\", String(options.limit));\n const qs = params.toString();\n const res = await fetch(\n `${this.apiUrl}/trees/${treeId}/entries${qs ? `?${qs}` : \"\"}`,\n );\n if (!res.ok) return empty;\n const json = await res.json();\n return {\n data: json.data ?? [],\n total: json.total ?? 0,\n page: json.page ?? 1,\n limit: json.limit ?? 50,\n totalPages: json.totalPages ?? 0,\n };\n } catch {\n return empty;\n }\n }\n\n /** Fetch a merkle proof for a player address. */\n async getProof(\n treeId: string | number,\n playerAddress: string,\n ): Promise<MerkleProofResponse | null> {\n try {\n const res = await fetch(\n `${this.apiUrl}/trees/${treeId}/proof/${playerAddress.toLowerCase()}`,\n );\n if (!res.ok) return null;\n return await res.json();\n } catch {\n return null;\n }\n }\n\n /** Store a merkle tree in the API (after on-chain registration). */\n async storeTree(\n request: CreateMerkleTreeRequest,\n ): Promise<CreateMerkleTreeResponse> {\n const res = await fetch(`${this.apiUrl}/trees`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(request),\n });\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to create merkle tree: ${text}`);\n }\n return await res.json();\n }\n\n /**\n * Build a merkle tree locally and return the calldata needed\n * to register it on-chain via the validator contract.\n *\n * The consumer is responsible for executing the call with their\n * own account/signer.\n */\n buildTreeCalldata(\n entries: MerkleEntry[],\n validatorAddress: string,\n ): TreeCalldata {\n const tree = buildMerkleTree(entries);\n return {\n tree,\n call: {\n contractAddress: validatorAddress,\n entrypoint: \"create_tree\",\n calldata: [tree.root],\n },\n };\n }\n\n /**\n * Parse the tree ID from transaction receipt events.\n * Returns null if the event cannot be found.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n parseTreeIdFromEvents(events: any[], validatorAddress: string): number | null {\n const validatorBigInt = BigInt(validatorAddress);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const treeEvent = events.find((e: any) => {\n try {\n return BigInt(e.from_address) === validatorBigInt;\n } catch {\n return false;\n }\n });\n if (!treeEvent?.keys?.[1]) return null;\n return Number(BigInt(treeEvent.keys[1]));\n }\n\n /**\n * Store the tree in the API after on-chain registration.\n * Pass the tree ID from the on-chain event along with the original entries.\n */\n async createTree(params: {\n treeId: number;\n name: string;\n description: string;\n entries: MerkleEntry[];\n }): Promise<CreateMerkleTreeResponse> {\n return this.storeTree({\n id: params.treeId,\n name: params.name,\n description: params.description,\n entries: params.entries,\n });\n }\n}\n\n/** Factory function for creating a MerkleClient. */\nexport function createMerkleClient(config: MerkleClientConfig): MerkleClient {\n return new MerkleClient(config);\n}\n"]}
package/dist/react.cjs CHANGED
@@ -1,9 +1,240 @@
1
1
  'use strict';
2
2
 
3
3
  var react = require('react');
4
+ var strkMerkleTree = require('@ericnordelo/strk-merkle-tree');
4
5
  var starknet = require('starknet');
6
+ var jsxRuntime = require('react/jsx-runtime');
5
7
 
6
- // src/react/useDebounce.ts
8
+ // src/react/MetagameProvider.tsx
9
+ function computeLeafValue(address, count) {
10
+ const intermediate = starknet.hash.computePedersenHash("0x0", address);
11
+ return starknet.hash.computePedersenHash(intermediate, "0x" + count.toString(16));
12
+ }
13
+ function buildMerkleTree(entries) {
14
+ if (entries.length === 0) {
15
+ throw new Error("Cannot build merkle tree with no entries");
16
+ }
17
+ const leaves = entries.map((e) => [computeLeafValue(e.address, e.count)]);
18
+ const tree = strkMerkleTree.StandardMerkleTree.of(leaves, ["felt252"], {
19
+ sortLeaves: true
20
+ });
21
+ return {
22
+ version: 2,
23
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
24
+ entries,
25
+ root: tree.root,
26
+ tree: tree.dump()
27
+ };
28
+ }
29
+
30
+ // src/merkle/client.ts
31
+ var DEFAULT_MERKLE_API_URLS = {
32
+ SN_SEPOLIA: "https://merkle-api-sepolia.up.railway.app",
33
+ SN_MAIN: "https://merkle-api.up.railway.app"
34
+ };
35
+ var MerkleClient = class {
36
+ apiUrl;
37
+ constructor(config) {
38
+ this.apiUrl = config.apiUrl ?? DEFAULT_MERKLE_API_URLS[config.chainId] ?? DEFAULT_MERKLE_API_URLS.SN_MAIN;
39
+ }
40
+ /** Fetch paginated list of merkle trees. */
41
+ async getTrees(options) {
42
+ const empty2 = {
43
+ data: [],
44
+ total: 0,
45
+ page: 1,
46
+ limit: 20,
47
+ totalPages: 0
48
+ };
49
+ try {
50
+ const params = new URLSearchParams();
51
+ if (options?.page) params.set("page", String(options.page));
52
+ if (options?.limit) params.set("limit", String(options.limit));
53
+ const qs = params.toString();
54
+ const res = await fetch(`${this.apiUrl}/trees${qs ? `?${qs}` : ""}`);
55
+ if (!res.ok) return empty2;
56
+ const json = await res.json();
57
+ return {
58
+ data: json.data ?? [],
59
+ total: json.total ?? 0,
60
+ page: json.page ?? 1,
61
+ limit: json.limit ?? 20,
62
+ totalPages: json.totalPages ?? 0
63
+ };
64
+ } catch {
65
+ return empty2;
66
+ }
67
+ }
68
+ /** Fetch entries for a specific merkle tree. */
69
+ async getTreeEntries(treeId, options) {
70
+ const empty2 = {
71
+ data: [],
72
+ total: 0,
73
+ page: 1,
74
+ limit: 50,
75
+ totalPages: 0
76
+ };
77
+ try {
78
+ const params = new URLSearchParams();
79
+ if (options?.page) params.set("page", String(options.page));
80
+ if (options?.limit) params.set("limit", String(options.limit));
81
+ const qs = params.toString();
82
+ const res = await fetch(
83
+ `${this.apiUrl}/trees/${treeId}/entries${qs ? `?${qs}` : ""}`
84
+ );
85
+ if (!res.ok) return empty2;
86
+ const json = await res.json();
87
+ return {
88
+ data: json.data ?? [],
89
+ total: json.total ?? 0,
90
+ page: json.page ?? 1,
91
+ limit: json.limit ?? 50,
92
+ totalPages: json.totalPages ?? 0
93
+ };
94
+ } catch {
95
+ return empty2;
96
+ }
97
+ }
98
+ /** Fetch a merkle proof for a player address. */
99
+ async getProof(treeId, playerAddress) {
100
+ try {
101
+ const res = await fetch(
102
+ `${this.apiUrl}/trees/${treeId}/proof/${playerAddress.toLowerCase()}`
103
+ );
104
+ if (!res.ok) return null;
105
+ return await res.json();
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+ /** Store a merkle tree in the API (after on-chain registration). */
111
+ async storeTree(request) {
112
+ const res = await fetch(`${this.apiUrl}/trees`, {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify(request)
116
+ });
117
+ if (!res.ok) {
118
+ const text = await res.text();
119
+ throw new Error(`Failed to create merkle tree: ${text}`);
120
+ }
121
+ return await res.json();
122
+ }
123
+ /**
124
+ * Build a merkle tree locally and return the calldata needed
125
+ * to register it on-chain via the validator contract.
126
+ *
127
+ * The consumer is responsible for executing the call with their
128
+ * own account/signer.
129
+ */
130
+ buildTreeCalldata(entries, validatorAddress) {
131
+ const tree = buildMerkleTree(entries);
132
+ return {
133
+ tree,
134
+ call: {
135
+ contractAddress: validatorAddress,
136
+ entrypoint: "create_tree",
137
+ calldata: [tree.root]
138
+ }
139
+ };
140
+ }
141
+ /**
142
+ * Parse the tree ID from transaction receipt events.
143
+ * Returns null if the event cannot be found.
144
+ */
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ parseTreeIdFromEvents(events, validatorAddress) {
147
+ const validatorBigInt = BigInt(validatorAddress);
148
+ const treeEvent = events.find((e) => {
149
+ try {
150
+ return BigInt(e.from_address) === validatorBigInt;
151
+ } catch {
152
+ return false;
153
+ }
154
+ });
155
+ if (!treeEvent?.keys?.[1]) return null;
156
+ return Number(BigInt(treeEvent.keys[1]));
157
+ }
158
+ /**
159
+ * Store the tree in the API after on-chain registration.
160
+ * Pass the tree ID from the on-chain event along with the original entries.
161
+ */
162
+ async createTree(params) {
163
+ return this.storeTree({
164
+ id: params.treeId,
165
+ name: params.name,
166
+ description: params.description,
167
+ entries: params.entries
168
+ });
169
+ }
170
+ };
171
+
172
+ // src/client.ts
173
+ var MetagameClient = class {
174
+ chainId;
175
+ merkle;
176
+ constructor(config) {
177
+ this.chainId = config.chainId;
178
+ const merkleConfig = {
179
+ chainId: config.chainId,
180
+ apiUrl: config.merkleApiUrl
181
+ };
182
+ this.merkle = new MerkleClient(merkleConfig);
183
+ }
184
+ };
185
+ function createMetagameClient(config) {
186
+ return new MetagameClient(config);
187
+ }
188
+ var MetagameContext = react.createContext(null);
189
+ function MetagameProvider({
190
+ children,
191
+ ...config
192
+ }) {
193
+ const client = react.useMemo(
194
+ () => createMetagameClient(config),
195
+ [config.chainId, config.merkleApiUrl]
196
+ );
197
+ return /* @__PURE__ */ jsxRuntime.jsx(MetagameContext.Provider, { value: client, children });
198
+ }
199
+ function useMetagameClient() {
200
+ const client = react.useContext(MetagameContext);
201
+ if (!client) {
202
+ throw new Error(
203
+ "useMetagameClient must be used within a MetagameProvider"
204
+ );
205
+ }
206
+ return client;
207
+ }
208
+ function useMerkleTrees(options) {
209
+ const client = useMetagameClient();
210
+ const [data, setData] = react.useState(null);
211
+ const [isLoading, setIsLoading] = react.useState(true);
212
+ react.useEffect(() => {
213
+ setIsLoading(true);
214
+ client.merkle.getTrees({ page: options?.page, limit: options?.limit }).then(setData).finally(() => setIsLoading(false));
215
+ }, [client, options?.page, options?.limit]);
216
+ return {
217
+ trees: data?.data ?? [],
218
+ total: data?.total ?? 0,
219
+ totalPages: data?.totalPages ?? 0,
220
+ page: data?.page ?? 1,
221
+ isLoading
222
+ };
223
+ }
224
+ function useMerkleProof(treeId, playerAddress) {
225
+ const client = useMetagameClient();
226
+ const [proof, setProof] = react.useState(null);
227
+ const [isLoading, setIsLoading] = react.useState(false);
228
+ react.useEffect(() => {
229
+ if (!treeId || !playerAddress) {
230
+ setProof(null);
231
+ return;
232
+ }
233
+ setIsLoading(true);
234
+ client.merkle.getProof(treeId, playerAddress).then(setProof).finally(() => setIsLoading(false));
235
+ }, [client, treeId, playerAddress]);
236
+ return { proof, isLoading };
237
+ }
7
238
  function useDebounce(value, delay) {
8
239
  const [debouncedValue, setDebouncedValue] = react.useState(value);
9
240
  react.useEffect(() => {
@@ -702,11 +933,7 @@ var useExtensionQualification = (provider, extensionAddress, contextId, playerAd
702
933
  extensionAddress,
703
934
  contextId,
704
935
  playerAddress,
705
- inputs: qualificationInputs.map((i) => ({
706
- tid: i.tournamentId,
707
- token: i.tokenId,
708
- pos: i.position
709
- }))
936
+ inputs: qualificationInputs.map((i) => ({ id: i.id, proof: i.proof }))
710
937
  });
711
938
  }, [enabled, provider, extensionAddress, contextId, playerAddress, qualificationInputs]);
712
939
  const lastFetchedKey = react.useRef("");
@@ -729,36 +956,26 @@ var useExtensionQualification = (provider, extensionAddress, contextId, playerAd
729
956
  const results = await Promise.all(
730
957
  qualificationInputs.map(async (input) => {
731
958
  try {
732
- const proof = [
733
- input.tournamentId,
734
- input.tokenId,
735
- input.position.toString()
736
- ];
737
959
  const entriesLeft = await getExtensionEntriesLeft(
738
960
  provider,
739
961
  extensionAddress,
740
962
  contextId,
741
963
  playerAddress,
742
- proof
964
+ input.proof
743
965
  );
744
966
  if (entriesLeft !== null && entriesLeft > 0) {
745
967
  return {
746
- id: `${input.tournamentId}-${input.tokenId}-${input.position}`,
747
- proof,
968
+ id: input.id,
969
+ proof: input.proof,
748
970
  entriesLeft,
749
- label: input.tournamentName,
750
- metadata: {
751
- tournamentId: input.tournamentId,
752
- tournamentName: input.tournamentName,
753
- tokenId: input.tokenId,
754
- position: input.position
755
- }
971
+ label: input.label,
972
+ metadata: input.metadata
756
973
  };
757
974
  }
758
975
  return null;
759
976
  } catch (err) {
760
977
  console.error(
761
- `Error checking qualification for ${input.tournamentId}:`,
978
+ `Error checking qualification ${input.id}:`,
762
979
  err
763
980
  );
764
981
  return null;
@@ -903,10 +1120,14 @@ var useOpusTrovesBannableEntries = (provider, games, config, enabled) => {
903
1120
  };
904
1121
  };
905
1122
 
1123
+ exports.MetagameProvider = MetagameProvider;
906
1124
  exports.useDebounce = useDebounce;
907
1125
  exports.useEntryFeePreview = useEntryFeePreview;
908
1126
  exports.useEntryQualification = useEntryQualification;
909
1127
  exports.useExtensionQualification = useExtensionQualification;
1128
+ exports.useMerkleProof = useMerkleProof;
1129
+ exports.useMerkleTrees = useMerkleTrees;
1130
+ exports.useMetagameClient = useMetagameClient;
910
1131
  exports.useOpusTrovesBannableEntries = useOpusTrovesBannableEntries;
911
1132
  exports.usePagination = usePagination;
912
1133
  exports.usePrizeTable = usePrizeTable;