Skip to content

Commit 51d9911

Browse files
committed
Improve Lithoscan mainnet network and wallet UX
1 parent 1c1c333 commit 51d9911

13 files changed

Lines changed: 367 additions & 31 deletions

File tree

Makalu/explorer/components/BrandLogo.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { NETWORK } from '@/lib/network';
33
export default function BrandLogo({ compact = false }: { compact?: boolean }) {
44
return (
55
<span className="flex shrink-0 items-center gap-3">
6-
<img src={NETWORK.logoPath} alt="LITHO" className="h-9 w-9 rounded-full object-contain" />
6+
<img
7+
src={NETWORK.logoPath}
8+
alt="Lithosphere"
9+
className="h-9 w-10 object-contain drop-shadow-[0_6px_14px_rgba(74,144,217,0.22)]"
10+
/>
711
{!compact && (
812
<span className="hidden sm:flex sm:flex-col sm:leading-none">
913
<span className="text-base font-bold text-[var(--color-text-primary)]">{NETWORK.explorerTitle}</span>

Makalu/explorer/components/Header.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const NAV_ITEMS: NavItem[] = [
4646
const MORE_ITEMS: NavItem[] = [
4747
{ label: 'Quantt Research', href: '/quantt' },
4848
{ label: 'Docs', href: 'https://docs.litho.ai', external: true },
49+
{ label: 'Makalu Testnet', href: 'https://makalu.litho.ai', external: true },
50+
{ label: 'Kamet Testnet', href: 'https://kamet.litho.ai', external: true },
4951
{ label: 'LITHO TGE', href: 'https://tge.ignite.trade', external: true },
5052
{ label: 'LITHO.ai', href: 'https://litho.ai', external: true },
5153
{ label: 'ACCESS', href: 'https://access.litho.ai', external: true },

Makalu/explorer/components/Layout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { ReactNode } from 'react';
22

3+
import { NETWORK } from '@/lib/network';
4+
35
import Footer from './Footer';
46
import Header from './Header';
57

68
export default function Layout({ children }: { children: ReactNode }) {
79
return (
8-
<div className="min-h-screen flex flex-col">
10+
<div className={`network-shell min-h-screen flex flex-col ${NETWORK.isMainnet ? 'network-mainnet' : 'network-testnet'}`}>
911
<Header />
1012
<main className="flex-1 w-full max-w-7xl mx-auto px-4 sm:px-6 py-6">
1113
{children}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {
2+
useWeb3Modal,
3+
useWeb3ModalAccount,
4+
useWeb3ModalProvider,
5+
} from '@web3modal/ethers/react';
6+
import { useCallback, useEffect, useRef, useState } from 'react';
7+
8+
import { NETWORK } from '@/lib/network';
9+
import { ensureExplorerNetwork, type WalletRequestProvider } from '@/lib/walletNetwork';
10+
11+
function CopyButton({ value, label }: { value: string; label: string }) {
12+
const [copied, setCopied] = useState(false);
13+
14+
async function copy() {
15+
await navigator.clipboard.writeText(value);
16+
setCopied(true);
17+
window.setTimeout(() => setCopied(false), 1500);
18+
}
19+
20+
return (
21+
<button
22+
type="button"
23+
onClick={copy}
24+
className="rounded-lg border border-[var(--color-border)] px-2.5 py-1 text-xs font-medium text-litho-500 transition hover:border-litho-300 hover:bg-litho-50 dark:text-litho-300 dark:hover:bg-litho-400/10"
25+
aria-label={`Copy ${label}`}
26+
>
27+
{copied ? 'Copied' : 'Copy'}
28+
</button>
29+
);
30+
}
31+
32+
export default function NetworkSetupCard() {
33+
const { open } = useWeb3Modal();
34+
const { isConnected, chainId } = useWeb3ModalAccount();
35+
const { walletProvider } = useWeb3ModalProvider();
36+
const [busy, setBusy] = useState(false);
37+
const [error, setError] = useState('');
38+
const pendingAdd = useRef(false);
39+
const connectedToExplorer = Number(chainId) === NETWORK.evmChainId;
40+
41+
const addOrSwitch = useCallback(async () => {
42+
setError('');
43+
44+
if (!NETWORK.walletReady) {
45+
setError(`${NETWORK.label} RPC is not configured.`);
46+
return;
47+
}
48+
49+
if (!isConnected || !walletProvider) {
50+
pendingAdd.current = true;
51+
await open({ view: 'Connect' });
52+
return;
53+
}
54+
55+
setBusy(true);
56+
try {
57+
await ensureExplorerNetwork(walletProvider as WalletRequestProvider);
58+
} catch (reason) {
59+
const message = reason instanceof Error ? reason.message : 'Could not add the network.';
60+
setError(message);
61+
} finally {
62+
setBusy(false);
63+
}
64+
}, [isConnected, open, walletProvider]);
65+
66+
useEffect(() => {
67+
if (!pendingAdd.current || !isConnected || !walletProvider) return;
68+
pendingAdd.current = false;
69+
void addOrSwitch();
70+
}, [addOrSwitch, isConnected, walletProvider]);
71+
72+
const details = [
73+
{ label: 'EVM chain ID', value: `${NETWORK.evmChainId} (${NETWORK.chainIdHex})` },
74+
{ label: 'Cosmos chain ID', value: NETWORK.cosmosChainId },
75+
{ label: 'RPC endpoint', value: NETWORK.rpcUrl || 'Not configured' },
76+
{ label: 'Native currency', value: 'LITHO' },
77+
];
78+
79+
return (
80+
<section className="network-setup-card rounded-3xl border border-[var(--color-border)] bg-[var(--color-bg-secondary)] p-6 shadow-sm">
81+
<div className="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
82+
<div>
83+
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-litho-500">
84+
Wallet network
85+
</div>
86+
<h2 className="mt-2 text-2xl font-semibold tracking-tight">{NETWORK.label}</h2>
87+
<p className="mt-2 max-w-xl text-sm leading-6 text-[var(--color-text-secondary)]">
88+
Add the official network configuration to your wallet before connecting or signing in.
89+
</p>
90+
</div>
91+
<button
92+
type="button"
93+
onClick={addOrSwitch}
94+
disabled={busy || connectedToExplorer}
95+
className="shrink-0 rounded-2xl bg-litho-500 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-litho-500/20 transition hover:bg-litho-600 disabled:cursor-default disabled:bg-emerald-500 disabled:opacity-100"
96+
>
97+
{busy
98+
? 'Adding network…'
99+
: connectedToExplorer
100+
? `✓ ${NETWORK.shortName} connected`
101+
: isConnected
102+
? `Switch to ${NETWORK.shortName}`
103+
: `Quick add ${NETWORK.shortName}`}
104+
</button>
105+
</div>
106+
107+
<dl className="mt-6 divide-y divide-[var(--color-border-light)] overflow-hidden rounded-2xl border border-[var(--color-border)]">
108+
{details.map((detail) => (
109+
<div key={detail.label} className="flex flex-col gap-2 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
110+
<dt className="text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
111+
{detail.label}
112+
</dt>
113+
<dd className="flex min-w-0 items-center gap-3">
114+
<code className="min-w-0 break-all text-sm text-[var(--color-text-primary)]">
115+
{detail.value}
116+
</code>
117+
<CopyButton value={detail.value} label={detail.label} />
118+
</dd>
119+
</div>
120+
))}
121+
</dl>
122+
123+
{error && (
124+
<p className="mt-4 rounded-xl border border-red-400/25 bg-red-400/10 px-4 py-3 text-sm text-red-600 dark:text-red-200">
125+
{error}
126+
</p>
127+
)}
128+
</section>
129+
);
130+
}

Makalu/explorer/components/ThanosSignIn.tsx

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
type StoredSession,
1313
} from '@/lib/auth';
1414
import { NETWORK } from '@/lib/network';
15+
import { isThanosConnector, isThanosIdentity, THANOS_INSTALL_URL } from '@/lib/thanos';
16+
import { ensureExplorerNetwork, type WalletRequestProvider } from '@/lib/walletNetwork';
1517

1618
/**
1719
* "Sign in with Thanos" — SIWE authentication, unified with wallet connection.
@@ -36,32 +38,57 @@ import { NETWORK } from '@/lib/network';
3638

3739
const APP_NAME = `${NETWORK.label} Explorer`;
3840
const CHAIN_ID = NETWORK.evmChainId;
39-
const THANOS_RDNS = 'fi.thanos.wallet';
40-
const THANOS_INSTALL_URL = 'https://thanos.fi';
41-
4241
function shorten(addr: string): string {
4342
return addr.length > 12 ? `${addr.slice(0, 6)}${addr.slice(-4)}` : addr;
4443
}
4544

4645
function findThanosConnector(): Connector | undefined {
47-
return ConnectorController.state.connectors.find(
48-
(c) =>
49-
c.id === 'eip6963' &&
50-
(c.info?.rdns === THANOS_RDNS || c.name?.toLowerCase().includes('thanos')),
51-
);
46+
return ConnectorController.state.connectors.find(isThanosConnector);
5247
}
5348

5449
/**
5550
* Thanos may announce after Web3Modal's init-time EIP-6963 discovery (the
5651
* extension's service worker can wake late). Re-request announcements once
5752
* before concluding it isn't installed.
5853
*/
59-
async function discoverThanosConnector(): Promise<Connector | undefined> {
54+
type ThanosWallet = { connector?: Connector; provider: Eip1193Provider };
55+
56+
async function discoverThanosWallet(): Promise<ThanosWallet | undefined> {
6057
const existing = findThanosConnector();
61-
if (existing) return existing;
58+
if (existing?.provider) {
59+
return { connector: existing, provider: existing.provider as Eip1193Provider };
60+
}
61+
62+
let announcedProvider: Eip1193Provider | undefined;
63+
const onAnnouncement = (event: Event) => {
64+
const detail = (event as CustomEvent<{
65+
info?: { rdns?: string; name?: string };
66+
provider?: Eip1193Provider;
67+
}>).detail;
68+
if (detail?.provider && isThanosIdentity(detail.info?.rdns, detail.info?.name)) {
69+
announcedProvider = detail.provider;
70+
}
71+
};
72+
73+
window.addEventListener('eip6963:announceProvider', onAnnouncement);
6274
window.dispatchEvent(new Event('eip6963:requestProvider'));
63-
await new Promise((resolve) => setTimeout(resolve, 300));
64-
return findThanosConnector();
75+
76+
try {
77+
// Extension service workers can wake after Web3Modal's initial discovery.
78+
// Allow late announcements and prefer Web3Modal's connector when it appears.
79+
for (let attempt = 0; attempt < 8; attempt += 1) {
80+
await new Promise((resolve) => setTimeout(resolve, 150));
81+
const connector = findThanosConnector();
82+
if (connector?.provider) {
83+
return { connector, provider: connector.provider as Eip1193Provider };
84+
}
85+
if (announcedProvider) return { provider: announcedProvider };
86+
}
87+
} finally {
88+
window.removeEventListener('eip6963:announceProvider', onAnnouncement);
89+
}
90+
91+
return announcedProvider ? { provider: announcedProvider } : undefined;
6592
}
6693

6794
/**
@@ -176,24 +203,27 @@ export default function ThanosSignIn() {
176203
setThanosMissing(false);
177204
setPhase('connecting');
178205
try {
179-
const connector = await discoverThanosConnector();
180-
if (!connector) {
206+
const wallet = await discoverThanosWallet();
207+
if (!wallet) {
181208
setThanosMissing(true);
182209
return;
183210
}
184-
// Connects (or switches the active connection to) Thanos with no picker
185-
// UI, updating all Web3Modal account state along the way.
186-
await ConnectionController.connectExternal(connector, connector.chain);
187-
const provider = connector.provider as Eip1193Provider | undefined;
188-
const accounts = provider
189-
? ((await provider.request({ method: 'eth_accounts' })) as string[])
190-
: [];
211+
const { connector, provider } = wallet;
212+
if (connector) {
213+
// Keep Web3Modal's shared account state in sync when its connector is available.
214+
await ConnectionController.connectExternal(connector, connector.chain);
215+
}
216+
let accounts = (await provider.request({ method: 'eth_accounts' })) as string[];
217+
if (!accounts?.length && !connector) {
218+
accounts = (await provider.request({ method: 'eth_requestAccounts' })) as string[];
219+
}
191220
const address = accounts?.[0];
192-
if (!provider || !address) {
221+
if (!address) {
193222
// connectExternal swallows a user rejection (it stores the error and
194223
// resolves) — an empty account list is how the rejection surfaces here.
195224
throw new Error('Wallet connection was declined in Thanos.');
196225
}
226+
await ensureExplorerNetwork(provider as WalletRequestProvider);
197227
setPhase('signing');
198228
const stored = await signSession(provider, address);
199229
setSession(stored);

Makalu/explorer/context/WalletContext.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import React, { createContext, useContext, useEffect, useState } from 'react';
2+
import { ConnectorController } from '@web3modal/core';
23
import { createWeb3Modal, defaultConfig, useWeb3Modal, useWeb3ModalAccount, useDisconnect } from '@web3modal/ethers/react';
34

45
import { NETWORK } from '@/lib/network';
6+
import { prioritizeThanosConnectors } from '@/lib/thanos';
57

68
const PROJECT_ID = '4d5085c5fd29c034f63f9256013dcd09';
79

@@ -96,9 +98,19 @@ if (typeof window !== 'undefined') {
9698
],
9799
themeMode: NETWORK.defaultTheme,
98100
themeVariables: {
99-
'--w3m-accent': '#34d399',
101+
'--w3m-accent': NETWORK.isMainnet ? '#4a90d9' : '#34d399',
100102
},
101103
});
104+
105+
// EIP-6963 wallets are shown in announcement order by Web3Modal. Keep the
106+
// Lithosphere-native Thanos wallet first without hiding any other wallet.
107+
const preferThanos = (connectors: typeof ConnectorController.state.connectors) => {
108+
const prioritized = prioritizeThanosConnectors(connectors);
109+
if (prioritized.every((connector, index) => connector === connectors[index])) return;
110+
queueMicrotask(() => ConnectorController.setConnectors(prioritized));
111+
};
112+
preferThanos(ConnectorController.state.connectors);
113+
ConnectorController.subscribeKey('connectors', preferThanos);
102114
} catch (error) {
103115
console.log('Web3Modal init:', error instanceof Error ? error.message : 'already initialized');
104116
}

Makalu/explorer/lib/network.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ export function buildNetworkConfig(env: PublicEnvironment) {
5353
rpcUrl,
5454
siteUrl,
5555
explorerTitle: env.NEXT_PUBLIC_EXPLORER_TITLE?.trim() || 'Lithoscan',
56-
logoPath: '/litho-coin-logo.svg',
57-
faviconPath: isMainnet ? '/litho-coin-logo.svg' : '/makalu-testnet-favicon.png',
56+
logoPath: '/litho-logo.png',
57+
faviconPath: isMainnet ? '/litho-logo.png' : '/makalu-testnet-favicon.png',
5858
defaultTheme,
5959
walletReady: validHttpUrl(rpcUrl),
6060
faucetEnabled: !isMainnet,

Makalu/explorer/lib/thanos.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export const THANOS_RDNS = 'fi.thanos.wallet';
2+
export const THANOS_INSTALL_URL = 'https://thanos.fi/';
3+
4+
type ConnectorIdentity = {
5+
name?: string | null;
6+
info?: { rdns?: string | null } | null;
7+
};
8+
9+
export function isThanosIdentity(rdns?: string | null, name?: string | null): boolean {
10+
const normalizedRdns = rdns?.trim().toLowerCase() || '';
11+
const normalizedName = name?.trim().toLowerCase() || '';
12+
13+
return normalizedRdns === THANOS_RDNS
14+
|| normalizedRdns.includes('thanos')
15+
|| normalizedName.includes('thanos');
16+
}
17+
18+
export function isThanosConnector(connector: ConnectorIdentity): boolean {
19+
return isThanosIdentity(connector.info?.rdns, connector.name);
20+
}
21+
22+
export function prioritizeThanosConnectors<T extends ConnectorIdentity>(
23+
connectors: T[],
24+
): T[] {
25+
return connectors
26+
.map((connector, index) => ({ connector, index }))
27+
.sort((left, right) => {
28+
const leftPreferred = isThanosConnector(left.connector) ? 0 : 1;
29+
const rightPreferred = isThanosConnector(right.connector) ? 0 : 1;
30+
return leftPreferred - rightPreferred || left.index - right.index;
31+
})
32+
.map(({ connector }) => connector);
33+
}

0 commit comments

Comments
 (0)