@tangle-network/blueprint-ui 0.1.0 → 0.1.2

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.
Files changed (48) hide show
  1. package/README.md +47 -11
  2. package/dist/BlueprintHostPanel-6iVEh-f1.d.ts +39 -0
  3. package/dist/chunk-37ADATBT.js +55 -0
  4. package/dist/chunk-37ADATBT.js.map +1 -0
  5. package/dist/chunk-A6PJT5YQ.js +1180 -0
  6. package/dist/chunk-A6PJT5YQ.js.map +1 -0
  7. package/dist/chunk-GD3AZEJL.js +327 -0
  8. package/dist/chunk-GD3AZEJL.js.map +1 -0
  9. package/dist/components.d.ts +179 -0
  10. package/dist/components.js +1127 -0
  11. package/dist/components.js.map +1 -0
  12. package/dist/host.d.ts +96 -0
  13. package/dist/host.js +39 -0
  14. package/dist/host.js.map +1 -0
  15. package/dist/index.d.ts +8470 -0
  16. package/dist/index.js +841 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/preset.d.ts +60 -0
  19. package/dist/preset.js +7 -0
  20. package/dist/preset.js.map +1 -0
  21. package/dist/registry-JhwB9BPD.d.ts +87 -0
  22. package/dist/styles.css +559 -0
  23. package/package.json +42 -13
  24. package/src/components/layout/AppDocument.tsx +1 -1
  25. package/src/components/layout/ChainSwitcher.tsx +27 -10
  26. package/src/components/layout/Web3Shell.tsx +81 -6
  27. package/src/components/web3/ConnectWalletCta.tsx +21 -0
  28. package/src/components.ts +6 -0
  29. package/src/contracts/abi.ts +10 -1
  30. package/src/contracts/chains.ts +23 -10
  31. package/src/contracts/publicClient.ts +52 -10
  32. package/src/hooks/useOperators.ts +203 -96
  33. package/src/hooks/useProvisionProgress.ts +2 -1
  34. package/src/hooks/useQuotes.ts +69 -14
  35. package/src/hooks/useSessionAuth.ts +2 -1
  36. package/src/hooks/useSidecarAuth.ts +173 -0
  37. package/src/hooks/useWagmiSidecarAuth.ts +11 -0
  38. package/src/hooks/useWalletEthBalance.ts +68 -0
  39. package/src/host/components/BlueprintHostHero.tsx +91 -0
  40. package/src/host/components/BlueprintHostPanel.tsx +24 -0
  41. package/src/host/index.ts +42 -0
  42. package/src/host/resolver.ts +204 -0
  43. package/src/host/types.ts +111 -0
  44. package/src/index.ts +48 -2
  45. package/src/stores/infra.ts +3 -2
  46. package/src/styles.css +128 -0
  47. package/src/utils/env.ts +22 -0
  48. package/src/utils/web3.ts +9 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/blueprint-ui",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Shared blueprint UI components, hooks, and contract utilities for Tangle Network apps",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -12,9 +12,13 @@
12
12
  "url": "https://github.com/tangle-network/blueprint-ui/issues"
13
13
  },
14
14
  "type": "module",
15
- "main": "./src/index.ts",
16
- "types": "./src/index.ts",
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "sideEffects": [
18
+ "**/*.css"
19
+ ],
17
20
  "files": [
21
+ "dist",
18
22
  "src",
19
23
  "README.md",
20
24
  "package.json",
@@ -24,16 +28,37 @@
24
28
  "access": "public"
25
29
  },
26
30
  "exports": {
27
- ".": "./src/index.ts",
28
- "./components": "./src/components.ts",
29
- "./preset": "./src/preset.ts"
31
+ ".": {
32
+ "import": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "./host": {
37
+ "import": "./dist/host.js",
38
+ "types": "./dist/host.d.ts",
39
+ "default": "./dist/host.js"
40
+ },
41
+ "./components": {
42
+ "import": "./dist/components.js",
43
+ "types": "./dist/components.d.ts",
44
+ "default": "./dist/components.js"
45
+ },
46
+ "./preset": {
47
+ "import": "./dist/preset.js",
48
+ "types": "./dist/preset.d.ts",
49
+ "default": "./dist/preset.js"
50
+ },
51
+ "./styles.css": "./dist/styles.css"
30
52
  },
31
53
  "scripts": {
32
- "typecheck": "tsc --noEmit"
54
+ "build": "tsup && node scripts/build-styles.mjs",
55
+ "dev": "tsup --watch",
56
+ "typecheck": "tsc --noEmit",
57
+ "prepack": "npm run build"
33
58
  },
34
59
  "peerDependencies": {
35
60
  "@tanstack/react-query": "^5.0.0",
36
- "@nanostores/react": "^0.7.0",
61
+ "@nanostores/react": "^0.7.0 || ^1.0.0",
37
62
  "@radix-ui/react-dialog": "^1.1.0",
38
63
  "@radix-ui/react-select": "^2.1.0",
39
64
  "@radix-ui/react-separator": "^1.1.0",
@@ -44,14 +69,14 @@
44
69
  "class-variance-authority": "^0.7.0",
45
70
  "clsx": "^2.1.0",
46
71
  "framer-motion": "^12.0.0",
47
- "nanostores": "^0.10.0",
48
- "react": "^19.0.0",
49
- "react-dom": "^19.0.0",
72
+ "nanostores": "^0.10.0 || ^1.0.0",
73
+ "react": "^18.0.0 || ^19.0.0",
74
+ "react-dom": "^18.0.0 || ^19.0.0",
50
75
  "react-router": "^7.0.0",
51
76
  "sonner": "^2.0.0",
52
- "tailwind-merge": "^3.2.0",
77
+ "tailwind-merge": "^2.6.0 || ^3.2.0",
53
78
  "viem": "^2.31.0",
54
- "wagmi": "^3.3.0"
79
+ "wagmi": "^2.19.0 || ^3.3.0"
55
80
  },
56
81
  "peerDependenciesMeta": {},
57
82
  "devDependencies": {
@@ -65,6 +90,7 @@
65
90
  "@radix-ui/react-tooltip": "^1.2.8",
66
91
  "@types/react": "18.3.1",
67
92
  "@types/react-dom": "18.3.1",
93
+ "@iconify-json/ph": "^1.2.2",
68
94
  "blo": "^2.0.0",
69
95
  "class-variance-authority": "^0.7.1",
70
96
  "clsx": "^2.1.1",
@@ -75,7 +101,10 @@
75
101
  "react-router": "^7.13.0",
76
102
  "sonner": "^2.0.7",
77
103
  "tailwind-merge": "^3.5.0",
104
+ "tsup": "^8.5.0",
78
105
  "typescript": "^5.5.2",
106
+ "unocss": "^66.5.4",
107
+ "unocss-preset-animations": "^1.1.1",
79
108
  "viem": "^2.46.2",
80
109
  "wagmi": "^3.5.0"
81
110
  }
@@ -22,7 +22,7 @@ function buildInlineThemeCode(themeStorageKeys: [string, string]): string {
22
22
 
23
23
  export function AppDocument({ children, description, themeStorageKeys }: AppDocumentProps) {
24
24
  return (
25
- <html lang="en" data-theme="dark">
25
+ <html lang="en" data-theme="dark" suppressHydrationWarning>
26
26
  <head>
27
27
  <meta charSet="utf-8" />
28
28
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -1,15 +1,32 @@
1
1
  import { useState, useRef, useEffect } from 'react';
2
2
  import { useStore } from '@nanostores/react';
3
3
  import { selectedChainIdStore } from '../../contracts/publicClient';
4
- import { getNetworks, tangleLocal, tangleTestnet, tangleMainnet } from '../../contracts/chains';
4
+ import { getNetworks } from '../../contracts/chains';
5
5
 
6
- const chainOrder = [tangleLocal.id, tangleTestnet.id, tangleMainnet.id];
6
+ function chainIcon(label: string | undefined, chainName: string | undefined): string {
7
+ if (label === 'Tangle Local' || chainName === 'Tangle Local') return 'i-ph:desktop';
8
+ if (label === 'Tangle Testnet' || chainName === 'Tangle Testnet') return 'i-ph:flask';
9
+ if (label === 'Tangle Mainnet' || chainName === 'Tangle') return 'i-ph:globe-hemisphere-west';
10
+ return 'i-ph:globe';
11
+ }
12
+
13
+ function orderedChainIds(): number[] {
14
+ const priority: Record<string, number> = {
15
+ 'Tangle Local': 0,
16
+ 'Tangle Testnet': 1,
17
+ 'Tangle Mainnet': 2,
18
+ Tangle: 2,
19
+ };
7
20
 
8
- const chainIcons: Record<number, string> = {
9
- [tangleLocal.id]: 'i-ph:desktop',
10
- [tangleTestnet.id]: 'i-ph:flask',
11
- [tangleMainnet.id]: 'i-ph:globe-hemisphere-west',
12
- };
21
+ return Object.entries(getNetworks())
22
+ .sort(([, a], [, b]) => {
23
+ const aPriority = priority[a?.label ?? a?.chain?.name ?? ''] ?? 99;
24
+ const bPriority = priority[b?.label ?? b?.chain?.name ?? ''] ?? 99;
25
+ if (aPriority !== bPriority) return aPriority - bPriority;
26
+ return a.chain.id - b.chain.id;
27
+ })
28
+ .map(([chainId]) => Number(chainId));
29
+ }
13
30
 
14
31
  export function ChainSwitcher() {
15
32
  const [open, setOpen] = useState(false);
@@ -38,7 +55,7 @@ export function ChainSwitcher() {
38
55
  className="flex items-center gap-1.5 px-2.5 py-2 rounded-lg text-xs font-data font-medium bg-bp-elements-background-depth-3 dark:bg-bp-elements-background-depth-4 border border-bp-elements-borderColor hover:border-bp-elements-borderColorActive/40 transition-all"
39
56
  title={current?.label ?? 'Select network'}
40
57
  >
41
- <div className={`${chainIcons[selectedChainId] ?? 'i-ph:globe'} text-sm text-bp-elements-icon-success`} />
58
+ <div className={`${chainIcon(current?.label, current?.chain?.name)} text-sm text-bp-elements-icon-success`} />
42
59
  <span className="hidden sm:inline text-bp-elements-textSecondary">{current?.shortLabel ?? 'Unknown'}</span>
43
60
  <div className={`i-ph:caret-down text-[10px] text-bp-elements-textTertiary transition-transform ${open ? 'rotate-180' : ''}`} />
44
61
  </button>
@@ -46,7 +63,7 @@ export function ChainSwitcher() {
46
63
  {open && (
47
64
  <div className="absolute right-0 top-full mt-2 w-48 glass-card-strong rounded-xl border border-bp-elements-dividerColor/50 py-1.5 z-50 shadow-lg">
48
65
  <div className="px-3 py-1.5 text-[10px] font-data uppercase tracking-wider text-bp-elements-textTertiary">Network</div>
49
- {chainOrder.map((chainId) => {
66
+ {orderedChainIds().map((chainId) => {
50
67
  const net = getNetworks()[chainId];
51
68
  if (!net) return null;
52
69
  const isSelected = chainId === selectedChainId;
@@ -58,7 +75,7 @@ export function ChainSwitcher() {
58
75
  isSelected ? 'bg-violet-500/10 text-violet-700 dark:text-violet-400' : 'hover:bg-bp-elements-item-backgroundHover text-bp-elements-textSecondary'
59
76
  }`}
60
77
  >
61
- <div className={`${chainIcons[chainId] ?? 'i-ph:globe'} text-sm ${isSelected ? 'text-violet-700 dark:text-violet-400' : 'text-bp-elements-textTertiary'}`} />
78
+ <div className={`${chainIcon(net.label, net.chain.name)} text-sm ${isSelected ? 'text-violet-700 dark:text-violet-400' : 'text-bp-elements-textTertiary'}`} />
62
79
  <span className="text-sm font-display font-medium">{net.label}</span>
63
80
  {isSelected && <div className="i-ph:check-bold text-xs ml-auto text-violet-700 dark:text-violet-400" />}
64
81
  </button>
@@ -1,13 +1,83 @@
1
1
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
- import { type ReactNode, useState } from 'react';
3
- import { type Config, WagmiProvider } from 'wagmi';
2
+ import { type ReactNode, useEffect, useRef, useState } from 'react';
3
+ import { type Config, WagmiContext, useReconnect } from 'wagmi';
4
4
 
5
5
  interface Web3ShellProps {
6
6
  config: Config;
7
+ reconnectOnMount?: boolean;
7
8
  children: ReactNode;
8
9
  }
9
10
 
10
- export function Web3Shell({ config, children }: Web3ShellProps) {
11
+ type RecentConnectorStorage = Pick<NonNullable<Config['storage']>, 'getItem' | 'removeItem'>;
12
+
13
+ // We prefer the last successful connector so silent restoration is fast and
14
+ // doesn't probe every available connector on startup.
15
+ async function findRecentConnector(currentConfig: Pick<Config, 'connectors' | 'storage'>) {
16
+ const storage = currentConfig.storage as RecentConnectorStorage | null | undefined;
17
+ const recentConnectorId = await storage?.getItem('recentConnectorId');
18
+ if (!recentConnectorId) return null;
19
+
20
+ const connector = currentConfig.connectors.find((candidate) => candidate.id === recentConnectorId) ?? null;
21
+ if (!connector) {
22
+ await storage?.removeItem('recentConnectorId');
23
+ }
24
+
25
+ return connector;
26
+ }
27
+
28
+ function ReconnectOnMount({
29
+ config,
30
+ reconnectOnMount,
31
+ }: Pick<Web3ShellProps, 'config' | 'reconnectOnMount'>) {
32
+ const { reconnectAsync } = useReconnect();
33
+ const attemptedRef = useRef(false);
34
+
35
+ useEffect(() => {
36
+ if (!reconnectOnMount || attemptedRef.current) return;
37
+ attemptedRef.current = true;
38
+
39
+ let cancelled = false;
40
+
41
+ // Why this exists:
42
+ // - In this app stack, wagmi's built-in provider hydrate path can kick off
43
+ // reconnect work during provider rerenders.
44
+ // - Route navigation rerenders the app shell, so wallet restore was being
45
+ // retriggered on page changes, which made the header look like it was
46
+ // reconnecting and added several seconds of delay.
47
+ //
48
+ // Our fix is to keep wagmi context/query wiring here, but move restore to a
49
+ // single effect guarded by a ref. A rerender keeps the same ref value, so
50
+ // reconnect only runs once per real mount and still runs again after a full
51
+ // page refresh.
52
+ void (async () => {
53
+ try {
54
+ const connector = await findRecentConnector(config);
55
+ if (cancelled) return;
56
+
57
+ if (connector) {
58
+ await reconnectAsync({ connectors: [connector] });
59
+ return;
60
+ }
61
+
62
+ await reconnectAsync();
63
+ } catch {
64
+ // A failed silent reconnect should leave the app disconnected.
65
+ }
66
+ })();
67
+
68
+ return () => {
69
+ cancelled = true;
70
+ };
71
+ }, [config, reconnectAsync, reconnectOnMount]);
72
+
73
+ return null;
74
+ }
75
+
76
+ export function Web3Shell({
77
+ config,
78
+ reconnectOnMount = true,
79
+ children,
80
+ }: Web3ShellProps) {
11
81
  const [queryClient] = useState(
12
82
  () =>
13
83
  new QueryClient({
@@ -21,8 +91,13 @@ export function Web3Shell({ config, children }: Web3ShellProps) {
21
91
  );
22
92
 
23
93
  return (
24
- <WagmiProvider config={config}>
25
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
26
- </WagmiProvider>
94
+ // We provide wagmi context directly instead of using WagmiProvider so we can
95
+ // control exactly when silent reconnect runs.
96
+ <WagmiContext.Provider value={config}>
97
+ <QueryClientProvider client={queryClient}>
98
+ <ReconnectOnMount config={config} reconnectOnMount={reconnectOnMount} />
99
+ {children}
100
+ </QueryClientProvider>
101
+ </WagmiContext.Provider>
27
102
  );
28
103
  }
@@ -0,0 +1,21 @@
1
+ interface ConnectWalletCtaProps {
2
+ onClick?: () => void;
3
+ isReconnecting?: boolean;
4
+ }
5
+
6
+ export function ConnectWalletCta({ onClick, isReconnecting = false }: ConnectWalletCtaProps) {
7
+ return (
8
+ <button
9
+ onClick={() => onClick?.()}
10
+ disabled={!onClick}
11
+ className="px-4 py-2.5 rounded-lg bg-violet-500/10 border border-violet-500/20 text-violet-700 dark:text-violet-400 text-sm font-display font-medium hover:bg-violet-500/20 transition-colors"
12
+ >
13
+ {isReconnecting ? (
14
+ <span className="flex items-center gap-2">
15
+ <span className="w-3 h-3 rounded-full border-2 border-violet-500/40 border-t-violet-600 dark:border-t-violet-400 animate-spin" />
16
+ Reconnecting...
17
+ </span>
18
+ ) : 'Connect'}
19
+ </button>
20
+ );
21
+ }
package/src/components.ts CHANGED
@@ -43,9 +43,15 @@ export { Web3Shell } from './components/layout/Web3Shell';
43
43
  export { ChainSwitcher } from './components/layout/ChainSwitcher';
44
44
  export { ThemeToggle } from './components/layout/ThemeToggle';
45
45
 
46
+ // ── Web3 ──
47
+ export { ConnectWalletCta } from './components/web3/ConnectWalletCta';
48
+
46
49
  // ── Forms ──
47
50
  export { FormField } from './components/forms/FormField';
48
51
  export type { FormSection } from './components/forms/BlueprintJobForm';
49
52
  export { BlueprintJobForm } from './components/forms/BlueprintJobForm';
50
53
  export { FormSummary } from './components/forms/FormSummary';
51
54
  export { JobExecutionDialog } from './components/forms/JobExecutionDialog';
55
+
56
+ // ── Blueprint Host ──
57
+ export { BlueprintHostHero, BlueprintHostPanel } from './host';
@@ -103,6 +103,7 @@ export const tangleServicesAbi = [
103
103
  { name: 'totalCost', type: 'uint256' },
104
104
  { name: 'timestamp', type: 'uint64' },
105
105
  { name: 'expiry', type: 'uint64' },
106
+ { name: 'confidentiality', type: 'uint8' },
106
107
  {
107
108
  name: 'securityCommitments',
108
109
  type: 'tuple[]',
@@ -118,6 +119,14 @@ export const tangleServicesAbi = [
118
119
  { name: 'exposureBps', type: 'uint16' },
119
120
  ],
120
121
  },
122
+ {
123
+ name: 'resourceCommitments',
124
+ type: 'tuple[]',
125
+ components: [
126
+ { name: 'kind', type: 'uint8' },
127
+ { name: 'count', type: 'uint64' },
128
+ ],
129
+ },
121
130
  ],
122
131
  },
123
132
  { name: 'signature', type: 'bytes' },
@@ -185,9 +194,9 @@ export const tangleServicesAbi = [
185
194
  type: 'event',
186
195
  name: 'ServiceRequested',
187
196
  inputs: [
188
- { name: 'requester', type: 'address', indexed: true },
189
197
  { name: 'requestId', type: 'uint64', indexed: true },
190
198
  { name: 'blueprintId', type: 'uint64', indexed: true },
199
+ { name: 'requester', type: 'address', indexed: true },
191
200
  ],
192
201
  },
193
202
  {
@@ -1,13 +1,14 @@
1
1
  import { defineChain } from 'viem';
2
2
  import { mainnet } from 'viem/chains';
3
3
  import type { Address, Chain } from 'viem';
4
+ import { getEnvVar, isDevEnv } from '../utils/env';
4
5
 
5
6
  /**
6
7
  * Resolve RPC URL for the current environment.
7
8
  * Handles local dev (hostname swap), Vite dev proxy, and remote access.
8
9
  */
9
10
  export function resolveRpcUrl(envUrl?: string): string {
10
- const configured = envUrl ?? import.meta.env.VITE_RPC_URL ?? 'http://localhost:8545';
11
+ const configured = envUrl ?? getEnvVar('VITE_RPC_URL') ?? 'http://localhost:8545';
11
12
  if (typeof window === 'undefined') return configured;
12
13
  try {
13
14
  const rpc = new URL(configured);
@@ -15,7 +16,7 @@ export function resolveRpcUrl(envUrl?: string): string {
15
16
  const pageHost = window.location.hostname;
16
17
  const isLocalPage = pageHost === '127.0.0.1' || pageHost === 'localhost';
17
18
  // Dev-mode proxy for LAN access to local RPC
18
- if (isLocalRpc && !isLocalPage && import.meta.env.DEV) {
19
+ if (isLocalRpc && !isLocalPage && isDevEnv()) {
19
20
  return `${window.location.origin}/rpc-proxy`;
20
21
  }
21
22
  // Non-dev LAN access: swap hostname
@@ -31,14 +32,26 @@ export function resolveRpcUrl(envUrl?: string): string {
31
32
 
32
33
  export const rpcUrl = resolveRpcUrl();
33
34
 
34
- export const tangleLocal = defineChain({
35
- id: Number(import.meta.env.VITE_CHAIN_ID ?? 31337),
36
- name: 'Tangle Local',
37
- nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
38
- rpcUrls: { default: { http: [rpcUrl] } },
39
- blockExplorers: { default: { name: 'Explorer', url: '' } },
40
- contracts: { multicall3: { address: '0xcA11bde05977b3631167028862bE2a173976CA11' } },
41
- });
35
+ export interface LocalChainOptions {
36
+ chainId?: number;
37
+ rpcUrl?: string;
38
+ }
39
+
40
+ export function createTangleLocalChain(options: LocalChainOptions = {}) {
41
+ const chainId = options.chainId ?? Number(getEnvVar('VITE_CHAIN_ID') ?? 31337);
42
+ const localRpcUrl = resolveRpcUrl(options.rpcUrl);
43
+
44
+ return defineChain({
45
+ id: chainId,
46
+ name: 'Tangle Local',
47
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
48
+ rpcUrls: { default: { http: [localRpcUrl] } },
49
+ blockExplorers: { default: { name: 'Explorer', url: '' } },
50
+ contracts: { multicall3: { address: '0xcA11bde05977b3631167028862bE2a173976CA11' } },
51
+ });
52
+ }
53
+
54
+ export const tangleLocal = createTangleLocalChain();
42
55
 
43
56
  export const tangleTestnet = defineChain({
44
57
  id: 3799,
@@ -3,8 +3,9 @@ import type { PublicClient } from 'viem';
3
3
  import { atom } from 'nanostores';
4
4
  import { getNetworks, tangleLocal, type CoreAddresses } from './chains';
5
5
  import { persistedAtom } from '../stores/persistedAtom';
6
+ import { getEnvVar } from '../utils/env';
6
7
 
7
- const defaultChainId = Number(import.meta.env.VITE_CHAIN_ID ?? tangleLocal.id);
8
+ const defaultChainId = Number(getEnvVar('VITE_CHAIN_ID') ?? tangleLocal.id);
8
9
 
9
10
  export const selectedChainIdStore = persistedAtom<number>({
10
11
  key: 'bp_selected_chain',
@@ -13,36 +14,76 @@ export const selectedChainIdStore = persistedAtom<number>({
13
14
 
14
15
  const clientCache = new Map<number, PublicClient>();
15
16
 
17
+ function configuredDefaultChainId(): number {
18
+ const networks = getNetworks();
19
+ if (networks[defaultChainId]) return defaultChainId;
20
+
21
+ for (const [chainId, net] of Object.entries(networks)) {
22
+ if (
23
+ net?.shortLabel === 'Local' ||
24
+ net?.label === 'Tangle Local' ||
25
+ net?.chain?.name === 'Tangle Local'
26
+ ) {
27
+ return Number(chainId);
28
+ }
29
+ }
30
+
31
+ const [firstConfigured] = Object.keys(networks);
32
+ return firstConfigured ? Number(firstConfigured) : defaultChainId;
33
+ }
34
+
35
+ function normalizeSelectedChainId(chainId: number): number {
36
+ const networks = getNetworks();
37
+ if (!Object.keys(networks).length) return chainId;
38
+ return networks[chainId] ? chainId : configuredDefaultChainId();
39
+ }
40
+
41
+ export function sanitizeSelectedChainId(): number {
42
+ const normalized = normalizeSelectedChainId(selectedChainIdStore.get());
43
+ if (normalized !== selectedChainIdStore.get()) {
44
+ selectedChainIdStore.set(normalized);
45
+ }
46
+ return normalized;
47
+ }
48
+
16
49
  function getOrCreateClient(chainId: number): PublicClient {
17
- const cached = clientCache.get(chainId);
50
+ const normalizedChainId = normalizeSelectedChainId(chainId);
51
+
52
+ const cached = clientCache.get(normalizedChainId);
18
53
  if (cached) return cached;
19
54
  const networks = getNetworks();
20
- const net = networks[chainId];
55
+ const net = networks[normalizedChainId];
21
56
  if (!net) {
22
- const fallback = networks[defaultChainId];
57
+ const fallback = networks[configuredDefaultChainId()];
23
58
  if (!fallback) {
24
59
  return createPublicClient({ chain: tangleLocal, transport: http() });
25
60
  }
26
61
  return createPublicClient({ chain: fallback.chain, transport: http(fallback.rpcUrl) });
27
62
  }
28
63
  const client = createPublicClient({ chain: net.chain, transport: http(net.rpcUrl) });
29
- clientCache.set(chainId, client);
64
+ clientCache.set(normalizedChainId, client);
30
65
  return client;
31
66
  }
32
67
 
33
- export const publicClientStore = atom<PublicClient>(getOrCreateClient(selectedChainIdStore.get()));
68
+ export const publicClientStore = atom<PublicClient>(getOrCreateClient(sanitizeSelectedChainId()));
34
69
 
35
70
  selectedChainIdStore.subscribe((chainId: number) => {
36
- publicClientStore.set(getOrCreateClient(chainId));
71
+ const normalized = normalizeSelectedChainId(chainId);
72
+ if (normalized !== chainId) {
73
+ selectedChainIdStore.set(normalized);
74
+ return;
75
+ }
76
+ publicClientStore.set(getOrCreateClient(normalized));
37
77
  });
38
78
 
39
79
  export function getPublicClient(): PublicClient {
80
+ sanitizeSelectedChainId();
40
81
  return publicClientStore.get();
41
82
  }
42
83
 
43
84
  export const publicClient = new Proxy({} as PublicClient, {
44
85
  get(_target, prop) {
45
- const client = getOrCreateClient(selectedChainIdStore.get());
86
+ const client = getOrCreateClient(sanitizeSelectedChainId());
46
87
  const value = (client as any)[prop];
47
88
  return typeof value === 'function' ? value.bind(client) : value;
48
89
  },
@@ -50,6 +91,7 @@ export const publicClient = new Proxy({} as PublicClient, {
50
91
 
51
92
  export function getAddresses<T extends CoreAddresses = CoreAddresses>(): T {
52
93
  const networks = getNetworks<T>();
53
- const net = networks[selectedChainIdStore.get()];
54
- return net?.addresses ?? networks[defaultChainId]?.addresses ?? {} as T;
94
+ const selectedChainId = sanitizeSelectedChainId();
95
+ const net = networks[selectedChainId];
96
+ return net?.addresses ?? networks[configuredDefaultChainId()]?.addresses ?? {} as T;
55
97
  }