@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/LICENSE +661 -0
- package/README.md +106 -0
- package/dist/index-DNIN-Squ.d.cts +1205 -0
- package/dist/index-DNIN-Squ.d.cts.map +1 -0
- package/dist/index-qy1AQMhr.d.ts +1205 -0
- package/dist/index-qy1AQMhr.d.ts.map +1 -0
- package/dist/index.cjs +2167 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1205 -0
- package/dist/index.d.ts +1205 -0
- package/dist/index.js +2079 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
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
|