@openzeppelin/ui-utils 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,2079 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ import { v4 } from "uuid";
4
+ import validator from "validator";
5
+
6
+ //#region src/contractInputs.ts
7
+ /**
8
+ * Returns names of adapter-declared required inputs that are missing/empty in values.
9
+ */
10
+ function getMissingRequiredContractInputs(adapter, values) {
11
+ try {
12
+ const required = (adapter.getContractDefinitionInputs ? adapter.getContractDefinitionInputs() : []).filter((field) => {
13
+ return field?.validation?.required === true;
14
+ });
15
+ const missing = [];
16
+ for (const field of required) {
17
+ const key = field.name || field.id || "";
18
+ const raw = values[key];
19
+ if (raw == null) {
20
+ missing.push(key);
21
+ continue;
22
+ }
23
+ if (typeof raw === "string" && raw.trim().length === 0) missing.push(key);
24
+ }
25
+ return missing;
26
+ } catch {
27
+ return [];
28
+ }
29
+ }
30
+ /**
31
+ * True if any adapter-declared required inputs are missing/empty.
32
+ */
33
+ function hasMissingRequiredContractInputs(adapter, values) {
34
+ if (!adapter) return false;
35
+ return getMissingRequiredContractInputs(adapter, values).length > 0;
36
+ }
37
+
38
+ //#endregion
39
+ //#region src/requiredInputs.ts
40
+ function normalizeSnapshotValue(value) {
41
+ if (value instanceof File) return {
42
+ name: value.name,
43
+ size: value.size,
44
+ lastModified: value.lastModified
45
+ };
46
+ if (typeof value === "string") return value.trim();
47
+ if (value === void 0) return null;
48
+ return value;
49
+ }
50
+ function extractRequiredFields(adapter) {
51
+ if (!adapter || typeof adapter.getContractDefinitionInputs !== "function") return [];
52
+ try {
53
+ return (adapter.getContractDefinitionInputs() || []).filter((field) => field.validation?.required);
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+ /**
59
+ * Builds a snapshot of required form input values.
60
+ * @param adapter - Contract adapter to get field definitions from
61
+ * @param formValues - Current form values
62
+ * @returns Snapshot of required field values, or null if no required fields
63
+ */
64
+ function buildRequiredInputSnapshot(adapter, formValues) {
65
+ if (!formValues) return null;
66
+ const requiredFields = extractRequiredFields(adapter);
67
+ if (requiredFields.length === 0) return null;
68
+ const snapshot = {};
69
+ const values = formValues;
70
+ for (const field of requiredFields) {
71
+ const key = field.name || field.id;
72
+ if (!key) continue;
73
+ snapshot[key] = normalizeSnapshotValue(values[key]);
74
+ }
75
+ return Object.keys(snapshot).length > 0 ? snapshot : null;
76
+ }
77
+ /**
78
+ * Compares two required input snapshots for equality.
79
+ * @param a - First snapshot to compare
80
+ * @param b - Second snapshot to compare
81
+ * @returns True if snapshots are equal, false otherwise
82
+ */
83
+ function requiredSnapshotsEqual(a, b) {
84
+ if (a === b) return true;
85
+ if (!a || !b) return false;
86
+ const keysA = Object.keys(a).sort();
87
+ const keysB = Object.keys(b).sort();
88
+ if (keysA.length !== keysB.length) return false;
89
+ for (let i = 0; i < keysA.length; i += 1) {
90
+ if (keysA[i] !== keysB[i]) return false;
91
+ const valueA = a[keysA[i]];
92
+ const valueB = b[keysA[i]];
93
+ if (typeof valueA === "object" && valueA !== null && typeof valueB === "object" && valueB !== null) {
94
+ if (JSON.stringify(valueA) !== JSON.stringify(valueB)) return false;
95
+ } else if (valueA !== valueB) return false;
96
+ }
97
+ return true;
98
+ }
99
+
100
+ //#endregion
101
+ //#region src/addressNormalization.ts
102
+ /**
103
+ * Normalizes a contract address by trimming whitespace and converting to lowercase.
104
+ * This is useful for case-insensitive and whitespace-insensitive address comparison.
105
+ *
106
+ * @param address - The address to normalize (string, null, or undefined)
107
+ * @returns The normalized address string, or empty string if input is falsy
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * normalizeAddress(' 0xABC123 ') // Returns '0xabc123'
112
+ * normalizeAddress('0xDEF456') // Returns '0xdef456'
113
+ * normalizeAddress(null) // Returns ''
114
+ * normalizeAddress(undefined) // Returns ''
115
+ * ```
116
+ */
117
+ function normalizeAddress(address) {
118
+ if (typeof address === "string") return address.trim().toLowerCase();
119
+ return "";
120
+ }
121
+ /**
122
+ * Compares two addresses after normalization.
123
+ * Returns true if both addresses normalize to the same value.
124
+ *
125
+ * @param address1 - First address to compare
126
+ * @param address2 - Second address to compare
127
+ * @returns True if addresses are equal after normalization
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * addressesEqual(' 0xABC ', '0xabc') // Returns true
132
+ * addressesEqual('0xDEF', '0xABC') // Returns false
133
+ * addressesEqual(null, '') // Returns true
134
+ * ```
135
+ */
136
+ function addressesEqual(address1, address2) {
137
+ return normalizeAddress(address1) === normalizeAddress(address2);
138
+ }
139
+
140
+ //#endregion
141
+ //#region src/logger.ts
142
+ var Logger = class Logger {
143
+ static instance;
144
+ options = {
145
+ enabled: getDefaultLoggerEnabled(),
146
+ level: "debug"
147
+ };
148
+ constructor() {}
149
+ static getInstance() {
150
+ if (!Logger.instance) Logger.instance = new Logger();
151
+ return Logger.instance;
152
+ }
153
+ configure(options) {
154
+ this.options = {
155
+ ...this.options,
156
+ ...options
157
+ };
158
+ }
159
+ shouldLog(level) {
160
+ if (!this.options.enabled) return false;
161
+ const levels = [
162
+ "debug",
163
+ "info",
164
+ "warn",
165
+ "error"
166
+ ];
167
+ const configuredLevelIndex = levels.indexOf(this.options.level);
168
+ return levels.indexOf(level) >= configuredLevelIndex;
169
+ }
170
+ formatMessage(level, system, message) {
171
+ return `[${level.toUpperCase()}][${system}] ${message}`;
172
+ }
173
+ debug(system, message, ...args) {
174
+ if (this.shouldLog("debug")) console.log(this.formatMessage("debug", system, message), ...args);
175
+ }
176
+ info(system, message, ...args) {
177
+ if (this.shouldLog("info")) console.log(this.formatMessage("info", system, message), ...args);
178
+ }
179
+ warn(system, message, ...args) {
180
+ if (this.shouldLog("warn")) console.warn(this.formatMessage("warn", system, message), ...args);
181
+ }
182
+ error(system, message, ...args) {
183
+ if (this.shouldLog("error")) console.error(this.formatMessage("error", system, message), ...args);
184
+ }
185
+ };
186
+ const logger = Logger.getInstance();
187
+ /**
188
+ * Determine whether logging should be enabled by default.
189
+ *
190
+ * - In Vite/browser contexts, use `import.meta.env.DEV`.
191
+ * - In Node/tsup contexts, use `process.env.NODE_ENV`.
192
+ *
193
+ * Defaults to disabled outside development to avoid runtime overhead and noise.
194
+ */
195
+ function getDefaultLoggerEnabled() {
196
+ try {
197
+ const viteEnv = import.meta.env;
198
+ if (viteEnv) {
199
+ const exportEnv = String(viteEnv.VITE_EXPORT_ENV || "").toLowerCase();
200
+ if (exportEnv === "staging" || exportEnv === "production") return false;
201
+ if (typeof viteEnv.DEV === "boolean") return viteEnv.DEV;
202
+ }
203
+ } catch {}
204
+ if (typeof process !== "undefined" && typeof process.env !== "undefined") {
205
+ const exportEnv = String(process.env.VITE_EXPORT_ENV || "").toLowerCase();
206
+ if (exportEnv === "staging" || exportEnv === "production") return false;
207
+ const nodeEnv = process.env.NODE_ENV;
208
+ return nodeEnv === "development" || nodeEnv === "test";
209
+ }
210
+ return false;
211
+ }
212
+
213
+ //#endregion
214
+ //#region src/AppConfigService.ts
215
+ const VITE_ENV_PREFIX = "VITE_APP_CFG_";
216
+ const LOG_SYSTEM = "AppConfigService";
217
+ /**
218
+ * AppConfigService
219
+ *
220
+ * Responsible for loading, merging, and providing access to the application's
221
+ * runtime configuration (`AppRuntimeConfig`).
222
+ */
223
+ var AppConfigService = class {
224
+ config;
225
+ isInitialized = false;
226
+ /**
227
+ * Creates a new AppConfigService with default configuration.
228
+ */
229
+ constructor() {
230
+ this.config = {
231
+ networkServiceConfigs: {},
232
+ globalServiceConfigs: {},
233
+ rpcEndpoints: {},
234
+ indexerEndpoints: {},
235
+ featureFlags: {},
236
+ defaultLanguage: "en"
237
+ };
238
+ logger.info(LOG_SYSTEM, "Service initialized with default configuration.");
239
+ }
240
+ loadFromViteEnvironment(envSource) {
241
+ logger.debug(LOG_SYSTEM, "BEGIN loadFromViteEnvironment. envSource received:", envSource ? JSON.stringify(envSource) : "undefined");
242
+ if (typeof envSource === "undefined") {
243
+ logger.warn(LOG_SYSTEM, "Vite environment object (envSource) was undefined. Skipping Vite env load.");
244
+ return;
245
+ }
246
+ const env = envSource;
247
+ const loadedNetworkServiceConfigs = {};
248
+ const loadedGlobalServiceConfigs = {};
249
+ const loadedRpcEndpoints = {};
250
+ const loadedIndexerEndpoints = {};
251
+ const loadedFeatureFlags = {};
252
+ for (const key in env) if (Object.prototype.hasOwnProperty.call(env, key) && env[key] !== void 0) {
253
+ const value = String(env[key]);
254
+ if (key.startsWith(`${VITE_ENV_PREFIX}API_KEY_`)) {
255
+ const serviceIdentifier = key.substring(`${VITE_ENV_PREFIX}API_KEY_`.length).toLowerCase().replace(/_/g, "-");
256
+ if (!loadedNetworkServiceConfigs[serviceIdentifier]) loadedNetworkServiceConfigs[serviceIdentifier] = {};
257
+ loadedNetworkServiceConfigs[serviceIdentifier].apiKey = value;
258
+ } else if (key.startsWith(`${VITE_ENV_PREFIX}SERVICE_`)) {
259
+ const fullSuffix = key.substring(`${VITE_ENV_PREFIX}SERVICE_`.length);
260
+ const firstUnderscoreIndex = fullSuffix.indexOf("_");
261
+ if (firstUnderscoreIndex > 0 && firstUnderscoreIndex < fullSuffix.length - 1) {
262
+ const serviceName = fullSuffix.substring(0, firstUnderscoreIndex).toLowerCase();
263
+ const paramName = fullSuffix.substring(firstUnderscoreIndex + 1).toLowerCase().replace(/_([a-z])/g, (g) => g[1].toUpperCase());
264
+ if (serviceName && paramName) {
265
+ if (!loadedGlobalServiceConfigs[serviceName]) loadedGlobalServiceConfigs[serviceName] = {};
266
+ loadedGlobalServiceConfigs[serviceName][paramName] = value;
267
+ logger.debug(LOG_SYSTEM, `Parsed service: '${serviceName}', param: '${paramName}', value: '${value}' from key: ${key}`);
268
+ } else logger.warn(LOG_SYSTEM, `Could not effectively parse service/param from key: ${key}`);
269
+ } else logger.warn(LOG_SYSTEM, `Could not determine service and param from key (missing underscore separator): ${key}`);
270
+ } else if (key === `${VITE_ENV_PREFIX}WALLETCONNECT_PROJECT_ID`) {
271
+ if (!loadedGlobalServiceConfigs.walletconnect) loadedGlobalServiceConfigs.walletconnect = {};
272
+ loadedGlobalServiceConfigs.walletconnect.projectId = value;
273
+ logger.debug(LOG_SYSTEM, `Parsed WalletConnect Project ID directly from key: ${key}, value: ${value}`);
274
+ } else if (key.startsWith(`${VITE_ENV_PREFIX}RPC_ENDPOINT_`)) {
275
+ const networkId = key.substring(`${VITE_ENV_PREFIX}RPC_ENDPOINT_`.length).toLowerCase().replace(/_/g, "-");
276
+ if (networkId) {
277
+ loadedRpcEndpoints[networkId] = value;
278
+ logger.debug(LOG_SYSTEM, `Loaded RPC override for ${networkId}: ${value}`);
279
+ }
280
+ } else if (key.startsWith(`${VITE_ENV_PREFIX}INDEXER_ENDPOINT_`)) {
281
+ const networkId = key.substring(`${VITE_ENV_PREFIX}INDEXER_ENDPOINT_`.length).toLowerCase().replace(/_/g, "-");
282
+ if (networkId) {
283
+ loadedIndexerEndpoints[networkId] = value;
284
+ logger.debug(LOG_SYSTEM, `Loaded indexer endpoint for ${networkId}: ${value}`);
285
+ }
286
+ } else if (key.startsWith(`${VITE_ENV_PREFIX}FEATURE_FLAG_`)) {
287
+ const flagName = key.substring(`${VITE_ENV_PREFIX}FEATURE_FLAG_`.length).toLowerCase();
288
+ loadedFeatureFlags[flagName] = value.toLowerCase() === "true";
289
+ } else if (key === `${VITE_ENV_PREFIX}DEFAULT_LANGUAGE`) this.config.defaultLanguage = value;
290
+ }
291
+ this.config.networkServiceConfigs = {
292
+ ...this.config.networkServiceConfigs,
293
+ ...loadedNetworkServiceConfigs
294
+ };
295
+ if (Object.keys(loadedGlobalServiceConfigs).length > 0) {
296
+ if (!this.config.globalServiceConfigs) this.config.globalServiceConfigs = {};
297
+ for (const serviceKeyInLoaded in loadedGlobalServiceConfigs) if (Object.prototype.hasOwnProperty.call(loadedGlobalServiceConfigs, serviceKeyInLoaded)) this.config.globalServiceConfigs[serviceKeyInLoaded] = {
298
+ ...this.config.globalServiceConfigs[serviceKeyInLoaded] || {},
299
+ ...loadedGlobalServiceConfigs[serviceKeyInLoaded]
300
+ };
301
+ }
302
+ if (Object.keys(loadedRpcEndpoints).length > 0) {
303
+ if (!this.config.rpcEndpoints) this.config.rpcEndpoints = {};
304
+ for (const networkKey in loadedRpcEndpoints) if (Object.prototype.hasOwnProperty.call(loadedRpcEndpoints, networkKey)) this.config.rpcEndpoints[networkKey] = loadedRpcEndpoints[networkKey];
305
+ }
306
+ if (Object.keys(loadedIndexerEndpoints).length > 0) {
307
+ if (!this.config.indexerEndpoints) this.config.indexerEndpoints = {};
308
+ for (const networkKey in loadedIndexerEndpoints) if (Object.prototype.hasOwnProperty.call(loadedIndexerEndpoints, networkKey)) this.config.indexerEndpoints[networkKey] = loadedIndexerEndpoints[networkKey];
309
+ }
310
+ this.config.featureFlags = {
311
+ ...this.config.featureFlags,
312
+ ...loadedFeatureFlags
313
+ };
314
+ logger.info(LOG_SYSTEM, "Resolved globalServiceConfigs after Vite env processing:", this.config.globalServiceConfigs ? JSON.stringify(this.config.globalServiceConfigs) : "undefined");
315
+ logger.info(LOG_SYSTEM, "Resolved rpcEndpoints after Vite env processing:", this.config.rpcEndpoints ? JSON.stringify(this.config.rpcEndpoints) : "undefined");
316
+ logger.info(LOG_SYSTEM, "Configuration loaded/merged from provided Vite environment variables.");
317
+ }
318
+ async loadFromJson(filePath = "/app.config.json") {
319
+ try {
320
+ const response = await fetch(filePath);
321
+ if (!response.ok) {
322
+ if (response.status === 404) logger.info(LOG_SYSTEM, `Optional configuration file not found at ${filePath}. Skipping.`);
323
+ else logger.warn(LOG_SYSTEM, `Failed to fetch config from ${filePath}: ${response.status} ${response.statusText}`);
324
+ return;
325
+ }
326
+ const externalConfig = await response.json();
327
+ if (externalConfig.networkServiceConfigs) {
328
+ if (!this.config.networkServiceConfigs) this.config.networkServiceConfigs = {};
329
+ for (const key in externalConfig.networkServiceConfigs) if (Object.prototype.hasOwnProperty.call(externalConfig.networkServiceConfigs, key)) this.config.networkServiceConfigs[key] = {
330
+ ...this.config.networkServiceConfigs[key] || {},
331
+ ...externalConfig.networkServiceConfigs[key]
332
+ };
333
+ }
334
+ if (externalConfig.globalServiceConfigs) {
335
+ if (!this.config.globalServiceConfigs) this.config.globalServiceConfigs = {};
336
+ for (const serviceKey in externalConfig.globalServiceConfigs) if (Object.prototype.hasOwnProperty.call(externalConfig.globalServiceConfigs, serviceKey)) this.config.globalServiceConfigs[serviceKey] = {
337
+ ...this.config.globalServiceConfigs[serviceKey] || {},
338
+ ...externalConfig.globalServiceConfigs[serviceKey]
339
+ };
340
+ }
341
+ if (externalConfig.rpcEndpoints) {
342
+ if (!this.config.rpcEndpoints) this.config.rpcEndpoints = {};
343
+ for (const networkKey in externalConfig.rpcEndpoints) if (Object.prototype.hasOwnProperty.call(externalConfig.rpcEndpoints, networkKey)) this.config.rpcEndpoints[networkKey] = externalConfig.rpcEndpoints[networkKey];
344
+ }
345
+ if (externalConfig.indexerEndpoints) {
346
+ if (!this.config.indexerEndpoints) this.config.indexerEndpoints = {};
347
+ for (const networkKey in externalConfig.indexerEndpoints) if (Object.prototype.hasOwnProperty.call(externalConfig.indexerEndpoints, networkKey)) this.config.indexerEndpoints[networkKey] = externalConfig.indexerEndpoints[networkKey];
348
+ }
349
+ if (externalConfig.featureFlags) this.config.featureFlags = {
350
+ ...this.config.featureFlags || {},
351
+ ...externalConfig.featureFlags
352
+ };
353
+ if (typeof externalConfig.defaultLanguage === "string") this.config.defaultLanguage = externalConfig.defaultLanguage;
354
+ logger.info(LOG_SYSTEM, `Configuration loaded/merged from ${filePath}`);
355
+ } catch (error) {
356
+ logger.error(LOG_SYSTEM, `Error loading or parsing config from ${filePath}:`, error);
357
+ }
358
+ }
359
+ /**
360
+ * Initializes the service by loading configuration from the specified strategies.
361
+ * @param strategies - Array of configuration loading strategies to apply
362
+ */
363
+ async initialize(strategies) {
364
+ logger.info(LOG_SYSTEM, "Initialization sequence started with strategies:", strategies);
365
+ for (const strategy of strategies) if (strategy.type === "viteEnv") this.loadFromViteEnvironment(strategy.env);
366
+ else if (strategy.type === "json") await this.loadFromJson(strategy.path);
367
+ this.isInitialized = true;
368
+ logger.info(LOG_SYSTEM, "Initialization complete.");
369
+ }
370
+ /**
371
+ * Gets the API key for a specific explorer service.
372
+ * @param serviceIdentifier - The service identifier
373
+ * @returns The API key if configured, undefined otherwise
374
+ */
375
+ getExplorerApiKey(serviceIdentifier) {
376
+ if (!this.isInitialized) logger.warn(LOG_SYSTEM, "getExplorerApiKey called before initialization.");
377
+ return this.config.networkServiceConfigs?.[serviceIdentifier]?.apiKey;
378
+ }
379
+ /**
380
+ * Gets the configuration for a global service.
381
+ * @param serviceName - The name of the service
382
+ * @returns The service configuration if found, undefined otherwise
383
+ */
384
+ getGlobalServiceConfig(serviceName) {
385
+ if (!this.isInitialized) logger.warn(LOG_SYSTEM, "getGlobalServiceConfig called before initialization.");
386
+ return this.config.globalServiceConfigs?.[serviceName];
387
+ }
388
+ /**
389
+ * Checks if a feature flag is enabled.
390
+ * @param flagName - The name of the feature flag
391
+ * @returns True if the feature is enabled, false otherwise
392
+ */
393
+ isFeatureEnabled(flagName) {
394
+ if (!this.isInitialized) logger.warn(LOG_SYSTEM, "isFeatureEnabled called before initialization.");
395
+ return this.config.featureFlags?.[flagName.toLowerCase()] ?? false;
396
+ }
397
+ /**
398
+ * Gets a global service parameter value.
399
+ * @param serviceName The name of the service
400
+ * @param paramName The name of the parameter
401
+ * @returns The parameter value (can be any type including objects, arrays) or undefined if not found
402
+ */
403
+ getGlobalServiceParam(serviceName, paramName) {
404
+ if (!this.isInitialized) {
405
+ logger.warn(LOG_SYSTEM, "getGlobalServiceParam called before initialization.");
406
+ return;
407
+ }
408
+ return (this.config.globalServiceConfigs?.[serviceName.toLowerCase()])?.[paramName];
409
+ }
410
+ /**
411
+ * Gets the RPC endpoint override for a specific network.
412
+ * @param networkId - The network identifier
413
+ * @returns The RPC endpoint configuration if found, undefined otherwise
414
+ */
415
+ getRpcEndpointOverride(networkId) {
416
+ if (!this.isInitialized) logger.warn(LOG_SYSTEM, "getRpcEndpointOverride called before initialization.");
417
+ return this.config.rpcEndpoints?.[networkId];
418
+ }
419
+ /**
420
+ * Get the indexer endpoint override for a specific network.
421
+ * Indexer endpoints are used for querying historical blockchain data.
422
+ * @param networkId The network identifier (e.g., 'stellar-testnet')
423
+ * @returns The indexer endpoint configuration, or undefined if not configured
424
+ */
425
+ getIndexerEndpointOverride(networkId) {
426
+ if (!this.isInitialized) logger.warn(LOG_SYSTEM, "getIndexerEndpointOverride called before initialization.");
427
+ return this.config.indexerEndpoints?.[networkId];
428
+ }
429
+ /**
430
+ * Returns the entire configuration object.
431
+ * Primarily for debugging or for parts of the app that need a broader view.
432
+ * Use specific getters like `getExplorerApiKey` or `isFeatureEnabled` where possible.
433
+ */
434
+ getConfig() {
435
+ return this.config;
436
+ }
437
+ /**
438
+ * Gets a nested configuration object with type safety.
439
+ *
440
+ * This is a helper method to safely access complex nested configuration objects
441
+ * with proper TypeScript type checking.
442
+ *
443
+ * @param serviceName The name of the service (e.g., 'walletui')
444
+ * @param paramName The parameter name that contains the nested object (e.g., 'config')
445
+ * Pass an empty string to get the entire service configuration.
446
+ * @returns The typed nested configuration object or undefined if not found
447
+ *
448
+ * @example
449
+ * // Get a typed UI kit configuration:
450
+ * const uiConfig = appConfigService.getTypedNestedConfig<UiKitConfiguration>('walletui', 'config');
451
+ * if (uiConfig) {
452
+ * console.log(uiConfig.kitName); // Properly typed
453
+ * }
454
+ *
455
+ * // Get entire service configuration:
456
+ * const allAnalytics = appConfigService.getTypedNestedConfig<AnalyticsConfig>('analytics', '');
457
+ */
458
+ getTypedNestedConfig(serviceName, paramName) {
459
+ if (!this.isInitialized) {
460
+ logger.warn(LOG_SYSTEM, "getTypedNestedConfig called before initialization.");
461
+ return;
462
+ }
463
+ try {
464
+ if (paramName === "") {
465
+ const serviceConfig = this.config.globalServiceConfigs?.[serviceName.toLowerCase()];
466
+ if (serviceConfig && typeof serviceConfig === "object") return serviceConfig;
467
+ return;
468
+ }
469
+ const param = this.getGlobalServiceParam(serviceName, paramName);
470
+ if (param && typeof param === "object") return param;
471
+ return;
472
+ } catch (error) {
473
+ logger.warn(LOG_SYSTEM, `Error accessing nested configuration for ${serviceName}.${paramName}:`, error);
474
+ return;
475
+ }
476
+ }
477
+ /**
478
+ * Checks if a nested configuration exists and has a specific property.
479
+ *
480
+ * @param serviceName The name of the service
481
+ * @param paramName The parameter name containing the nested object
482
+ * @param propName The property name to check for
483
+ * @returns True if the property exists in the nested configuration
484
+ *
485
+ * @example
486
+ * if (appConfigService.hasNestedConfigProperty('walletui', 'config', 'showInjectedConnector')) {
487
+ * // Do something when the property exists
488
+ * }
489
+ */
490
+ hasNestedConfigProperty(serviceName, paramName, propName) {
491
+ const nestedConfig = this.getTypedNestedConfig(serviceName, paramName);
492
+ return nestedConfig !== void 0 && Object.prototype.hasOwnProperty.call(nestedConfig, propName);
493
+ }
494
+ /**
495
+ * Gets wallet UI configuration for a specific ecosystem.
496
+ * Uses ecosystem-namespaced format with optional default fallback.
497
+ *
498
+ * @param ecosystemId The ecosystem ID (e.g., 'stellar', 'evm', 'solana')
499
+ * @returns The wallet UI configuration for the ecosystem, or undefined
500
+ *
501
+ * @example
502
+ * Configuration format:
503
+ * {
504
+ * "globalServiceConfigs": {
505
+ * "walletui": {
506
+ * "stellar": { "kitName": "stellar-wallets-kit", "kitConfig": {} },
507
+ * "evm": { "kitName": "rainbowkit", "kitConfig": {} },
508
+ * "default": { "kitName": "custom", "kitConfig": {} }
509
+ * }
510
+ * }
511
+ * }
512
+ * const stellarConfig = appConfigService.getWalletUIConfig('stellar');
513
+ */
514
+ getWalletUIConfig(ecosystemId) {
515
+ if (!this.isInitialized) {
516
+ logger.warn(LOG_SYSTEM, "getWalletUIConfig called before initialization.");
517
+ return;
518
+ }
519
+ try {
520
+ const walletUIService = this.config.globalServiceConfigs?.walletui;
521
+ if (!walletUIService) return;
522
+ if (ecosystemId && walletUIService[ecosystemId] && typeof walletUIService[ecosystemId] === "object") {
523
+ logger.debug(LOG_SYSTEM, `Found ecosystem-specific wallet UI config for ${ecosystemId}`);
524
+ return walletUIService[ecosystemId];
525
+ }
526
+ if (walletUIService.default && typeof walletUIService.default === "object") {
527
+ logger.debug(LOG_SYSTEM, `Using default wallet UI config for ecosystem ${ecosystemId}`);
528
+ return walletUIService.default;
529
+ }
530
+ return;
531
+ } catch (error) {
532
+ logger.warn(LOG_SYSTEM, `Error accessing wallet UI configuration for ecosystem ${ecosystemId}:`, error);
533
+ return;
534
+ }
535
+ }
536
+ };
537
+ const appConfigService = new AppConfigService();
538
+
539
+ //#endregion
540
+ //#region src/UserRpcConfigService.ts
541
+ /**
542
+ * Service for managing user-configured RPC endpoints.
543
+ * Stores RPC configurations in localStorage for persistence across sessions.
544
+ */
545
+ var UserRpcConfigService = class {
546
+ static STORAGE_PREFIX = "tfb_rpc_config_";
547
+ static eventListeners = /* @__PURE__ */ new Map();
548
+ /**
549
+ * Emits an RPC configuration event to all registered listeners
550
+ */
551
+ static emitEvent(event) {
552
+ const listeners = this.eventListeners.get(event.networkId) || /* @__PURE__ */ new Set();
553
+ const globalListeners = this.eventListeners.get("*") || /* @__PURE__ */ new Set();
554
+ [...listeners, ...globalListeners].forEach((listener) => {
555
+ try {
556
+ listener(event);
557
+ } catch (error) {
558
+ logger.error("UserRpcConfigService", "Error in event listener:", error);
559
+ }
560
+ });
561
+ }
562
+ /**
563
+ * Subscribes to RPC configuration changes for a specific network or all networks
564
+ * @param networkId The network identifier or '*' for all networks
565
+ * @param listener The callback to invoke when RPC config changes
566
+ * @returns Unsubscribe function
567
+ */
568
+ static subscribe(networkId, listener) {
569
+ if (!this.eventListeners.has(networkId)) this.eventListeners.set(networkId, /* @__PURE__ */ new Set());
570
+ this.eventListeners.get(networkId).add(listener);
571
+ return () => {
572
+ const listeners = this.eventListeners.get(networkId);
573
+ if (listeners) {
574
+ listeners.delete(listener);
575
+ if (listeners.size === 0) this.eventListeners.delete(networkId);
576
+ }
577
+ };
578
+ }
579
+ /**
580
+ * Saves a user RPC configuration for a specific network.
581
+ * @param networkId The network identifier
582
+ * @param config The RPC configuration to save
583
+ */
584
+ static saveUserRpcConfig(networkId, config) {
585
+ try {
586
+ const storageKey = `${this.STORAGE_PREFIX}${networkId}`;
587
+ localStorage.setItem(storageKey, JSON.stringify(config));
588
+ logger.info("UserRpcConfigService", `Saved RPC config for network ${networkId}`);
589
+ this.emitEvent({
590
+ type: "rpc-config-changed",
591
+ networkId,
592
+ config
593
+ });
594
+ } catch (error) {
595
+ logger.error("UserRpcConfigService", "Failed to save RPC config:", error);
596
+ }
597
+ }
598
+ /**
599
+ * Retrieves a user RPC configuration for a specific network.
600
+ * @param networkId The network identifier
601
+ * @returns The stored configuration or null if not found
602
+ */
603
+ static getUserRpcConfig(networkId) {
604
+ try {
605
+ const storageKey = `${this.STORAGE_PREFIX}${networkId}`;
606
+ const stored = localStorage.getItem(storageKey);
607
+ if (!stored) return null;
608
+ const config = JSON.parse(stored);
609
+ logger.info("UserRpcConfigService", `Retrieved RPC config for network ${networkId}`);
610
+ return config;
611
+ } catch (error) {
612
+ logger.error("UserRpcConfigService", "Failed to retrieve RPC config:", error);
613
+ return null;
614
+ }
615
+ }
616
+ /**
617
+ * Clears a user RPC configuration for a specific network.
618
+ * @param networkId The network identifier
619
+ */
620
+ static clearUserRpcConfig(networkId) {
621
+ try {
622
+ const storageKey = `${this.STORAGE_PREFIX}${networkId}`;
623
+ localStorage.removeItem(storageKey);
624
+ logger.info("UserRpcConfigService", `Cleared RPC config for network ${networkId}`);
625
+ this.emitEvent({
626
+ type: "rpc-config-cleared",
627
+ networkId
628
+ });
629
+ } catch (error) {
630
+ logger.error("UserRpcConfigService", "Failed to clear RPC config:", error);
631
+ }
632
+ }
633
+ /**
634
+ * Clears all user RPC configurations.
635
+ */
636
+ static clearAllUserRpcConfigs() {
637
+ try {
638
+ const keysToRemove = [];
639
+ for (let i = 0; i < localStorage.length; i++) {
640
+ const key = localStorage.key(i);
641
+ if (key?.startsWith(this.STORAGE_PREFIX)) keysToRemove.push(key);
642
+ }
643
+ keysToRemove.forEach((key) => localStorage.removeItem(key));
644
+ logger.info("UserRpcConfigService", `Cleared ${keysToRemove.length} RPC configs`);
645
+ } catch (error) {
646
+ logger.error("UserRpcConfigService", "Failed to clear all RPC configs:", error);
647
+ }
648
+ }
649
+ };
650
+ const userRpcConfigService = UserRpcConfigService;
651
+
652
+ //#endregion
653
+ //#region src/UserExplorerConfigService.ts
654
+ /**
655
+ * Service for managing user-configured block explorer endpoints and API keys.
656
+ * Stores explorer configurations in localStorage for persistence across sessions.
657
+ */
658
+ var UserExplorerConfigService = class {
659
+ static STORAGE_PREFIX = "tfb_explorer_config_";
660
+ static eventListeners = /* @__PURE__ */ new Map();
661
+ /**
662
+ * Emits an explorer configuration event to all registered listeners
663
+ */
664
+ static emitEvent(event) {
665
+ const listeners = this.eventListeners.get(event.networkId) || /* @__PURE__ */ new Set();
666
+ const globalListeners = this.eventListeners.get("*") || /* @__PURE__ */ new Set();
667
+ [...listeners, ...globalListeners].forEach((listener) => {
668
+ try {
669
+ listener(event);
670
+ } catch (error) {
671
+ logger.error("UserExplorerConfigService", "Error in event listener:", error);
672
+ }
673
+ });
674
+ }
675
+ /**
676
+ * Subscribes to explorer configuration changes for a specific network or all networks
677
+ * @param networkId The network identifier or '*' for all networks
678
+ * @param listener The callback to invoke when explorer config changes
679
+ * @returns Unsubscribe function
680
+ */
681
+ static subscribe(networkId, listener) {
682
+ if (!this.eventListeners.has(networkId)) this.eventListeners.set(networkId, /* @__PURE__ */ new Set());
683
+ this.eventListeners.get(networkId).add(listener);
684
+ return () => {
685
+ const listeners = this.eventListeners.get(networkId);
686
+ if (listeners) {
687
+ listeners.delete(listener);
688
+ if (listeners.size === 0) this.eventListeners.delete(networkId);
689
+ }
690
+ };
691
+ }
692
+ /**
693
+ * Saves a user explorer configuration for a specific network.
694
+ * @param networkId The network identifier
695
+ * @param config The explorer configuration to save
696
+ */
697
+ static saveUserExplorerConfig(networkId, config) {
698
+ try {
699
+ const storageKey = `${this.STORAGE_PREFIX}${networkId}`;
700
+ localStorage.setItem(storageKey, JSON.stringify(config));
701
+ logger.info("UserExplorerConfigService", `Saved explorer config for network ${networkId}`);
702
+ this.emitEvent({
703
+ type: "explorer-config-changed",
704
+ networkId,
705
+ config
706
+ });
707
+ } catch (error) {
708
+ logger.error("UserExplorerConfigService", "Failed to save explorer config:", error);
709
+ }
710
+ }
711
+ /**
712
+ * Retrieves a user explorer configuration for a specific network.
713
+ * First checks for global settings, then falls back to network-specific settings.
714
+ * @param networkId The network identifier
715
+ * @returns The stored configuration or null if not found
716
+ */
717
+ static getUserExplorerConfig(networkId) {
718
+ try {
719
+ const globalKey = `${this.STORAGE_PREFIX}__global__`;
720
+ const globalStored = localStorage.getItem(globalKey);
721
+ if (globalStored) {
722
+ const globalConfig = JSON.parse(globalStored);
723
+ if (globalConfig.applyToAllNetworks) {
724
+ logger.info("UserExplorerConfigService", `Using global explorer config for network ${networkId}`);
725
+ return globalConfig;
726
+ }
727
+ }
728
+ const storageKey = `${this.STORAGE_PREFIX}${networkId}`;
729
+ const stored = localStorage.getItem(storageKey);
730
+ if (!stored) return null;
731
+ const config = JSON.parse(stored);
732
+ logger.info("UserExplorerConfigService", `Retrieved explorer config for network ${networkId}`);
733
+ return config;
734
+ } catch (error) {
735
+ logger.error("UserExplorerConfigService", "Failed to retrieve explorer config:", error);
736
+ return null;
737
+ }
738
+ }
739
+ /**
740
+ * Clears a user explorer configuration for a specific network.
741
+ * @param networkId The network identifier
742
+ */
743
+ static clearUserExplorerConfig(networkId) {
744
+ try {
745
+ const storageKey = `${this.STORAGE_PREFIX}${networkId}`;
746
+ localStorage.removeItem(storageKey);
747
+ logger.info("UserExplorerConfigService", `Cleared explorer config for network ${networkId}`);
748
+ this.emitEvent({
749
+ type: "explorer-config-cleared",
750
+ networkId
751
+ });
752
+ } catch (error) {
753
+ logger.error("UserExplorerConfigService", "Failed to clear explorer config:", error);
754
+ }
755
+ }
756
+ /**
757
+ * Clears all user explorer configurations.
758
+ */
759
+ static clearAllUserExplorerConfigs() {
760
+ try {
761
+ const keysToRemove = [];
762
+ for (let i = 0; i < localStorage.length; i++) {
763
+ const key = localStorage.key(i);
764
+ if (key?.startsWith(this.STORAGE_PREFIX)) keysToRemove.push(key);
765
+ }
766
+ keysToRemove.forEach((key) => {
767
+ const networkId = key.substring(this.STORAGE_PREFIX.length);
768
+ localStorage.removeItem(key);
769
+ this.emitEvent({
770
+ type: "explorer-config-cleared",
771
+ networkId
772
+ });
773
+ });
774
+ logger.info("UserExplorerConfigService", `Cleared ${keysToRemove.length} explorer configs`);
775
+ } catch (error) {
776
+ logger.error("UserExplorerConfigService", "Failed to clear all explorer configs:", error);
777
+ }
778
+ }
779
+ /**
780
+ * Gets all network IDs that have explorer configurations.
781
+ * @returns Array of network IDs
782
+ */
783
+ static getConfiguredNetworkIds() {
784
+ try {
785
+ const networkIds = [];
786
+ for (let i = 0; i < localStorage.length; i++) {
787
+ const key = localStorage.key(i);
788
+ if (key?.startsWith(this.STORAGE_PREFIX)) {
789
+ const networkId = key.substring(this.STORAGE_PREFIX.length);
790
+ if (networkId !== "__global__") networkIds.push(networkId);
791
+ }
792
+ }
793
+ return networkIds;
794
+ } catch (error) {
795
+ logger.error("UserExplorerConfigService", "Failed to get configured network IDs:", error);
796
+ return [];
797
+ }
798
+ }
799
+ };
800
+ const userExplorerConfigService = UserExplorerConfigService;
801
+
802
+ //#endregion
803
+ //#region src/UserNetworkServiceConfigService.ts
804
+ /**
805
+ * Service for managing user-defined network service configurations.
806
+ *
807
+ * This service provides a generic, chain-agnostic way to store and retrieve
808
+ * per-network, per-service user configurations.
809
+ *
810
+ * Configurations are stored in localStorage with the key format:
811
+ * `tfb_service_config_{serviceId}__{networkId}`
812
+ *
813
+ * @example
814
+ * ```typescript
815
+ * // Save RPC configuration for Sepolia
816
+ * userNetworkServiceConfigService.save('ethereum-sepolia', 'rpc', {
817
+ * rpcUrl: 'https://sepolia.infura.io/v3/your-key'
818
+ * });
819
+ *
820
+ * // Retrieve configuration
821
+ * const config = userNetworkServiceConfigService.get('ethereum-sepolia', 'rpc');
822
+ *
823
+ * // Subscribe to changes
824
+ * const unsubscribe = userNetworkServiceConfigService.subscribe(
825
+ * 'ethereum-sepolia',
826
+ * 'rpc',
827
+ * (event) => {
828
+ * console.log('Config changed:', event.config);
829
+ * }
830
+ * );
831
+ * ```
832
+ *
833
+ * @class UserNetworkServiceConfigService
834
+ */
835
+ var UserNetworkServiceConfigService = class {
836
+ static STORAGE_PREFIX = "tfb_service_config_";
837
+ static listeners = /* @__PURE__ */ new Map();
838
+ /**
839
+ * Generates a localStorage key for a network-service combination.
840
+ *
841
+ * @private
842
+ * @param networkId - The network ID
843
+ * @param serviceId - The service ID
844
+ * @returns The storage key string
845
+ */
846
+ static key(networkId, serviceId) {
847
+ return `${this.STORAGE_PREFIX}${serviceId}__${networkId}`;
848
+ }
849
+ /**
850
+ * Subscribes to configuration change events for a specific network and/or service.
851
+ *
852
+ * Use '*' as a wildcard to listen to all networks or all services.
853
+ * The listener will be called whenever a matching configuration changes or is cleared.
854
+ *
855
+ * @param networkId - Network ID to listen to, or '*' for all networks
856
+ * @param serviceId - Service ID to listen to, or '*' for all services
857
+ * @param listener - Callback function to invoke when matching events occur
858
+ * @returns Unsubscribe function to remove the listener
859
+ *
860
+ * @example
861
+ * ```typescript
862
+ * // Listen to all RPC config changes across all networks
863
+ * const unsubscribe = userNetworkServiceConfigService.subscribe('*', 'rpc', (event) => {
864
+ * console.log(`${event.networkId} RPC config changed`);
865
+ * });
866
+ *
867
+ * // Later, unsubscribe
868
+ * unsubscribe();
869
+ * ```
870
+ */
871
+ static subscribe(networkId, serviceId, listener) {
872
+ const k = `${networkId}:${serviceId}`;
873
+ if (!this.listeners.has(k)) this.listeners.set(k, /* @__PURE__ */ new Set());
874
+ this.listeners.get(k).add(listener);
875
+ return () => {
876
+ const set = this.listeners.get(k);
877
+ if (set) {
878
+ set.delete(listener);
879
+ if (set.size === 0) this.listeners.delete(k);
880
+ }
881
+ };
882
+ }
883
+ /**
884
+ * Emits an event to all matching subscribers.
885
+ * Subscribers are matched based on exact network/service IDs or wildcards.
886
+ *
887
+ * @private
888
+ * @param event - The event to emit
889
+ */
890
+ static emit(event) {
891
+ const targets = [
892
+ `${event.networkId}:${event.serviceId}`,
893
+ `${event.networkId}:*`,
894
+ `*:${event.serviceId}`,
895
+ `*:*`
896
+ ];
897
+ for (const k of targets) {
898
+ const set = this.listeners.get(k);
899
+ if (!set) continue;
900
+ for (const fn of set) try {
901
+ fn(event);
902
+ } catch (e) {
903
+ logger.error("UserNetworkServiceConfigService", "Error in event listener:", e);
904
+ }
905
+ }
906
+ }
907
+ /**
908
+ * Saves a service configuration for a specific network.
909
+ *
910
+ * The configuration is stored in localStorage and all matching subscribers
911
+ * are notified via a 'service-config-changed' event.
912
+ *
913
+ * @param networkId - The network ID (e.g., 'ethereum-sepolia')
914
+ * @param serviceId - The service ID (e.g., 'rpc', 'explorer', 'contract-definitions')
915
+ * @param config - The configuration object to save
916
+ *
917
+ * @example
918
+ * ```typescript
919
+ * userNetworkServiceConfigService.save('ethereum-sepolia', 'rpc', {
920
+ * rpcUrl: 'https://sepolia.infura.io/v3/your-key'
921
+ * });
922
+ * ```
923
+ */
924
+ static save(networkId, serviceId, config) {
925
+ try {
926
+ localStorage.setItem(this.key(networkId, serviceId), JSON.stringify(config));
927
+ logger.info("UserNetworkServiceConfigService", `Saved config for ${serviceId} on network ${networkId}`);
928
+ this.emit({
929
+ type: "service-config-changed",
930
+ networkId,
931
+ serviceId,
932
+ config
933
+ });
934
+ } catch (e) {
935
+ logger.error("UserNetworkServiceConfigService", "Failed to save config:", e);
936
+ }
937
+ }
938
+ /**
939
+ * Retrieves a saved service configuration for a specific network.
940
+ *
941
+ * @param networkId - The network ID (e.g., 'ethereum-sepolia')
942
+ * @param serviceId - The service ID (e.g., 'rpc', 'explorer', 'contract-definitions')
943
+ * @returns The configuration object, or null if not found or if retrieval fails
944
+ *
945
+ * @example
946
+ * ```typescript
947
+ * const config = userNetworkServiceConfigService.get('ethereum-sepolia', 'rpc');
948
+ * if (config) {
949
+ * console.log('RPC URL:', config.rpcUrl);
950
+ * }
951
+ * ```
952
+ */
953
+ static get(networkId, serviceId) {
954
+ try {
955
+ const raw = localStorage.getItem(this.key(networkId, serviceId));
956
+ return raw ? JSON.parse(raw) : null;
957
+ } catch (e) {
958
+ logger.error("UserNetworkServiceConfigService", "Failed to retrieve config:", e);
959
+ return null;
960
+ }
961
+ }
962
+ /**
963
+ * Clears a saved service configuration for a specific network.
964
+ *
965
+ * Removes the configuration from localStorage and notifies all matching subscribers
966
+ * via a 'service-config-cleared' event.
967
+ *
968
+ * @param networkId - The network ID (e.g., 'ethereum-sepolia')
969
+ * @param serviceId - The service ID (e.g., 'rpc', 'explorer', 'contract-definitions')
970
+ *
971
+ * @example
972
+ * ```typescript
973
+ * userNetworkServiceConfigService.clear('ethereum-sepolia', 'rpc');
974
+ * ```
975
+ */
976
+ static clear(networkId, serviceId) {
977
+ try {
978
+ localStorage.removeItem(this.key(networkId, serviceId));
979
+ logger.info("UserNetworkServiceConfigService", `Cleared config for ${serviceId} on network ${networkId}`);
980
+ this.emit({
981
+ type: "service-config-cleared",
982
+ networkId,
983
+ serviceId
984
+ });
985
+ } catch (e) {
986
+ logger.error("UserNetworkServiceConfigService", "Failed to clear config:", e);
987
+ }
988
+ }
989
+ };
990
+ /**
991
+ * Singleton instance of UserNetworkServiceConfigService.
992
+ * This is the preferred way to access the service.
993
+ *
994
+ * @example
995
+ * ```typescript
996
+ * import { userNetworkServiceConfigService } from '@openzeppelin/ui-utils';
997
+ *
998
+ * userNetworkServiceConfigService.save('ethereum-sepolia', 'rpc', { rpcUrl: '...' });
999
+ * ```
1000
+ */
1001
+ const userNetworkServiceConfigService = UserNetworkServiceConfigService;
1002
+
1003
+ //#endregion
1004
+ //#region src/fieldDefaults.ts
1005
+ /**
1006
+ * Get a default value for a field type.
1007
+ * This is a chain-agnostic utility that provides appropriate default values
1008
+ * based on the UI field type.
1009
+ *
1010
+ * @param fieldType - The UI field type
1011
+ * @returns The appropriate default value for that field type
1012
+ */
1013
+ function getDefaultValueForType(fieldType) {
1014
+ switch (fieldType) {
1015
+ case "checkbox": return false;
1016
+ case "number":
1017
+ case "amount": return 0;
1018
+ case "array": return [];
1019
+ case "object": return {};
1020
+ case "array-object": return [];
1021
+ case "map": return [];
1022
+ case "blockchain-address":
1023
+ case "bigint":
1024
+ case "text":
1025
+ case "textarea":
1026
+ case "bytes":
1027
+ case "email":
1028
+ case "password":
1029
+ case "select":
1030
+ case "radio":
1031
+ case "date":
1032
+ case "hidden":
1033
+ default: return "";
1034
+ }
1035
+ }
1036
+
1037
+ //#endregion
1038
+ //#region src/fieldValidation.ts
1039
+ /**
1040
+ * Enhances field validation with numeric bounds based on parameter type.
1041
+ * Only applies bounds if they are not already set in the validation object.
1042
+ *
1043
+ * @param validation - Existing validation rules (may be undefined)
1044
+ * @param parameterType - The blockchain parameter type (e.g., 'uint32', 'U32', 'Uint<0..255>')
1045
+ * @param boundsMap - Chain-specific map of type names to min/max bounds
1046
+ * @returns Enhanced validation object with numeric bounds applied
1047
+ *
1048
+ * @example
1049
+ * ```typescript
1050
+ * const stellarBounds = { U32: { min: 0, max: 4_294_967_295 } };
1051
+ * const validation = enhanceNumericValidation(undefined, 'U32', stellarBounds);
1052
+ * // Returns: { min: 0, max: 4_294_967_295 }
1053
+ * ```
1054
+ */
1055
+ function enhanceNumericValidation(validation, parameterType, boundsMap) {
1056
+ const result = { ...validation ?? {} };
1057
+ const bounds = boundsMap[parameterType];
1058
+ if (!bounds) return result;
1059
+ if (bounds.min !== void 0 && result.min === void 0) result.min = bounds.min;
1060
+ if (bounds.max !== void 0 && result.max === void 0) result.max = bounds.max;
1061
+ return result;
1062
+ }
1063
+
1064
+ //#endregion
1065
+ //#region src/typeguards.ts
1066
+ /**
1067
+ * Type guard to check if a value is a non-null object (Record<string, unknown>).
1068
+ * Useful for safely accessing properties on an 'unknown' type after this check.
1069
+ * @param value - The value to check.
1070
+ * @returns True if the value is a non-null object, false otherwise.
1071
+ */
1072
+ function isRecordWithProperties(value) {
1073
+ return typeof value === "object" && value !== null;
1074
+ }
1075
+ /**
1076
+ * Type guard to check if a value is a plain object (not an array, not null).
1077
+ * This is useful for distinguishing between objects and arrays, since arrays are technically objects in JavaScript.
1078
+ * @param value - The value to check.
1079
+ * @returns True if the value is a plain object (not array, not null), false otherwise.
1080
+ */
1081
+ function isPlainObject(value) {
1082
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1083
+ }
1084
+
1085
+ //#endregion
1086
+ //#region src/cn.ts
1087
+ /**
1088
+ * Combines class names using clsx and tailwind-merge.
1089
+ * @param inputs - Class values to combine
1090
+ * @returns Merged class name string
1091
+ */
1092
+ function cn(...inputs) {
1093
+ return twMerge(clsx(inputs));
1094
+ }
1095
+
1096
+ //#endregion
1097
+ //#region src/formatting.ts
1098
+ /**
1099
+ * String and date formatting utility functions
1100
+ * These utilities help with common formatting operations
1101
+ */
1102
+ /**
1103
+ * Truncates a string (like an Ethereum address) in the middle
1104
+ * @param str The string to truncate
1105
+ * @param startChars Number of characters to show at the beginning
1106
+ * @param endChars Number of characters to show at the end
1107
+ * @returns The truncated string with ellipsis in the middle
1108
+ */
1109
+ function truncateMiddle(str, startChars = 6, endChars = 4) {
1110
+ if (!str) return "";
1111
+ if (str.length <= startChars + endChars) return str;
1112
+ return `${str.substring(0, startChars)}...${str.substring(str.length - endChars)}`;
1113
+ }
1114
+ /**
1115
+ * Formats a timestamp as a relative time string (e.g., "2h ago", "just now")
1116
+ * @param date The date to format
1117
+ * @returns A human-readable relative time string
1118
+ */
1119
+ function formatTimestamp(date) {
1120
+ const diffMs = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
1121
+ const diffMinutes = Math.floor(diffMs / (1e3 * 60));
1122
+ const diffHours = Math.floor(diffMs / (1e3 * 60 * 60));
1123
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1124
+ if (diffMinutes < 1) return "just now";
1125
+ if (diffMinutes < 60) return `${diffMinutes}m ago`;
1126
+ if (diffHours < 24) return `${diffHours}h ago`;
1127
+ if (diffDays < 7) return `${diffDays}d ago`;
1128
+ return date.toLocaleDateString();
1129
+ }
1130
+ /**
1131
+ * Detects whether a string contains hex-encoded or base64-encoded binary data.
1132
+ * Useful for auto-detecting the encoding format of user inputs across blockchain adapters.
1133
+ *
1134
+ * @param value - The string to analyze
1135
+ * @returns 'hex' if the string appears to be hexadecimal, 'base64' if it appears to be base64
1136
+ *
1137
+ * @example
1138
+ * ```typescript
1139
+ * detectBytesEncoding("48656c6c6f") // → 'hex'
1140
+ * detectBytesEncoding("SGVsbG8=") // → 'base64'
1141
+ * detectBytesEncoding("0x48656c6c6f") // → 'hex' (after stripping 0x prefix)
1142
+ * ```
1143
+ */
1144
+ function detectBytesEncoding(value) {
1145
+ const trimmed = value?.trim() ?? "";
1146
+ const without0x = trimmed.startsWith("0x") || trimmed.startsWith("0X") ? trimmed.slice(2) : trimmed;
1147
+ const hexRegex = /^[0-9a-fA-F]+$/;
1148
+ const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
1149
+ if (hexRegex.test(without0x) && without0x.length > 0 && without0x.length % 2 === 0) return "hex";
1150
+ if (base64Regex.test(trimmed) && trimmed.length % 4 === 0) try {
1151
+ const decoded = atob(trimmed);
1152
+ if (btoa(decoded).replace(/=+$/, "") === trimmed.replace(/=+$/, "")) return "base64";
1153
+ } catch {}
1154
+ return "hex";
1155
+ }
1156
+
1157
+ //#endregion
1158
+ //#region src/generateId.ts
1159
+ /**
1160
+ * General utility functions, which are not specific to any blockchain
1161
+ * It's important to keep these functions as simple as possible and avoid any
1162
+ * dependencies from other packages.
1163
+ */
1164
+ /**
1165
+ * Generates a unique ID for form fields, components, etc.
1166
+ * Uses crypto.getRandomValues() for browser-compatible random ID generation.
1167
+ *
1168
+ * @param prefix Optional prefix to add before the UUID
1169
+ * @returns A string ID that is guaranteed to be unique
1170
+ */
1171
+ function generateId(prefix) {
1172
+ const uuid = v4();
1173
+ return prefix ? `${prefix}_${uuid}` : uuid;
1174
+ }
1175
+
1176
+ //#endregion
1177
+ //#region src/validators.ts
1178
+ /**
1179
+ * URL validation utilities
1180
+ */
1181
+ /**
1182
+ * Validates if a string is a valid URL (supports http, https, and ftp protocols).
1183
+ * Relies solely on the URL constructor for validation.
1184
+ *
1185
+ * @param urlString - The string to validate
1186
+ * @returns True if the URL is valid, false otherwise
1187
+ */
1188
+ function isValidUrl(urlString) {
1189
+ if (!urlString || typeof urlString !== "string") return false;
1190
+ try {
1191
+ new URL(urlString);
1192
+ return true;
1193
+ } catch {
1194
+ return false;
1195
+ }
1196
+ }
1197
+ /**
1198
+ * Gets a user-friendly error message for invalid URLs.
1199
+ *
1200
+ * @returns Standard error message for invalid URLs
1201
+ */
1202
+ function getInvalidUrlMessage() {
1203
+ return "Please enter a valid URL (e.g., https://example.com)";
1204
+ }
1205
+
1206
+ //#endregion
1207
+ //#region src/async.ts
1208
+ /**
1209
+ * Utility to add delay between operations
1210
+ * @param ms - Milliseconds to delay
1211
+ * @returns Promise that resolves after the specified delay
1212
+ */
1213
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1214
+ /**
1215
+ * Executes operations in batches with rate limiting to prevent API overload
1216
+ * @param operations - Array of functions that return promises
1217
+ * @param batchSize - Number of operations to execute in parallel per batch (default: 2)
1218
+ * @param delayMs - Delay in milliseconds between batches (default: 100)
1219
+ * @returns Promise that resolves to an array of results from all operations
1220
+ */
1221
+ async function rateLimitedBatch(operations, batchSize = 2, delayMs = 100) {
1222
+ const results = [];
1223
+ for (let i = 0; i < operations.length; i += batchSize) {
1224
+ const batch = operations.slice(i, i + batchSize);
1225
+ const batchResults = await Promise.all(batch.map((operation) => operation()));
1226
+ results.push(...batchResults);
1227
+ if (i + batchSize < operations.length) await delay(delayMs);
1228
+ }
1229
+ return results;
1230
+ }
1231
+ /**
1232
+ * Wraps a promise with a timeout. Rejects with a descriptive Error after timeoutMs.
1233
+ *
1234
+ * @param promise The promise to wrap
1235
+ * @param timeoutMs Timeout in milliseconds
1236
+ * @param label Optional label to include in the timeout error message
1237
+ */
1238
+ function withTimeout(promise, timeoutMs, label) {
1239
+ return new Promise((resolve, reject) => {
1240
+ const timer = setTimeout(() => {
1241
+ reject(/* @__PURE__ */ new Error(`${label ?? "operation"} timed out after ${timeoutMs}ms`));
1242
+ }, timeoutMs);
1243
+ promise.then((value) => {
1244
+ clearTimeout(timer);
1245
+ resolve(value);
1246
+ }).catch((err) => {
1247
+ clearTimeout(timer);
1248
+ reject(err);
1249
+ });
1250
+ });
1251
+ }
1252
+ /**
1253
+ * Default concurrency limit for parallel operations.
1254
+ * Set to a reasonable value that balances performance and service limits.
1255
+ */
1256
+ const DEFAULT_CONCURRENCY_LIMIT = 10;
1257
+ /**
1258
+ * Execute an array of promise-returning functions with a concurrency limit.
1259
+ *
1260
+ * Uses a worker pool approach that maintains up to `limit` concurrent operations.
1261
+ * As soon as one operation completes, the next one starts immediately, maximizing
1262
+ * throughput while respecting the concurrency limit.
1263
+ *
1264
+ * Results are returned in the same order as the input tasks, regardless of
1265
+ * completion order.
1266
+ *
1267
+ * @param tasks Array of functions that return promises
1268
+ * @param limit Maximum number of concurrent executions (default: 10)
1269
+ * @returns Promise resolving to array of results in same order as input tasks
1270
+ *
1271
+ * @example
1272
+ * ```typescript
1273
+ * // Fetch 100 role members with max 10 concurrent RPC requests
1274
+ * const tasks = memberIndices.map((index) => () => getRoleMember(contract, role, index));
1275
+ * const members = await promiseAllWithLimit(tasks, 10);
1276
+ * ```
1277
+ */
1278
+ async function promiseAllWithLimit(tasks, limit = DEFAULT_CONCURRENCY_LIMIT) {
1279
+ if (tasks.length === 0) return [];
1280
+ if (limit >= tasks.length) return Promise.all(tasks.map((task) => task()));
1281
+ const results = new Array(tasks.length);
1282
+ let currentIndex = 0;
1283
+ async function worker() {
1284
+ while (currentIndex < tasks.length) {
1285
+ const index = currentIndex++;
1286
+ const task = tasks[index];
1287
+ results[index] = await task();
1288
+ }
1289
+ }
1290
+ const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => worker());
1291
+ await Promise.all(workers);
1292
+ return results;
1293
+ }
1294
+ /**
1295
+ * Execute an array of promise-returning functions with a concurrency limit,
1296
+ * settling all promises (similar to Promise.allSettled but with concurrency control).
1297
+ *
1298
+ * Unlike promiseAllWithLimit, this function does not fail fast on errors.
1299
+ * All tasks will be executed regardless of individual failures.
1300
+ *
1301
+ * @param tasks Array of functions that return promises
1302
+ * @param limit Maximum number of concurrent executions (default: 10)
1303
+ * @returns Promise resolving to array of settled results in same order as input tasks
1304
+ *
1305
+ * @example
1306
+ * ```typescript
1307
+ * const tasks = items.map((item) => () => fetchItem(item.id));
1308
+ * const results = await promiseAllSettledWithLimit(tasks, 10);
1309
+ *
1310
+ * for (const result of results) {
1311
+ * if (result.status === 'fulfilled') {
1312
+ * console.log('Success:', result.value);
1313
+ * } else {
1314
+ * console.log('Failed:', result.reason);
1315
+ * }
1316
+ * }
1317
+ * ```
1318
+ */
1319
+ async function promiseAllSettledWithLimit(tasks, limit = DEFAULT_CONCURRENCY_LIMIT) {
1320
+ if (tasks.length === 0) return [];
1321
+ if (limit >= tasks.length) return Promise.allSettled(tasks.map((task) => task()));
1322
+ const results = new Array(tasks.length);
1323
+ let currentIndex = 0;
1324
+ async function worker() {
1325
+ while (currentIndex < tasks.length) {
1326
+ const index = currentIndex++;
1327
+ const task = tasks[index];
1328
+ try {
1329
+ results[index] = {
1330
+ status: "fulfilled",
1331
+ value: await task()
1332
+ };
1333
+ } catch (reason) {
1334
+ results[index] = {
1335
+ status: "rejected",
1336
+ reason
1337
+ };
1338
+ }
1339
+ }
1340
+ }
1341
+ const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => worker());
1342
+ await Promise.all(workers);
1343
+ return results;
1344
+ }
1345
+
1346
+ //#endregion
1347
+ //#region src/hash.ts
1348
+ /**
1349
+ * Simple browser-compatible hash utilities
1350
+ * These functions provide deterministic hashing for content comparison
1351
+ * and are not intended for cryptographic purposes.
1352
+ */
1353
+ /**
1354
+ * Creates a simple hash from a string using a non-cryptographic algorithm
1355
+ * Suitable for content comparison, caching keys, and quick fingerprinting
1356
+ *
1357
+ * @param str - The string to hash
1358
+ * @returns A hexadecimal hash string (always positive)
1359
+ *
1360
+ * @example
1361
+ * ```typescript
1362
+ * const hash1 = simpleHash('{"name": "test"}');
1363
+ * const hash2 = simpleHash('{"name": "test"}');
1364
+ * console.log(hash1 === hash2); // true - deterministic
1365
+ * ```
1366
+ */
1367
+ function simpleHash(str) {
1368
+ let hash = 0;
1369
+ for (let i = 0; i < str.length; i++) {
1370
+ const char = str.charCodeAt(i);
1371
+ hash = (hash << 5) - hash + char;
1372
+ hash = hash & hash;
1373
+ }
1374
+ return Math.abs(hash).toString(16);
1375
+ }
1376
+
1377
+ //#endregion
1378
+ //#region src/bytesValidation.ts
1379
+ /**
1380
+ * Validates bytes input using the established validator.js library.
1381
+ *
1382
+ * This function provides comprehensive validation for blockchain bytes data including:
1383
+ * - Hex encoding validation (with optional 0x prefix)
1384
+ * - Base64 encoding validation
1385
+ * - Byte length validation
1386
+ * - Format detection
1387
+ *
1388
+ * @param value - The input string to validate
1389
+ * @param options - Validation options
1390
+ * @returns Validation result with details
1391
+ *
1392
+ * @example
1393
+ * ```typescript
1394
+ * validateBytes('48656c6c6f') // → { isValid: true, detectedFormat: 'hex', byteSize: 5 }
1395
+ * validateBytes('SGVsbG8=') // → { isValid: true, detectedFormat: 'base64', byteSize: 5 }
1396
+ * validateBytes('invalid') // → { isValid: false, error: '...' }
1397
+ * ```
1398
+ */
1399
+ function validateBytes(value, options = {}) {
1400
+ const { acceptedFormats = "both", maxBytes, allowHexPrefix = true } = options;
1401
+ if (!value || value.trim() === "") return {
1402
+ isValid: true,
1403
+ cleanedValue: "",
1404
+ byteSize: 0
1405
+ };
1406
+ const cleanValue = value.trim().replace(/\s+/g, "");
1407
+ const hasHexPrefix = cleanValue.startsWith("0x");
1408
+ const withoutPrefix = hasHexPrefix ? cleanValue.slice(2) : cleanValue;
1409
+ if (withoutPrefix === "") return {
1410
+ isValid: false,
1411
+ error: "Bytes value cannot be empty",
1412
+ cleanedValue: cleanValue
1413
+ };
1414
+ let detectedFormat = null;
1415
+ let byteSize = 0;
1416
+ if (validator.isHexadecimal(withoutPrefix)) {
1417
+ detectedFormat = "hex";
1418
+ if (withoutPrefix.length % 2 !== 0) return {
1419
+ isValid: false,
1420
+ error: "Hex string must have even number of characters",
1421
+ cleanedValue: cleanValue,
1422
+ detectedFormat
1423
+ };
1424
+ byteSize = withoutPrefix.length / 2;
1425
+ } else if (validator.isBase64(withoutPrefix)) {
1426
+ detectedFormat = "base64";
1427
+ try {
1428
+ byteSize = atob(withoutPrefix).length;
1429
+ } catch {
1430
+ return {
1431
+ isValid: false,
1432
+ error: "Invalid base64 encoding",
1433
+ cleanedValue: cleanValue
1434
+ };
1435
+ }
1436
+ }
1437
+ if (!detectedFormat) return {
1438
+ isValid: false,
1439
+ error: "Invalid format. Expected hex or base64 encoding",
1440
+ cleanedValue: cleanValue
1441
+ };
1442
+ if (acceptedFormats !== "both") {
1443
+ if (acceptedFormats === "hex" && detectedFormat !== "hex") return {
1444
+ isValid: false,
1445
+ error: "Only hex format is accepted for this field",
1446
+ cleanedValue: cleanValue,
1447
+ detectedFormat
1448
+ };
1449
+ if (acceptedFormats === "base64" && detectedFormat !== "base64") return {
1450
+ isValid: false,
1451
+ error: "Only base64 format is accepted for this field",
1452
+ cleanedValue: cleanValue,
1453
+ detectedFormat
1454
+ };
1455
+ }
1456
+ if (detectedFormat === "hex" && hasHexPrefix && !allowHexPrefix) return {
1457
+ isValid: false,
1458
+ error: "0x prefix not allowed for this field",
1459
+ cleanedValue: cleanValue,
1460
+ detectedFormat
1461
+ };
1462
+ if (maxBytes && byteSize > maxBytes) return {
1463
+ isValid: false,
1464
+ error: `Maximum ${maxBytes} bytes allowed (${detectedFormat === "hex" ? `${maxBytes * 2} hex characters` : `${maxBytes} bytes`})`,
1465
+ cleanedValue: cleanValue,
1466
+ detectedFormat,
1467
+ byteSize
1468
+ };
1469
+ return {
1470
+ isValid: true,
1471
+ cleanedValue: cleanValue,
1472
+ detectedFormat,
1473
+ byteSize
1474
+ };
1475
+ }
1476
+ /**
1477
+ * Simple validation function that returns boolean or error string
1478
+ * (for compatibility with existing React Hook Form validation)
1479
+ *
1480
+ * @param value - The input string to validate
1481
+ * @param options - Validation options
1482
+ * @returns true if valid, error string if invalid
1483
+ */
1484
+ function validateBytesSimple(value, options = {}) {
1485
+ const result = validateBytes(value, options);
1486
+ return result.isValid ? true : result.error || "Invalid bytes format";
1487
+ }
1488
+ /**
1489
+ * Extracts the size from a Bytes<N> type string, or returns undefined for dynamic Uint8Array
1490
+ *
1491
+ * @param type - Type string (e.g., "Bytes<32>", "Uint8Array", "bytes")
1492
+ * @returns Size in bytes if fixed-size, undefined if dynamic
1493
+ *
1494
+ * @example
1495
+ * ```typescript
1496
+ * getBytesSize('Bytes<32>') // → 32
1497
+ * getBytesSize('Bytes<64>') // → 64
1498
+ * getBytesSize('Uint8Array') // → undefined
1499
+ * getBytesSize('bytes') // → undefined
1500
+ * ```
1501
+ */
1502
+ function getBytesSize(type) {
1503
+ const match = type.match(/^Bytes<(\d+)>$/i);
1504
+ if (match) return Number.parseInt(match[1], 10);
1505
+ }
1506
+
1507
+ //#endregion
1508
+ //#region src/bytesConversion.ts
1509
+ /**
1510
+ * Cross-platform bytes conversion utilities that work in both browser and Node.js
1511
+ * without requiring Buffer polyfills.
1512
+ */
1513
+ /**
1514
+ * Convert a hex string to Uint8Array using native browser APIs
1515
+ * @param hex - Hex string (with or without 0x prefix)
1516
+ * @returns Uint8Array representation
1517
+ */
1518
+ function hexToBytes(hex) {
1519
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
1520
+ if (cleanHex.length % 2 !== 0) throw new Error("Hex string must have even length");
1521
+ if (!/^[0-9a-fA-F]*$/.test(cleanHex)) throw new Error("Invalid hex characters in string");
1522
+ const bytes = new Uint8Array(cleanHex.length / 2);
1523
+ for (let i = 0; i < cleanHex.length; i += 2) bytes[i / 2] = parseInt(cleanHex.substring(i, i + 2), 16);
1524
+ return bytes;
1525
+ }
1526
+ /**
1527
+ * Convert a base64 string to Uint8Array using native browser APIs
1528
+ * Handles data URLs by stripping the prefix
1529
+ * @param base64 - Base64 encoded string (with optional data URL prefix)
1530
+ * @returns Uint8Array representation
1531
+ */
1532
+ function base64ToBytes(base64) {
1533
+ const cleaned = base64.includes(",") ? base64.split(",")[1] : base64;
1534
+ const binaryString = atob(cleaned);
1535
+ const len = binaryString.length;
1536
+ const bytes = new Uint8Array(len);
1537
+ for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i);
1538
+ return bytes;
1539
+ }
1540
+ /**
1541
+ * Convert Uint8Array to hex string
1542
+ * @param bytes - Uint8Array to convert
1543
+ * @param withPrefix - Whether to include '0x' prefix
1544
+ * @returns Hex string representation
1545
+ */
1546
+ function bytesToHex(bytes, withPrefix = false) {
1547
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1548
+ return withPrefix ? `0x${hex}` : hex;
1549
+ }
1550
+ /**
1551
+ * Convert string to bytes based on detected encoding (hex or base64)
1552
+ * @param value - The string value to convert
1553
+ * @param encoding - The detected encoding type
1554
+ * @returns Uint8Array representation
1555
+ */
1556
+ function stringToBytes(value, encoding) {
1557
+ switch (encoding) {
1558
+ case "hex": return hexToBytes(value);
1559
+ case "base64": return base64ToBytes(value);
1560
+ default: throw new Error(`Unsupported encoding: ${encoding}. Supported encodings: hex, base64`);
1561
+ }
1562
+ }
1563
+
1564
+ //#endregion
1565
+ //#region src/environment.ts
1566
+ /**
1567
+ * Utility functions for environment detection
1568
+ */
1569
+ /**
1570
+ * Check if the application is running in development or test environment
1571
+ * @returns True if NODE_ENV is 'development' or 'test'
1572
+ */
1573
+ function isDevelopmentOrTestEnvironment() {
1574
+ return process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
1575
+ }
1576
+ /**
1577
+ * Check if the application is running in production environment
1578
+ * @returns True if NODE_ENV is 'production'
1579
+ */
1580
+ function isProductionEnvironment() {
1581
+ return process.env.NODE_ENV === "production";
1582
+ }
1583
+
1584
+ //#endregion
1585
+ //#region src/RouterService.ts
1586
+ /**
1587
+ * Default implementation that relies on the browser Location API.
1588
+ * The builder app can replace this with a router-bound implementation if needed.
1589
+ */
1590
+ var BrowserRouterService = class {
1591
+ currentLocation() {
1592
+ if (typeof window === "undefined") return "";
1593
+ return window.location.href;
1594
+ }
1595
+ getParam(name) {
1596
+ if (typeof window === "undefined") return null;
1597
+ return new URLSearchParams(window.location.search).get(name);
1598
+ }
1599
+ navigate(path) {
1600
+ if (typeof window === "undefined") return;
1601
+ if (path.startsWith("http://") || path.startsWith("https://")) window.location.assign(path);
1602
+ else {
1603
+ const url = new URL(path, window.location.origin);
1604
+ window.history.pushState({}, "", url.toString());
1605
+ window.dispatchEvent(new PopStateEvent("popstate"));
1606
+ }
1607
+ }
1608
+ };
1609
+ /**
1610
+ * Singleton instance for global consumption.
1611
+ */
1612
+ const routerService = new BrowserRouterService();
1613
+
1614
+ //#endregion
1615
+ //#region src/AnalyticsService.ts
1616
+ /**
1617
+ * Google Analytics service for tracking user interactions.
1618
+ * Manages Google Analytics initialization and event tracking.
1619
+ * Only active when the analytics_enabled feature flag is true.
1620
+ *
1621
+ * This is a generic service that provides core analytics functionality.
1622
+ * App-specific tracking methods should be implemented in app-level hooks
1623
+ * that use the generic `trackEvent` method.
1624
+ *
1625
+ * @example
1626
+ * ```typescript
1627
+ * // Initialize analytics (typically done once at app startup)
1628
+ * AnalyticsService.initialize('G-XXXXXXXXXX');
1629
+ *
1630
+ * // Track a custom event
1631
+ * AnalyticsService.trackEvent('button_clicked', { button_name: 'submit' });
1632
+ *
1633
+ * // Track page view
1634
+ * AnalyticsService.trackPageView('Dashboard', '/dashboard');
1635
+ * ```
1636
+ */
1637
+ var AnalyticsService = class {
1638
+ static initialized = false;
1639
+ /**
1640
+ * Initialize Google Analytics
1641
+ * @param tagId - Google Analytics tag ID (e.g., G-N3DZK5FCT1)
1642
+ */
1643
+ static initialize(tagId) {
1644
+ if (!tagId) {
1645
+ logger.warn("AnalyticsService", "No tag ID provided");
1646
+ return;
1647
+ }
1648
+ if (!this.isEnabled()) {
1649
+ logger.info("AnalyticsService", "Analytics is disabled via feature flag");
1650
+ return;
1651
+ }
1652
+ if (this.initialized) {
1653
+ logger.info("AnalyticsService", "Already initialized");
1654
+ return;
1655
+ }
1656
+ try {
1657
+ this.loadGtagScript(tagId);
1658
+ this.initializeGtag(tagId);
1659
+ this.initialized = true;
1660
+ logger.info("AnalyticsService", "Initialized successfully");
1661
+ } catch (error) {
1662
+ logger.error("AnalyticsService", "Failed to initialize:", error);
1663
+ }
1664
+ }
1665
+ /**
1666
+ * Check if analytics is enabled via feature flag
1667
+ */
1668
+ static isEnabled() {
1669
+ return appConfigService.isFeatureEnabled("analytics_enabled");
1670
+ }
1671
+ /**
1672
+ * Reset the analytics service state (primarily for testing)
1673
+ */
1674
+ static reset() {
1675
+ this.initialized = false;
1676
+ }
1677
+ /**
1678
+ * Generic event tracking method.
1679
+ * Use this to track any custom event with arbitrary parameters.
1680
+ *
1681
+ * @param eventName - Name of the event (e.g., 'button_clicked', 'form_submitted')
1682
+ * @param parameters - Key-value pairs of event parameters
1683
+ *
1684
+ * @example
1685
+ * ```typescript
1686
+ * AnalyticsService.trackEvent('ecosystem_selected', { ecosystem: 'evm' });
1687
+ * AnalyticsService.trackEvent('wizard_step', { step_number: 2, step_name: 'configure' });
1688
+ * ```
1689
+ */
1690
+ static trackEvent(eventName, parameters) {
1691
+ if (!this.isEnabled()) return;
1692
+ try {
1693
+ if (typeof window.gtag === "function") window.gtag("event", eventName, parameters);
1694
+ else logger.warn("AnalyticsService", "gtag is not available");
1695
+ } catch (error) {
1696
+ logger.error("AnalyticsService", `Failed to track event '${eventName}':`, error);
1697
+ }
1698
+ }
1699
+ /**
1700
+ * Track page view event.
1701
+ * Common event shared across all apps.
1702
+ *
1703
+ * @param pageName - Human-readable name of the page
1704
+ * @param pagePath - URL path of the page
1705
+ *
1706
+ * @example
1707
+ * ```typescript
1708
+ * AnalyticsService.trackPageView('Dashboard', '/dashboard');
1709
+ * ```
1710
+ */
1711
+ static trackPageView(pageName, pagePath) {
1712
+ this.trackEvent("page_view", {
1713
+ page_title: pageName,
1714
+ page_path: pagePath
1715
+ });
1716
+ }
1717
+ /**
1718
+ * Track network selection event.
1719
+ * Common event shared across all apps that involve network selection.
1720
+ *
1721
+ * @param networkId - Selected network ID
1722
+ * @param ecosystem - Ecosystem the network belongs to (e.g., 'evm', 'stellar')
1723
+ *
1724
+ * @example
1725
+ * ```typescript
1726
+ * AnalyticsService.trackNetworkSelection('ethereum-mainnet', 'evm');
1727
+ * ```
1728
+ */
1729
+ static trackNetworkSelection(networkId, ecosystem) {
1730
+ this.trackEvent("network_selected", {
1731
+ network_id: networkId,
1732
+ ecosystem
1733
+ });
1734
+ }
1735
+ /**
1736
+ * Load the Google Analytics gtag script
1737
+ * @private
1738
+ */
1739
+ static loadGtagScript(tagId) {
1740
+ if (document.querySelector(`script[src*="gtag/js?id=${tagId}"]`)) return;
1741
+ if (!window.dataLayer) window.dataLayer = [];
1742
+ window.gtag = window.gtag || function gtag() {
1743
+ window.dataLayer.push(arguments);
1744
+ };
1745
+ const script = document.createElement("script");
1746
+ script.async = true;
1747
+ script.src = `https://www.googletagmanager.com/gtag/js?id=${tagId}`;
1748
+ document.head.appendChild(script);
1749
+ }
1750
+ /**
1751
+ * Initialize gtag with configuration
1752
+ * @private
1753
+ */
1754
+ static initializeGtag(tagId) {
1755
+ if (typeof window.gtag === "function") {
1756
+ window.gtag("js", /* @__PURE__ */ new Date());
1757
+ window.gtag("config", tagId);
1758
+ }
1759
+ }
1760
+ };
1761
+
1762
+ //#endregion
1763
+ //#region src/deepLink.ts
1764
+ /**
1765
+ * Parses URL query parameters into a key-value object.
1766
+ * @returns Object containing all URL query parameters
1767
+ */
1768
+ function parseDeepLink() {
1769
+ const params = new URLSearchParams(window.location.search);
1770
+ const result = {};
1771
+ params.forEach((value, key) => {
1772
+ result[key] = value;
1773
+ });
1774
+ return result;
1775
+ }
1776
+ /**
1777
+ * Gets the forced service from deep link parameters.
1778
+ * @param params - Deep link parameters object
1779
+ * @returns Service name if specified, null otherwise
1780
+ */
1781
+ function getForcedService(params) {
1782
+ return params.service ?? null;
1783
+ }
1784
+ /**
1785
+ * Computes the effective provider preference based on priority order.
1786
+ * @param input - Configuration object with provider options
1787
+ * @returns The effective provider and its source
1788
+ */
1789
+ function computeEffectiveProviderPreference(input) {
1790
+ if (input.forcedService && input.forcedService.length > 0) return {
1791
+ effectiveProvider: input.forcedService,
1792
+ source: "urlForced"
1793
+ };
1794
+ if (input.uiSelection && input.uiSelection.length > 0) return {
1795
+ effectiveProvider: input.uiSelection,
1796
+ source: "ui"
1797
+ };
1798
+ if (input.appDefault && input.appDefault.length > 0) return {
1799
+ effectiveProvider: input.appDefault,
1800
+ source: "appConfig"
1801
+ };
1802
+ return {
1803
+ effectiveProvider: input.adapterDefaultOrder[0],
1804
+ source: "adapterDefault"
1805
+ };
1806
+ }
1807
+
1808
+ //#endregion
1809
+ //#region src/sanitize.ts
1810
+ /**
1811
+ * Minimal HTML sanitizer for client-side rendering of adapter-provided notes.
1812
+ *
1813
+ * - Strips <script>/<style> blocks and closing tags
1814
+ * - Removes inline event handlers (on*)
1815
+ * - Neutralizes javascript: URLs in href/src
1816
+ * - Whitelists a small set of tags: a,b,strong,i,em,code,br,ul,ol,li,p
1817
+ *
1818
+ * This utility is intentionally small and dependency-free. If we decide to
1819
+ * allow richer HTML, we can swap this implementation with a vetted library
1820
+ * (e.g., DOMPurify) behind the same function signature.
1821
+ */
1822
+ function sanitizeHtml(html) {
1823
+ if (!html) return "";
1824
+ let out = html.replace(/<\/(?:script|style)>/gi, "").replace(/<(?:script|style)[\s\S]*?>[\s\S]*?<\/(?:script|style)>/gi, "");
1825
+ out = out.replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "");
1826
+ out = out.replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "");
1827
+ out = out.replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "");
1828
+ out = out.replace(/(href|src)\s*=\s*"javascript:[^"]*"/gi, "$1=\"#\"");
1829
+ out = out.replace(/(href|src)\s*=\s*'javascript:[^']*'/gi, "$1=\"#\"");
1830
+ out = out.replace(/<(?!\/?(?:a|b|strong|i|em|code|br|ul|ol|li|p)\b)[^>]*>/gi, "");
1831
+ return out;
1832
+ }
1833
+
1834
+ //#endregion
1835
+ //#region src/access/snapshot.ts
1836
+ /**
1837
+ * Validates an access snapshot structure
1838
+ * @param snapshot The snapshot to validate
1839
+ * @returns True if valid, false otherwise
1840
+ */
1841
+ function validateSnapshot(snapshot) {
1842
+ if (!snapshot || typeof snapshot !== "object") return false;
1843
+ if (!Array.isArray(snapshot.roles)) return false;
1844
+ const roleIds = /* @__PURE__ */ new Set();
1845
+ for (const roleAssignment of snapshot.roles) {
1846
+ if (!roleAssignment || typeof roleAssignment !== "object") return false;
1847
+ if (!roleAssignment.role || typeof roleAssignment.role !== "object") return false;
1848
+ const roleId = roleAssignment.role.id;
1849
+ if (!roleId || typeof roleId !== "string" || roleId.trim() === "") return false;
1850
+ if (roleIds.has(roleId)) return false;
1851
+ roleIds.add(roleId);
1852
+ if (!Array.isArray(roleAssignment.members)) return false;
1853
+ const memberSet = /* @__PURE__ */ new Set();
1854
+ for (const member of roleAssignment.members) {
1855
+ if (typeof member !== "string" || member.trim() === "") return false;
1856
+ if (memberSet.has(member)) return false;
1857
+ memberSet.add(member);
1858
+ }
1859
+ }
1860
+ if (snapshot.ownership !== void 0) {
1861
+ if (!snapshot.ownership || typeof snapshot.ownership !== "object") return false;
1862
+ if (snapshot.ownership.owner !== null && (typeof snapshot.ownership.owner !== "string" || snapshot.ownership.owner.trim() === "")) return false;
1863
+ }
1864
+ return true;
1865
+ }
1866
+ /**
1867
+ * Serializes an access snapshot to JSON string
1868
+ * @param snapshot The snapshot to serialize
1869
+ * @returns JSON string representation
1870
+ * @throws Error if snapshot is invalid
1871
+ */
1872
+ function serializeSnapshot(snapshot) {
1873
+ if (!validateSnapshot(snapshot)) throw new Error("Invalid snapshot structure");
1874
+ return JSON.stringify(snapshot, null, 2);
1875
+ }
1876
+ /**
1877
+ * Deserializes a JSON string to an access snapshot
1878
+ * @param json The JSON string to deserialize
1879
+ * @returns Access snapshot object
1880
+ * @throws Error if JSON is invalid or snapshot structure is invalid
1881
+ */
1882
+ function deserializeSnapshot(json) {
1883
+ let parsed;
1884
+ try {
1885
+ parsed = JSON.parse(json);
1886
+ } catch (error) {
1887
+ throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : "Unknown error"}`);
1888
+ }
1889
+ if (!validateSnapshot(parsed)) throw new Error("Invalid snapshot structure after deserialization");
1890
+ return parsed;
1891
+ }
1892
+ /**
1893
+ * Creates an empty snapshot
1894
+ * @returns Empty snapshot with no roles and no ownership
1895
+ */
1896
+ function createEmptySnapshot() {
1897
+ return { roles: [] };
1898
+ }
1899
+ /**
1900
+ * Finds a role assignment by role ID
1901
+ * @param snapshot The snapshot to search
1902
+ * @param roleId The role ID to find
1903
+ * @returns The role assignment if found, undefined otherwise
1904
+ */
1905
+ function findRoleAssignment(snapshot, roleId) {
1906
+ return snapshot.roles.find((assignment) => assignment.role.id === roleId);
1907
+ }
1908
+ /**
1909
+ * Checks if a snapshot has any roles
1910
+ * @param snapshot The snapshot to check
1911
+ * @returns True if snapshot has at least one role assignment
1912
+ */
1913
+ function hasRoles(snapshot) {
1914
+ return snapshot.roles.length > 0;
1915
+ }
1916
+ /**
1917
+ * Checks if a snapshot has ownership information
1918
+ * @param snapshot The snapshot to check
1919
+ * @returns True if snapshot has ownership information
1920
+ */
1921
+ function hasOwnership(snapshot) {
1922
+ return snapshot.ownership !== void 0 && snapshot.ownership !== null;
1923
+ }
1924
+ /**
1925
+ * Gets the total number of role members across all roles
1926
+ * @param snapshot The snapshot to count
1927
+ * @returns Total number of unique members across all roles
1928
+ */
1929
+ function getTotalMemberCount(snapshot) {
1930
+ const allMembers = /* @__PURE__ */ new Set();
1931
+ for (const roleAssignment of snapshot.roles) for (const member of roleAssignment.members) allMembers.add(member);
1932
+ return allMembers.size;
1933
+ }
1934
+ /**
1935
+ * Gets all unique members across all roles
1936
+ * @param snapshot The snapshot to extract members from
1937
+ * @returns Array of unique member addresses
1938
+ */
1939
+ function getAllMembers(snapshot) {
1940
+ const allMembers = /* @__PURE__ */ new Set();
1941
+ for (const roleAssignment of snapshot.roles) for (const member of roleAssignment.members) allMembers.add(member);
1942
+ return Array.from(allMembers);
1943
+ }
1944
+ /**
1945
+ * Compares two snapshots and returns differences
1946
+ * @param snapshot1 First snapshot
1947
+ * @param snapshot2 Second snapshot
1948
+ * @returns Object describing differences
1949
+ */
1950
+ function compareSnapshots(snapshot1, snapshot2) {
1951
+ const rolesAdded = [];
1952
+ const rolesRemoved = [];
1953
+ const rolesModified = [];
1954
+ const roleMap1 = /* @__PURE__ */ new Map();
1955
+ const roleMap2 = /* @__PURE__ */ new Map();
1956
+ for (const assignment of snapshot1.roles) roleMap1.set(assignment.role.id, assignment);
1957
+ for (const assignment of snapshot2.roles) roleMap2.set(assignment.role.id, assignment);
1958
+ for (const [roleId, assignment] of roleMap2) if (!roleMap1.has(roleId)) rolesAdded.push(assignment);
1959
+ for (const [roleId, assignment] of roleMap1) if (!roleMap2.has(roleId)) rolesRemoved.push(assignment);
1960
+ for (const [roleId, assignment1] of roleMap1) {
1961
+ const assignment2 = roleMap2.get(roleId);
1962
+ if (assignment2) {
1963
+ const members1 = new Set(assignment1.members);
1964
+ const members2 = new Set(assignment2.members);
1965
+ const membersAdded = assignment2.members.filter((m) => !members1.has(m));
1966
+ const membersRemoved = assignment1.members.filter((m) => !members2.has(m));
1967
+ if (membersAdded.length > 0 || membersRemoved.length > 0) rolesModified.push({
1968
+ role: assignment1.role,
1969
+ membersAdded,
1970
+ membersRemoved
1971
+ });
1972
+ }
1973
+ }
1974
+ return {
1975
+ rolesAdded,
1976
+ rolesRemoved,
1977
+ rolesModified,
1978
+ ownershipChanged: snapshot1.ownership?.owner !== snapshot2.ownership?.owner
1979
+ };
1980
+ }
1981
+
1982
+ //#endregion
1983
+ //#region src/access/errors.ts
1984
+ /**
1985
+ * Type guard to check if an error is an AccessControlError
1986
+ *
1987
+ * @param error The error to check
1988
+ * @returns True if the error has the AccessControlError structure
1989
+ *
1990
+ * @example
1991
+ * ```typescript
1992
+ * try {
1993
+ * await service.grantRole(...);
1994
+ * } catch (error) {
1995
+ * if (isAccessControlError(error)) {
1996
+ * console.log('Access control error:', error.contractAddress);
1997
+ * }
1998
+ * }
1999
+ * ```
2000
+ */
2001
+ function isAccessControlError(error) {
2002
+ return error instanceof Error && "contractAddress" in error && (typeof error.contractAddress === "string" || error.contractAddress === void 0);
2003
+ }
2004
+ /**
2005
+ * Helper to safely extract error message from unknown error type
2006
+ *
2007
+ * @param error The error to extract message from
2008
+ * @returns The error message string
2009
+ *
2010
+ * @example
2011
+ * ```typescript
2012
+ * try {
2013
+ * await someOperation();
2014
+ * } catch (error) {
2015
+ * const message = getErrorMessage(error);
2016
+ * logger.error('Operation failed:', message);
2017
+ * }
2018
+ * ```
2019
+ */
2020
+ function getErrorMessage(error) {
2021
+ if (error instanceof Error) return error.message;
2022
+ return String(error);
2023
+ }
2024
+ /**
2025
+ * Helper to create a user-friendly error message with full context
2026
+ *
2027
+ * This function formats an AccessControlError into a readable multi-line message
2028
+ * that includes all relevant context (contract address, roles, operations, etc.).
2029
+ *
2030
+ * @param error The AccessControlError to format
2031
+ * @returns Formatted error message string with all context
2032
+ *
2033
+ * @example
2034
+ * ```typescript
2035
+ * import { PermissionDenied } from '@openzeppelin/ui-types';
2036
+ * import { formatAccessControlError } from '@openzeppelin/ui-utils';
2037
+ *
2038
+ * try {
2039
+ * await service.grantRole(...);
2040
+ * } catch (error) {
2041
+ * if (error instanceof PermissionDenied) {
2042
+ * const formatted = formatAccessControlError(error);
2043
+ * showErrorToUser(formatted);
2044
+ * }
2045
+ * }
2046
+ * ```
2047
+ *
2048
+ * Output format:
2049
+ * ```
2050
+ * [ErrorName] Error message
2051
+ * Contract: 0x123...
2052
+ * [Additional context based on error type]
2053
+ * ```
2054
+ */
2055
+ function formatAccessControlError(error) {
2056
+ let message = `[${error.name}] ${error.message}`;
2057
+ if (error.contractAddress) message += `\nContract: ${error.contractAddress}`;
2058
+ const errorWithProperties = error;
2059
+ if (error.name === "PermissionDenied") {
2060
+ if (errorWithProperties.requiredRole) message += `\nRequired Role: ${errorWithProperties.requiredRole}`;
2061
+ if (errorWithProperties.callerAddress) message += `\nCaller: ${errorWithProperties.callerAddress}`;
2062
+ } else if (error.name === "IndexerUnavailable") {
2063
+ if (errorWithProperties.networkId) message += `\nNetwork: ${errorWithProperties.networkId}`;
2064
+ if (errorWithProperties.endpointUrl) message += `\nEndpoint: ${errorWithProperties.endpointUrl}`;
2065
+ } else if (error.name === "ConfigurationInvalid") {
2066
+ if (errorWithProperties.configField) message += `\nInvalid Field: ${errorWithProperties.configField}`;
2067
+ if (errorWithProperties.providedValue !== void 0) message += `\nProvided Value: ${JSON.stringify(errorWithProperties.providedValue)}`;
2068
+ } else if (error.name === "OperationFailed") {
2069
+ if (errorWithProperties.operation) message += `\nOperation: ${errorWithProperties.operation}`;
2070
+ if (errorWithProperties.cause) message += `\nCause: ${errorWithProperties.cause.message}`;
2071
+ } else if (error.name === "UnsupportedContractFeatures") {
2072
+ if (errorWithProperties.missingFeatures && Array.isArray(errorWithProperties.missingFeatures)) message += `\nMissing Features: ${errorWithProperties.missingFeatures.join(", ")}`;
2073
+ }
2074
+ return message;
2075
+ }
2076
+
2077
+ //#endregion
2078
+ export { AnalyticsService, AppConfigService, DEFAULT_CONCURRENCY_LIMIT, UserExplorerConfigService, UserNetworkServiceConfigService, UserRpcConfigService, addressesEqual, appConfigService, base64ToBytes, buildRequiredInputSnapshot, bytesToHex, cn, compareSnapshots, computeEffectiveProviderPreference, createEmptySnapshot, delay, deserializeSnapshot, detectBytesEncoding, enhanceNumericValidation, findRoleAssignment, formatAccessControlError, formatTimestamp, generateId, getAllMembers, getBytesSize, getDefaultValueForType, getErrorMessage, getForcedService, getInvalidUrlMessage, getMissingRequiredContractInputs, getTotalMemberCount, hasMissingRequiredContractInputs, hasOwnership, hasRoles, hexToBytes, isAccessControlError, isDevelopmentOrTestEnvironment, isPlainObject, isProductionEnvironment, isRecordWithProperties, isValidUrl, logger, normalizeAddress, parseDeepLink, promiseAllSettledWithLimit, promiseAllWithLimit, rateLimitedBatch, requiredSnapshotsEqual, routerService, sanitizeHtml, serializeSnapshot, simpleHash, stringToBytes, truncateMiddle, userExplorerConfigService, userNetworkServiceConfigService, userRpcConfigService, validateBytes, validateBytesSimple, validateSnapshot, withTimeout };
2079
+ //# sourceMappingURL=index.js.map