@openzeppelin/ui-react 3.0.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { AnalyticsService, cn, isRecordWithProperties, logger } from "@openzeppelin/ui-utils";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { QueryClient, useQuery } from "@tanstack/react-query";
4
5
  import { Button } from "@openzeppelin/ui-components";
5
6
 
6
7
  //#region src/version.ts
7
- const VERSION = "3.0.1";
8
+ const VERSION = "3.2.0";
8
9
 
9
10
  //#endregion
10
11
  //#region src/hooks/AdapterContext.tsx
@@ -57,11 +58,31 @@ function RuntimeProvider({ children, resolveRuntime }) {
57
58
  const [loadingNetworks, setLoadingNetworks] = useState(/* @__PURE__ */ new Set());
58
59
  const runtimeRegistryRef = useRef(runtimeRegistry);
59
60
  const failedNetworksRef = useRef(/* @__PURE__ */ new Set());
61
+ const resolverGenerationRef = useRef(0);
62
+ const resolverInitializedRef = useRef(false);
60
63
  useEffect(() => {
61
64
  runtimeRegistryRef.current = runtimeRegistry;
62
65
  }, [runtimeRegistry]);
63
66
  useEffect(() => {
67
+ if (resolverInitializedRef.current) resolverGenerationRef.current += 1;
68
+ else resolverInitializedRef.current = true;
69
+ const registry = runtimeRegistryRef.current;
70
+ const runtimes = Object.values(registry);
71
+ const hadCachedRuntimes = runtimes.length > 0;
64
72
  failedNetworksRef.current = /* @__PURE__ */ new Set();
73
+ setLoadingNetworks(/* @__PURE__ */ new Set());
74
+ if (hadCachedRuntimes) {
75
+ setRuntimeRegistry({});
76
+ setTimeout(() => {
77
+ runtimes.forEach((runtime) => {
78
+ try {
79
+ runtime.dispose();
80
+ } catch (error) {
81
+ logger.error("RuntimeProvider", "Error disposing runtime during registry flush:", error);
82
+ }
83
+ });
84
+ }, 0);
85
+ }
65
86
  }, [resolveRuntime]);
66
87
  useEffect(() => {
67
88
  return () => {
@@ -149,7 +170,23 @@ function RuntimeProvider({ children, resolveRuntime }) {
149
170
  return newSet;
150
171
  });
151
172
  logger.info("RuntimeProvider", `Starting runtime initialization for network ${networkId} (${networkConfig.name})`);
173
+ const generation = resolverGenerationRef.current;
152
174
  resolveRuntime(networkConfig).then((runtime) => {
175
+ if (generation !== resolverGenerationRef.current) {
176
+ setTimeout(() => {
177
+ try {
178
+ runtime.dispose();
179
+ } catch (error) {
180
+ logger.error("RuntimeProvider", `Error disposing stale runtime for network ${networkId}:`, error);
181
+ }
182
+ }, 0);
183
+ setLoadingNetworks((prev) => {
184
+ const newSet = new Set(prev);
185
+ newSet.delete(networkId);
186
+ return newSet;
187
+ });
188
+ return;
189
+ }
153
190
  logger.info("RuntimeProvider", `Runtime for network ${networkId} loaded successfully`, {
154
191
  type: runtime.constructor.name,
155
192
  objectId: Object.prototype.toString.call(runtime)
@@ -164,6 +201,14 @@ function RuntimeProvider({ children, resolveRuntime }) {
164
201
  return newSet;
165
202
  });
166
203
  }).catch((error) => {
204
+ if (generation !== resolverGenerationRef.current) {
205
+ setLoadingNetworks((prev) => {
206
+ const newSet = new Set(prev);
207
+ newSet.delete(networkId);
208
+ return newSet;
209
+ });
210
+ return;
211
+ }
167
212
  logger.error("RuntimeProvider", `Error loading runtime for network ${networkId}:`, error);
168
213
  failedNetworksRef.current.add(networkId);
169
214
  setLoadingNetworks((prev) => {
@@ -244,12 +289,12 @@ const WALLET_STATE_CONTEXT_KEY = Symbol.for("@openzeppelin/ui-react/WalletStateC
244
289
  * 4. For React contexts specifically, having the same context object is what matters,
245
290
  * and this pattern guarantees that after initialization
246
291
  */
247
- function getOrCreateSharedContext() {
292
+ function getOrCreateSharedContext$1() {
248
293
  const global = globalThis;
249
294
  if (!global[WALLET_STATE_CONTEXT_KEY]) global[WALLET_STATE_CONTEXT_KEY] = createContext(void 0);
250
295
  return global[WALLET_STATE_CONTEXT_KEY];
251
296
  }
252
- const WalletStateContext = getOrCreateSharedContext();
297
+ const WalletStateContext = getOrCreateSharedContext$1();
253
298
  /**
254
299
  * Hook to access wallet state from WalletStateProvider.
255
300
  * @throws Error if used outside of WalletStateProvider
@@ -877,6 +922,625 @@ function useWalletReconnectionHandler(selectedNetworkConfigId, selectedCapabilit
877
922
  ]);
878
923
  }
879
924
 
925
+ //#endregion
926
+ //#region src/hooks/nameResolution/resolutionConfig.ts
927
+ /**
928
+ * Default resolution config. Values sit within the spec's "seconds-to-minutes"
929
+ * caching guidance; forward debounce is 300ms (typed char-by-char), reverse is 0.
930
+ */
931
+ const DEFAULT_CONFIG = {
932
+ staleTimeMs: 6e4,
933
+ gcTimeMs: 3e5,
934
+ forwardDebounceMs: 300,
935
+ reverseDebounceMs: 0,
936
+ transientRetryCount: 2
937
+ };
938
+ /** Stable prefix for every resolution cache key — namespaces this package's keys. */
939
+ const RESOLUTION_KEY_PREFIX = "oz-name-resolution";
940
+ const runtimeInstanceIds = /* @__PURE__ */ new WeakMap();
941
+ let nextRuntimeInstanceId = 1;
942
+ /**
943
+ * Assign a stable, serializable id to each {@link EcosystemRuntime} object identity.
944
+ * A disposed-and-recreated runtime (e.g. SF-4 opt-in toggle after INV-218 flush) gets
945
+ * a new id so resolution cache keys miss and results refresh (INV-230).
946
+ */
947
+ function getRuntimeInstanceId(runtime) {
948
+ if (!runtime) return 0;
949
+ let id = runtimeInstanceIds.get(runtime);
950
+ if (id === void 0) {
951
+ id = nextRuntimeInstanceId;
952
+ nextRuntimeInstanceId += 1;
953
+ runtimeInstanceIds.set(runtime, id);
954
+ }
955
+ return id;
956
+ }
957
+ /**
958
+ * Build a network-scoped, namespace-separated cache key (INV-40). A name resolves
959
+ * differently per network and provenance can be chain-scoped, so `networkId` is
960
+ * part of the key; `namespace` keeps forward and reverse lookups disjoint;
961
+ * `runtimeInstanceId` scopes entries to the active capability instance (INV-230).
962
+ *
963
+ * @param namespace - `'name'` (forward) or `'addr'` (reverse).
964
+ * @param networkId - Active network id (`''` when no network is selected).
965
+ * @param normalizedInput - Trimmed + lowercased input (INV-26).
966
+ * @param runtimeInstanceId - Active runtime instance discriminator (`0` when absent).
967
+ * @returns A readonly tuple suitable for a TanStack Query `queryKey`.
968
+ */
969
+ function buildResolutionKey(namespace, networkId, normalizedInput, runtimeInstanceId = 0) {
970
+ return [
971
+ RESOLUTION_KEY_PREFIX,
972
+ namespace,
973
+ networkId,
974
+ normalizedInput,
975
+ runtimeInstanceId
976
+ ];
977
+ }
978
+ /**
979
+ * Whether a resolution error is transient (worth retrying) versus a definitive,
980
+ * stable answer (INV-47). Exhaustive over SF-1's closed error union with no
981
+ * `default`: adding a new error code to `@openzeppelin/ui-types` will fail to
982
+ * compile here until it is classified — deliberately coupling SF-2 to SF-1 INV-7.
983
+ *
984
+ * @param error - A typed {@link NameResolutionError}.
985
+ * @returns `true` for transient failures (timeout / gateway / adapter), `false`
986
+ * for definitive negatives and unsupported-network answers.
987
+ */
988
+ function isTransientError(error) {
989
+ switch (error.code) {
990
+ case "RESOLUTION_TIMEOUT":
991
+ case "EXTERNAL_GATEWAY_ERROR":
992
+ case "ADAPTER_ERROR": return true;
993
+ case "NAME_NOT_FOUND":
994
+ case "ADDRESS_NOT_FOUND":
995
+ case "UNSUPPORTED_NAME":
996
+ case "UNSUPPORTED_NETWORK": return false;
997
+ }
998
+ }
999
+ /**
1000
+ * Construct a QueryClient tuned for name resolution: ambient refetch triggers are
1001
+ * disabled at the client level (INV-41) so a resolved value never silently
1002
+ * changes under the user on window focus or network reconnect.
1003
+ */
1004
+ function createResolutionQueryClient() {
1005
+ return new QueryClient({ defaultOptions: { queries: {
1006
+ refetchOnWindowFocus: false,
1007
+ refetchOnReconnect: false
1008
+ } } });
1009
+ }
1010
+ /**
1011
+ * Process-global key for the default resolution QueryClient. Uses `Symbol.for`
1012
+ * so every duplicated module instance (Vite pre-bundling, npm-installed adapters)
1013
+ * shares ONE client — the same cross-bundle-singleton pattern as
1014
+ * `WalletStateContext`. This is what makes the cache shared with zero wiring
1015
+ * (SC-001) and enables cross-instance dedup / warm-cache reuse (INV-33 / INV-37 / INV-48).
1016
+ */
1017
+ const DEFAULT_CLIENT_KEY = Symbol.for("@openzeppelin/ui-react/NameResolutionQueryClient");
1018
+ /**
1019
+ * Lazily create (exactly once) and return the process-global resolution
1020
+ * QueryClient used when no `NameResolutionProvider` is mounted (INV-48).
1021
+ *
1022
+ * @returns The shared default resolution QueryClient (stable across calls).
1023
+ */
1024
+ function getDefaultResolutionQueryClient() {
1025
+ const globalScope = globalThis;
1026
+ if (!globalScope[DEFAULT_CLIENT_KEY]) globalScope[DEFAULT_CLIENT_KEY] = createResolutionQueryClient();
1027
+ return globalScope[DEFAULT_CLIENT_KEY];
1028
+ }
1029
+
1030
+ //#endregion
1031
+ //#region src/hooks/nameResolution/NameResolutionContext.ts
1032
+ /**
1033
+ * Process-global key for the context object. Uses `Symbol.for` so all duplicated
1034
+ * module instances share ONE React context — the same cross-bundle-singleton
1035
+ * pattern as `WalletStateContext` (bundler pre-bundling / npm-installed adapters).
1036
+ */
1037
+ const CONTEXT_KEY = Symbol.for("@openzeppelin/ui-react/NameResolutionContext");
1038
+ function getOrCreateSharedContext() {
1039
+ const globalScope = globalThis;
1040
+ if (!globalScope[CONTEXT_KEY]) globalScope[CONTEXT_KEY] = createContext(null);
1041
+ return globalScope[CONTEXT_KEY];
1042
+ }
1043
+ /**
1044
+ * React context for resolution. `null` default triggers the module-singleton
1045
+ * fallback in {@link useNameResolutionContext} — no Provider is required (INV-48).
1046
+ */
1047
+ const NameResolutionContext = getOrCreateSharedContext();
1048
+ /**
1049
+ * Stable fallback value used when no Provider is mounted. Cached per module so the
1050
+ * returned reference is referentially stable across renders (INV-38); it wraps the
1051
+ * process-global default client, so caches are still shared even across bundles.
1052
+ */
1053
+ let fallbackValue;
1054
+ function getFallbackContextValue() {
1055
+ if (!fallbackValue) fallbackValue = {
1056
+ queryClient: getDefaultResolutionQueryClient(),
1057
+ config: DEFAULT_CONFIG
1058
+ };
1059
+ return fallbackValue;
1060
+ }
1061
+ /**
1062
+ * Read the resolution context, falling back to the zero-wiring default (global
1063
+ * singleton client + {@link DEFAULT_CONFIG}) when no `NameResolutionProvider` is
1064
+ * mounted (INV-48). Never throws for a missing provider.
1065
+ *
1066
+ * @returns The active {@link NameResolutionContextValue}.
1067
+ */
1068
+ function useNameResolutionContext() {
1069
+ return useContext(NameResolutionContext) ?? getFallbackContextValue();
1070
+ }
1071
+
1072
+ //#endregion
1073
+ //#region src/hooks/nameResolution/resolutionState.ts
1074
+ /**
1075
+ * Internal error used to bridge SF-1's `ok: false` results (and defensively-caught
1076
+ * adapter throws) into TanStack Query's thrown-error channel, carrying the typed
1077
+ * {@link NameResolutionError} through so the mapping step can surface it unchanged
1078
+ * (INV-43). Never leaks past the hook boundary.
1079
+ */
1080
+ var ResolutionQueryError = class extends Error {
1081
+ resolutionError;
1082
+ /** @param resolutionError - The typed error to carry through react-query. */
1083
+ constructor(resolutionError) {
1084
+ super(`name resolution failed: ${resolutionError.code}`);
1085
+ this.name = "ResolutionQueryError";
1086
+ this.resolutionError = resolutionError;
1087
+ }
1088
+ };
1089
+ /**
1090
+ * Convert any value from react-query's error channel into a typed, closed-union
1091
+ * {@link NameResolutionError} (INV-43). A {@link ResolutionQueryError} is
1092
+ * unwrapped verbatim; anything else (a react-query internal error) is mapped to
1093
+ * `ADAPTER_ERROR` so no untyped throw reaches a component.
1094
+ *
1095
+ * @param error - The raw `query.error` value (`unknown`).
1096
+ * @returns A typed error drawn only from SF-1's closed union.
1097
+ */
1098
+ function toNameResolutionError(error) {
1099
+ if (error instanceof ResolutionQueryError) return error.resolutionError;
1100
+ return {
1101
+ code: "ADAPTER_ERROR",
1102
+ message: error instanceof Error ? error.message : String(error),
1103
+ cause: error
1104
+ };
1105
+ }
1106
+ /**
1107
+ * Map a settled query's state to exactly one non-gate {@link EngineResult} arm
1108
+ * (INV-42, INV-44). The `input` echoed here is the debounced value the query was
1109
+ * keyed on, so `data`/`error` are always paired with the input that produced them
1110
+ * (INV-24). Success without data degrades to `loading` rather than a resolved arm
1111
+ * missing its payload.
1112
+ *
1113
+ * @param input - The debounced, normalized input keying this query.
1114
+ * @param query - The settled query state.
1115
+ * @param retry - Bound `refetch` for the `error` arm (INV-34).
1116
+ * @returns The `resolved`, `error`, or `loading` arm.
1117
+ */
1118
+ function mapSettledQuery(input, query, retry) {
1119
+ if (query.isSuccess && query.data !== void 0) return {
1120
+ status: "resolved",
1121
+ input,
1122
+ data: query.data
1123
+ };
1124
+ if (query.isError) return {
1125
+ status: "error",
1126
+ input,
1127
+ error: toNameResolutionError(query.error),
1128
+ retry
1129
+ };
1130
+ return {
1131
+ status: "loading",
1132
+ input
1133
+ };
1134
+ }
1135
+
1136
+ //#endregion
1137
+ //#region src/hooks/nameResolution/useResolutionEngine.ts
1138
+ const LOG_SCOPE = "useResolutionEngine";
1139
+ /** No-op retry for synthesized errors that never entered the query path (INV-45). */
1140
+ const NOOP_RETRY = () => {};
1141
+ /**
1142
+ * Shared engine behind `useResolveName` / `useResolveAddress`. Owns input
1143
+ * normalization, debouncing, the `useQuery` wiring (explicit-client form), the
1144
+ * capability-absence gate, and the mapping to a direction-agnostic
1145
+ * {@link EngineResult}. Not exported from the package.
1146
+ */
1147
+ function useResolutionEngine(params) {
1148
+ const { input, namespace, debounceMs, enabled, shouldAttempt, getMethod } = params;
1149
+ const walletState = useContext(WalletStateContext);
1150
+ const activeRuntime = walletState?.activeRuntime ?? null;
1151
+ const activeNetworkId = walletState?.activeNetworkId ?? null;
1152
+ const isRuntimeLoading = walletState?.isRuntimeLoading ?? false;
1153
+ const { queryClient, config } = useNameResolutionContext();
1154
+ const trimmedLive = (input ?? "").trim();
1155
+ const normalizedLive = trimmedLive.toLowerCase();
1156
+ const [debouncedTrimmed, setDebouncedTrimmed] = useState(trimmedLive);
1157
+ const seededRef = useRef(false);
1158
+ useEffect(() => {
1159
+ if (!seededRef.current) {
1160
+ seededRef.current = true;
1161
+ return;
1162
+ }
1163
+ if (debounceMs <= 0) {
1164
+ setDebouncedTrimmed(trimmedLive);
1165
+ return;
1166
+ }
1167
+ const timer = setTimeout(() => setDebouncedTrimmed(trimmedLive), debounceMs);
1168
+ return () => clearTimeout(timer);
1169
+ }, [trimmedLive, debounceMs]);
1170
+ const effectiveTrimmed = debounceMs <= 0 ? trimmedLive : debouncedTrimmed;
1171
+ const normalizedKey = effectiveTrimmed.toLowerCase();
1172
+ const capability = activeRuntime?.nameResolution;
1173
+ const networkId = activeNetworkId ?? "";
1174
+ const runtimeInstanceId = getRuntimeInstanceId(activeRuntime);
1175
+ const method = capability ? getMethod(capability) : void 0;
1176
+ const liveAttemptable = capability ? shouldAttempt(capability, normalizedLive) : false;
1177
+ const keyAttemptable = capability ? shouldAttempt(capability, normalizedKey) : false;
1178
+ const queryEnabled = enabled && trimmedLive !== "" && capability != null && method != null && normalizedKey !== "" && keyAttemptable;
1179
+ const query = useQuery({
1180
+ queryKey: buildResolutionKey(namespace, networkId, normalizedKey, runtimeInstanceId),
1181
+ queryFn: async () => {
1182
+ if (!method) throw new ResolutionQueryError({
1183
+ code: "ADAPTER_ERROR",
1184
+ message: "resolution method unexpectedly missing"
1185
+ });
1186
+ const adapterArg = namespace === "addr" ? effectiveTrimmed : normalizedKey;
1187
+ let result;
1188
+ try {
1189
+ result = await method(adapterArg);
1190
+ } catch (cause) {
1191
+ logger.warn(LOG_SCOPE, "resolution method threw instead of returning ok:false", cause);
1192
+ throw new ResolutionQueryError({
1193
+ code: "ADAPTER_ERROR",
1194
+ message: cause instanceof Error ? cause.message : String(cause),
1195
+ cause
1196
+ });
1197
+ }
1198
+ if (!result.ok) throw new ResolutionQueryError(result.error);
1199
+ return result.value;
1200
+ },
1201
+ enabled: queryEnabled,
1202
+ staleTime: config.staleTimeMs,
1203
+ gcTime: config.gcTimeMs,
1204
+ refetchOnWindowFocus: false,
1205
+ refetchOnReconnect: false,
1206
+ retry: (failureCount, error) => (error instanceof ResolutionQueryError ? isTransientError(error.resolutionError) : true) && failureCount < config.transientRetryCount
1207
+ }, queryClient);
1208
+ if (!enabled) return { status: "idle" };
1209
+ if (trimmedLive === "") return { status: "idle" };
1210
+ if (capability) {
1211
+ if (!liveAttemptable) return { status: "idle" };
1212
+ if (!method) return unsupportedNetwork(normalizedLive, networkId);
1213
+ if (normalizedKey !== normalizedLive) return {
1214
+ status: "debouncing",
1215
+ input: normalizedLive
1216
+ };
1217
+ } else {
1218
+ if (isRuntimeLoading) return {
1219
+ status: "loading",
1220
+ input: normalizedLive
1221
+ };
1222
+ return unsupportedNetwork(normalizedLive, networkId);
1223
+ }
1224
+ const retry = () => {
1225
+ query.refetch();
1226
+ };
1227
+ return mapSettledQuery(normalizedKey, query, retry);
1228
+ }
1229
+ /**
1230
+ * Synthesize an `UNSUPPORTED_NETWORK` error arm without touching react-query
1231
+ * (INV-45). Drawn from SF-1's closed union so it is indistinguishable from an
1232
+ * adapter-returned `UNSUPPORTED_NETWORK`; `networkId` may be `''` (INV-49). Retry
1233
+ * is a no-op — there is no query to refetch.
1234
+ */
1235
+ function unsupportedNetwork(input, networkId) {
1236
+ return {
1237
+ status: "error",
1238
+ input,
1239
+ error: {
1240
+ code: "UNSUPPORTED_NETWORK",
1241
+ networkId
1242
+ },
1243
+ retry: NOOP_RETRY
1244
+ };
1245
+ }
1246
+
1247
+ //#endregion
1248
+ //#region src/hooks/nameResolution/useResolveName.ts
1249
+ /**
1250
+ * Forward-resolve a name to an address. Soft-reads the active runtime from
1251
+ * `WalletStateContext` (never throws when no provider is mounted — degrades to
1252
+ * idle / UNSUPPORTED_NETWORK) and calls `runtime.nameResolution?.resolveName`.
1253
+ * Debounces `name`, caches per (network, name) via the owned QueryClient, and is
1254
+ * protected against out-of-order responses (distinct inputs → distinct query keys).
1255
+ *
1256
+ * Returns a discriminated union keyed on `status`; it never throws for expected
1257
+ * failures (they surface in the `error` arm) and never pairs a name with a
1258
+ * different name's resolved address (INV-24).
1259
+ *
1260
+ * @param name - The name to resolve. `null` / empty / a value failing the
1261
+ * adapter's `isValidName` yields `status: 'idle'` (never `error`).
1262
+ * @param options - `debounceMs` / `enabled` overrides.
1263
+ * @returns The current {@link UseResolveNameResult}.
1264
+ */
1265
+ function useResolveName(name, options) {
1266
+ const { config } = useNameResolutionContext();
1267
+ return toNameResult(useResolutionEngine({
1268
+ input: name,
1269
+ namespace: "name",
1270
+ debounceMs: options?.debounceMs ?? config.forwardDebounceMs,
1271
+ enabled: options?.enabled ?? true,
1272
+ shouldAttempt: (cap, normalized) => cap.isValidName(normalized),
1273
+ getMethod: (cap) => cap.resolveName
1274
+ }));
1275
+ }
1276
+ /**
1277
+ * Remap the engine's generic `input` to `name` (INV-24: keyed off the debounced
1278
+ * input carried in the engine result, not the live prop). Exhaustive over the
1279
+ * union so a new status cannot silently fall through (INV-23 / INV-42).
1280
+ */
1281
+ function toNameResult(engine) {
1282
+ switch (engine.status) {
1283
+ case "idle": return { status: "idle" };
1284
+ case "debouncing": return {
1285
+ status: "debouncing",
1286
+ name: engine.input
1287
+ };
1288
+ case "loading": return {
1289
+ status: "loading",
1290
+ name: engine.input
1291
+ };
1292
+ case "resolved": return {
1293
+ status: "resolved",
1294
+ name: engine.input,
1295
+ data: engine.data
1296
+ };
1297
+ case "error": return {
1298
+ status: "error",
1299
+ name: engine.input,
1300
+ error: engine.error,
1301
+ retry: engine.retry
1302
+ };
1303
+ }
1304
+ }
1305
+
1306
+ //#endregion
1307
+ //#region src/hooks/nameResolution/useResolveAddress.ts
1308
+ /**
1309
+ * Reverse-resolve an address to a name. Soft-reads the active runtime from
1310
+ * `WalletStateContext` (never throws when no provider is mounted — degrades to
1311
+ * idle / UNSUPPORTED_NETWORK) and calls `runtime.nameResolution?.resolveAddress`.
1312
+ * Caches per (network, address) via the owned QueryClient. Not debounced by
1313
+ * default — addresses are pasted, not typed char-by-char.
1314
+ *
1315
+ * Applies no client-side address-shape check (INV-32): resolution is attempted on
1316
+ * any non-empty input and malformed addresses are rejected via the adapter's typed
1317
+ * error union. Address-shape validation is SF-3's concern.
1318
+ *
1319
+ * @param address - The address to reverse-resolve. `null` / empty yields
1320
+ * `status: 'idle'`.
1321
+ * @param options - `debounceMs` / `enabled` overrides.
1322
+ * @returns The current {@link UseResolveAddressResult}.
1323
+ */
1324
+ function useResolveAddress(address, options) {
1325
+ const { config } = useNameResolutionContext();
1326
+ return toAddressResult(useResolutionEngine({
1327
+ input: address,
1328
+ namespace: "addr",
1329
+ debounceMs: options?.debounceMs ?? config.reverseDebounceMs,
1330
+ enabled: options?.enabled ?? true,
1331
+ shouldAttempt: () => true,
1332
+ getMethod: (cap) => cap.resolveAddress
1333
+ }));
1334
+ }
1335
+ /**
1336
+ * Remap the engine's generic `input` to `address` (INV-24). A `debouncing` arm —
1337
+ * only reachable when a caller passes a non-zero reverse `debounceMs` — collapses
1338
+ * to `loading` so {@link UseResolveAddressResult} keeps no `debouncing` variant.
1339
+ */
1340
+ function toAddressResult(engine) {
1341
+ switch (engine.status) {
1342
+ case "idle": return { status: "idle" };
1343
+ case "debouncing":
1344
+ case "loading": return {
1345
+ status: "loading",
1346
+ address: engine.input
1347
+ };
1348
+ case "resolved": return {
1349
+ status: "resolved",
1350
+ address: engine.input,
1351
+ data: engine.data
1352
+ };
1353
+ case "error": return {
1354
+ status: "error",
1355
+ address: engine.input,
1356
+ error: engine.error,
1357
+ retry: engine.retry
1358
+ };
1359
+ }
1360
+ }
1361
+
1362
+ //#endregion
1363
+ //#region src/hooks/nameResolution/NameResolutionProvider.tsx
1364
+ /**
1365
+ * Optional provider that overrides resolution config and/or the QueryClient for
1366
+ * its subtree. It does NOT render a `QueryClientProvider`: the client is passed
1367
+ * explicitly to `useQuery` (INV-48), so no ambient react-query provider is needed.
1368
+ *
1369
+ * Config is merged field-by-field over {@link DEFAULT_CONFIG} (INV-30), and the
1370
+ * context value is memoized so a Provider re-render does not cascade to every hook
1371
+ * (INV-38).
1372
+ */
1373
+ function NameResolutionProvider({ children, config, queryClient }) {
1374
+ const value = useMemo(() => ({
1375
+ queryClient: queryClient ?? getDefaultResolutionQueryClient(),
1376
+ config: {
1377
+ ...DEFAULT_CONFIG,
1378
+ ...config
1379
+ }
1380
+ }), [queryClient, config]);
1381
+ return /* @__PURE__ */ jsx(NameResolutionContext.Provider, {
1382
+ value,
1383
+ children
1384
+ });
1385
+ }
1386
+
1387
+ //#endregion
1388
+ //#region src/hooks/nameResolution/useRuntimeNameResolver.ts
1389
+ /**
1390
+ * Stable empty resolver returned when no runtime / capability is available.
1391
+ * No methods ⇒ the consuming field treats a typed name as unsupported and
1392
+ * surfaces `UNSUPPORTED_NETWORK` (INV-119) while hex behavior stays legacy.
1393
+ */
1394
+ const EMPTY_RESOLVER = {};
1395
+ /**
1396
+ * Project the active runtime's `NameResolutionCapability` into the injected
1397
+ * {@link NameResolver} seam consumed by `@openzeppelin/ui-components` (SF-3, D3).
1398
+ * Mounted ambiently by the renderer:
1399
+ *
1400
+ * ```tsx
1401
+ * <NameResolverProvider {...useRuntimeNameResolver()}>{form}</NameResolverProvider>
1402
+ * ```
1403
+ *
1404
+ * Each imperative call is backed by SF-2's **owned** resolution `QueryClient`
1405
+ * via `fetchQuery`, reusing SF-2's exact query-key convention
1406
+ * (`buildResolutionKey('name', networkId, normalizedName)`) — so a name
1407
+ * resolved through the field and the same name resolved by a bare
1408
+ * `useResolveName` hit the SAME cache entry: shared cache, dedupe, and
1409
+ * out-of-order safety, never a parallel cache (INV-119).
1410
+ *
1411
+ * Degradation, in order (all method-omission, never a throw):
1412
+ * - No `WalletStateProvider` mounted → empty resolver. The context is read
1413
+ * directly (not via `useWalletState()`, which throws) so the ambient
1414
+ * renderer mount stays safe for wallet-less usage.
1415
+ * - No active runtime, or runtime without the capability → empty resolver.
1416
+ * - Capability without `resolveName` → `isValidName` only; forward unsupported.
1417
+ *
1418
+ * When the capability is present but the network itself is unsupported, the
1419
+ * adapter's `resolveName` resolves `{ ok: false, error: UNSUPPORTED_NETWORK }`
1420
+ * and that result passes through unchanged.
1421
+ *
1422
+ * @returns A referentially stable {@link NameResolver} for the active runtime.
1423
+ */
1424
+ function useRuntimeNameResolver() {
1425
+ const walletState = useContext(WalletStateContext);
1426
+ const { queryClient, config } = useNameResolutionContext();
1427
+ const activeRuntime = walletState?.activeRuntime ?? null;
1428
+ const capability = activeRuntime?.nameResolution;
1429
+ const networkId = walletState?.activeNetworkId ?? "";
1430
+ const runtimeInstanceId = getRuntimeInstanceId(activeRuntime);
1431
+ return useMemo(() => {
1432
+ if (!capability) return EMPTY_RESOLVER;
1433
+ const resolveNameMethod = capability.resolveName;
1434
+ return {
1435
+ isValidName: (name) => capability.isValidName(name),
1436
+ resolveName: resolveNameMethod ? async (name) => {
1437
+ const normalized = name.trim().toLowerCase();
1438
+ try {
1439
+ return {
1440
+ ok: true,
1441
+ value: await queryClient.fetchQuery({
1442
+ queryKey: buildResolutionKey("name", networkId, normalized, runtimeInstanceId),
1443
+ queryFn: async () => {
1444
+ let result;
1445
+ try {
1446
+ result = await resolveNameMethod(normalized);
1447
+ } catch (cause) {
1448
+ throw new ResolutionQueryError({
1449
+ code: "ADAPTER_ERROR",
1450
+ message: cause instanceof Error ? cause.message : String(cause),
1451
+ cause
1452
+ });
1453
+ }
1454
+ if (!result.ok) throw new ResolutionQueryError(result.error);
1455
+ return result.value;
1456
+ },
1457
+ staleTime: config.staleTimeMs,
1458
+ gcTime: config.gcTimeMs,
1459
+ retry: (failureCount, error) => (error instanceof ResolutionQueryError ? isTransientError(error.resolutionError) : true) && failureCount < config.transientRetryCount
1460
+ })
1461
+ };
1462
+ } catch (error) {
1463
+ return {
1464
+ ok: false,
1465
+ error: toNameResolutionError(error)
1466
+ };
1467
+ }
1468
+ } : void 0
1469
+ };
1470
+ }, [
1471
+ capability,
1472
+ queryClient,
1473
+ config,
1474
+ networkId,
1475
+ runtimeInstanceId
1476
+ ]);
1477
+ }
1478
+
1479
+ //#endregion
1480
+ //#region src/hooks/runtime/runtimeCreationConfig.ts
1481
+ /** Safe default: miss-fallback OFF; no uiKit override. INV-204 */
1482
+ const DEFAULT_RUNTIME_CREATION_CONFIG = {};
1483
+ /**
1484
+ * Normalizes the opt-in flag to the adapter's strict-enable contract.
1485
+ * UIKit uses this at the threading boundary so `false` and `undefined` never
1486
+ * emit `enableMainnetL1MissFallback: false` into adapter options (absent = OFF).
1487
+ */
1488
+ function isMainnetL1MissFallbackEnabled(options) {
1489
+ return options?.enableMainnetL1MissFallback === true;
1490
+ }
1491
+ /**
1492
+ * Builds the `createRuntime` options bag for adapter threading.
1493
+ * When OFF, `nameResolution` is omitted (INV-207). When ON, spreads only
1494
+ * `{ enableMainnetL1MissFallback: true }` (INV-208).
1495
+ */
1496
+ function buildCreateRuntimeOptions(options) {
1497
+ return {
1498
+ ...options?.uiKit !== void 0 ? { uiKit: options.uiKit } : {},
1499
+ ...isMainnetL1MissFallbackEnabled(options?.nameResolution) ? { nameResolution: { enableMainnetL1MissFallback: true } } : {}
1500
+ };
1501
+ }
1502
+
1503
+ //#endregion
1504
+ //#region src/hooks/runtime/createResolveRuntime.ts
1505
+ /**
1506
+ * Build a `resolveRuntime` callback for {@link RuntimeProvider} that forwards
1507
+ * `CreateRuntimeOptions` to `ecosystemDefinition.createRuntime`.
1508
+ *
1509
+ * Does not cache — {@link RuntimeProvider} owns the per-network singleton registry.
1510
+ */
1511
+ function createResolveRuntime(ecosystemDefinition, params) {
1512
+ const { profile, options } = params;
1513
+ const mergedOptions = buildCreateRuntimeOptions(options);
1514
+ return (networkConfig) => Promise.resolve(ecosystemDefinition.createRuntime(profile, networkConfig, mergedOptions));
1515
+ }
1516
+
1517
+ //#endregion
1518
+ //#region src/hooks/runtime/useResolveRuntime.ts
1519
+ /**
1520
+ * Memoized {@link createResolveRuntime} for React apps. Recompute when
1521
+ * `ecosystemDefinition`, `profile`, or opt-in posture change (INV-217, INV-222).
1522
+ *
1523
+ * Changing the returned function identity MUST trigger runtime registry flush
1524
+ * in {@link RuntimeProvider} (INV-218).
1525
+ */
1526
+ function useResolveRuntime(ecosystemDefinition, params) {
1527
+ const { profile, options } = params;
1528
+ const uiKit = options?.uiKit;
1529
+ const enableMainnetL1MissFallback = options?.nameResolution?.enableMainnetL1MissFallback;
1530
+ return useMemo(() => createResolveRuntime(ecosystemDefinition, {
1531
+ profile,
1532
+ options: {
1533
+ ...uiKit !== void 0 ? { uiKit } : {},
1534
+ ...enableMainnetL1MissFallback === true ? { nameResolution: { enableMainnetL1MissFallback: true } } : {}
1535
+ }
1536
+ }), [
1537
+ ecosystemDefinition,
1538
+ profile,
1539
+ uiKit,
1540
+ enableMainnetL1MissFallback
1541
+ ]);
1542
+ }
1543
+
880
1544
  //#endregion
881
1545
  //#region src/components/WalletConnectionUI.tsx
882
1546
  /**
@@ -1084,5 +1748,5 @@ const NetworkSwitchManager = ({ wallet, networkCatalog, targetNetworkId, onNetwo
1084
1748
  };
1085
1749
 
1086
1750
  //#endregion
1087
- export { AnalyticsContext, AnalyticsProvider, NetworkSwitchManager, RuntimeContext, RuntimeProvider, VERSION, WalletConnectionHeader, WalletConnectionUI, WalletStateContext, WalletStateProvider, useAnalytics, useDerivedAccountStatus, useDerivedChainInfo, useDerivedConnectStatus, useDerivedDisconnect, useDerivedSwitchChainStatus, useRuntimeContext, useWalletComponents, useWalletReconnectionHandler, useWalletState };
1751
+ export { AnalyticsContext, AnalyticsProvider, DEFAULT_CONFIG, DEFAULT_RUNTIME_CREATION_CONFIG, NameResolutionContext, NameResolutionProvider, NetworkSwitchManager, RuntimeContext, RuntimeProvider, VERSION, WalletConnectionHeader, WalletConnectionUI, WalletStateContext, WalletStateProvider, createResolveRuntime, isMainnetL1MissFallbackEnabled, useAnalytics, useDerivedAccountStatus, useDerivedChainInfo, useDerivedConnectStatus, useDerivedDisconnect, useDerivedSwitchChainStatus, useNameResolutionContext, useResolveAddress, useResolveName, useResolveRuntime, useRuntimeContext, useRuntimeNameResolver, useWalletComponents, useWalletReconnectionHandler, useWalletState };
1088
1752
  //# sourceMappingURL=index.mjs.map