@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.
- package/README.md +47 -11
- package/dist/BlueprintHostPanel-6iVEh-f1.d.ts +39 -0
- package/dist/chunk-37ADATBT.js +55 -0
- package/dist/chunk-37ADATBT.js.map +1 -0
- package/dist/chunk-A6PJT5YQ.js +1180 -0
- package/dist/chunk-A6PJT5YQ.js.map +1 -0
- package/dist/chunk-GD3AZEJL.js +327 -0
- package/dist/chunk-GD3AZEJL.js.map +1 -0
- package/dist/components.d.ts +179 -0
- package/dist/components.js +1127 -0
- package/dist/components.js.map +1 -0
- package/dist/host.d.ts +96 -0
- package/dist/host.js +39 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +8470 -0
- package/dist/index.js +841 -0
- package/dist/index.js.map +1 -0
- package/dist/preset.d.ts +60 -0
- package/dist/preset.js +7 -0
- package/dist/preset.js.map +1 -0
- package/dist/registry-JhwB9BPD.d.ts +87 -0
- package/dist/styles.css +559 -0
- package/package.json +42 -13
- package/src/components/layout/AppDocument.tsx +1 -1
- package/src/components/layout/ChainSwitcher.tsx +27 -10
- package/src/components/layout/Web3Shell.tsx +81 -6
- package/src/components/web3/ConnectWalletCta.tsx +21 -0
- package/src/components.ts +6 -0
- package/src/contracts/abi.ts +10 -1
- package/src/contracts/chains.ts +23 -10
- package/src/contracts/publicClient.ts +52 -10
- package/src/hooks/useOperators.ts +203 -96
- package/src/hooks/useProvisionProgress.ts +2 -1
- package/src/hooks/useQuotes.ts +69 -14
- package/src/hooks/useSessionAuth.ts +2 -1
- package/src/hooks/useSidecarAuth.ts +173 -0
- package/src/hooks/useWagmiSidecarAuth.ts +11 -0
- package/src/hooks/useWalletEthBalance.ts +68 -0
- package/src/host/components/BlueprintHostHero.tsx +91 -0
- package/src/host/components/BlueprintHostPanel.tsx +24 -0
- package/src/host/index.ts +42 -0
- package/src/host/resolver.ts +204 -0
- package/src/host/types.ts +111 -0
- package/src/index.ts +48 -2
- package/src/stores/infra.ts +3 -2
- package/src/styles.css +128 -0
- package/src/utils/env.ts +22 -0
- 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.
|
|
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": "./
|
|
16
|
-
"types": "./
|
|
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
|
-
".":
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
"
|
|
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
|
|
4
|
+
import { getNetworks } from '../../contracts/chains';
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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={`${
|
|
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
|
-
{
|
|
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={`${
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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';
|
package/src/contracts/abi.ts
CHANGED
|
@@ -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
|
{
|
package/src/contracts/chains.ts
CHANGED
|
@@ -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 ??
|
|
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 &&
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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(
|
|
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
|
|
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[
|
|
55
|
+
const net = networks[normalizedChainId];
|
|
21
56
|
if (!net) {
|
|
22
|
-
const fallback = networks[
|
|
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(
|
|
64
|
+
clientCache.set(normalizedChainId, client);
|
|
30
65
|
return client;
|
|
31
66
|
}
|
|
32
67
|
|
|
33
|
-
export const publicClientStore = atom<PublicClient>(getOrCreateClient(
|
|
68
|
+
export const publicClientStore = atom<PublicClient>(getOrCreateClient(sanitizeSelectedChainId()));
|
|
34
69
|
|
|
35
70
|
selectedChainIdStore.subscribe((chainId: number) => {
|
|
36
|
-
|
|
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(
|
|
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
|
|
54
|
-
|
|
94
|
+
const selectedChainId = sanitizeSelectedChainId();
|
|
95
|
+
const net = networks[selectedChainId];
|
|
96
|
+
return net?.addresses ?? networks[configuredDefaultChainId()]?.addresses ?? {} as T;
|
|
55
97
|
}
|