Ship bridge lanes, public API access doc, and WalletConnect client stack.
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 15s
Validate Explorer / frontend (push) Failing after 20s
Validate Explorer / smoke-e2e (push) Has been skipped

Align CCIP catalog UX with 11-lane config-ready routes, document the no-key public API decision, and enable browser WalletConnect pairing with backend session registration and deploy-time project ID wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-05-23 02:21:37 -07:00
parent efd7c8bbcb
commit ab9c1f9f98
18 changed files with 4278 additions and 150 deletions

View File

@@ -472,7 +472,15 @@ export default function BridgeMonitoringPage({
{bridgeRoutes.lastModified ? ` · updated ${relativeAge(bridgeRoutes.lastModified)}` : ''})
</>
) : null}
.
. Gnosis, Cronos, Celo, and Wemix lanes are aligned to deployed CCIP receivers fund LINK on each remote bridge before live traffic.
</p>
<p className="mb-4 text-sm text-gray-600 dark:text-gray-400">
Operator runbook:{' '}
<Link href="/docs/public-api-access" className="text-primary-600 hover:underline">
public API access
</Link>{' '}
· config-ready chain completion in repo{' '}
<code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-900">CONFIG_READY_CHAINS_COMPLETION_RUNBOOK.md</code>
</p>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">

View File

@@ -7,6 +7,8 @@ import type {
} from '@/components/wallet/AddToMetaMask'
import { AddToMetaMask } from '@/components/wallet/AddToMetaMask'
import WalletConnectPostureNote from '@/components/wallet/WalletConnectPostureNote'
import { connectAndAuthenticateWalletConnect, getActiveWalletConnectSessionId, isWalletConnectClientReady, loadWalletConnectConfig } from '@/services/wallet/walletConnectClient'
import { registerWalletConnectSession } from '@/services/api/walletConnect'
import Link from 'next/link'
import { Explain, useUiMode } from '@/components/common/UiModeContext'
import { accessApi, type WalletAccessSession } from '@/services/api/access'
@@ -48,6 +50,8 @@ export default function WalletPage(props: WalletPageProps) {
const [walletSession, setWalletSession] = useState<WalletAccessSession | null>(null)
const [connectingWallet, setConnectingWallet] = useState(false)
const [walletError, setWalletError] = useState<string | null>(null)
const [walletConnectReady, setWalletConnectReady] = useState(false)
const [connectingWalletConnect, setConnectingWalletConnect] = useState(false)
const [copiedAddress, setCopiedAddress] = useState(false)
const [watchlistEntries, setWatchlistEntries] = useState<string[]>([])
const [addressInfo, setAddressInfo] = useState<AddressInfo | null>(null)
@@ -68,6 +72,7 @@ export default function WalletPage(props: WalletPageProps) {
syncSession()
syncWatchlist()
void loadWalletConnectConfig().then((config) => setWalletConnectReady(isWalletConnectClientReady(config)))
window.addEventListener('explorer-access-session-changed', syncSession)
window.addEventListener('storage', syncWatchlist)
return () => {
@@ -76,6 +81,30 @@ export default function WalletPage(props: WalletPageProps) {
}
}, [])
const handleConnectWalletConnect = async () => {
setConnectingWalletConnect(true)
setWalletError(null)
try {
const config = await loadWalletConnectConfig()
if (!isWalletConnectClientReady(config)) {
throw new Error('WalletConnect is not enabled. Set WALLETCONNECT_PROJECT_ID on the explorer backend.')
}
const result = await connectAndAuthenticateWalletConnect(config, async (resolveAddress, signMessage) => {
const session = await accessApi.connectWalletSessionWithSigner(resolveAddress, signMessage)
setWalletSession(session)
return { address: session.address }
})
const sessionId = getActiveWalletConnectSessionId()
if (sessionId && result.address) {
await registerWalletConnectSession({ sessionId, address: result.address, chainId: 138 })
}
} catch (error) {
setWalletError(error instanceof Error ? error.message : 'WalletConnect connection failed.')
} finally {
setConnectingWalletConnect(false)
}
}
const handleConnectWallet = async () => {
setConnectingWallet(true)
setWalletError(null)
@@ -279,12 +308,25 @@ export default function WalletPage(props: WalletPageProps) {
<>
<button
type="button"
onClick={handleConnectWallet}
onClick={() => void handleConnectWallet()}
disabled={connectingWallet}
className="rounded-lg bg-primary-600 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-70"
>
{connectingWallet ? 'Connecting wallet…' : 'Connect wallet'}
</button>
<button
type="button"
onClick={() => void handleConnectWalletConnect()}
disabled={!walletConnectReady || connectingWalletConnect}
title={
walletConnectReady
? 'Pair a mobile wallet via WalletConnect QR'
: 'Set WALLETCONNECT_PROJECT_ID on the explorer backend to enable WalletConnect'
}
className="rounded-lg border border-indigo-300 px-3 py-2 text-sm font-semibold text-indigo-700 hover:bg-indigo-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-60 dark:border-indigo-800 dark:text-indigo-300 dark:hover:bg-indigo-950/20"
>
{connectingWalletConnect ? 'Opening WalletConnect…' : 'WalletConnect'}
</button>
<Link
href="/access"
className="rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-900/60"

View File

@@ -5,6 +5,11 @@ import { Card } from '@/libs/frontend-ui-primitives'
import PageIntro from '@/components/common/PageIntro'
const docsCards = [
{
title: 'Public API access',
href: '/docs/public-api-access',
description: 'Read-only JSON endpoints, managed RPC keys on /access, and the current no-key policy for Blockscout reads.',
},
{
title: 'GRU Guide',
href: '/docs/gru',

View File

@@ -0,0 +1,87 @@
'use client'
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import PageIntro from '@/components/common/PageIntro'
import { explorerPublicApiLinks } from '@/data/explorerOperations'
export default function PublicApiAccessDocsPage() {
return (
<div className="container mx-auto px-4 py-6 sm:py-8">
<PageIntro
eyebrow="Explorer Documentation"
title="Public API access"
description="How integrators use read-only explorer APIs today, how managed RPC keys work on /access, and the planned path if public rate limits require API keys."
actions={[
{ href: '/access', label: 'Account access' },
{ href: '/wallet', label: 'Wallet tools' },
{ href: '/operations', label: 'Operations hub' },
]}
/>
<div className="space-y-6">
<Card title="Decision summary (2026-05-23)">
<ul className="list-disc space-y-2 pl-5 text-sm leading-6 text-gray-600 dark:text-gray-400">
<li>
<strong>Blockscout read API</strong> (<code>/api/v2/*</code>) and the public JSON surfaces listed below remain{' '}
<strong>unauthenticated</strong> for integrators on the public explorer domain.
</li>
<li>
<strong>Managed RPC product keys</strong> are issued through wallet-authenticated{' '}
<Link href="/access" className="text-primary-600 hover:underline">
Account access
</Link>{' '}
(Core RPC / thirdweb-rpc products). These keys gate managed RPC endpoints not the public Blockscout read layer.
</li>
<li>
If abuse or rate limits require change, the preferred near-term path is <strong>Option B</strong>: nginx/API-gateway
throttling with optional <code>X-API-Key</code> for higher quotas. A full external developer portal remains optional.
</li>
</ul>
</Card>
<Card title="Read-only public endpoints (no key)">
<ul className="space-y-3 text-sm">
{explorerPublicApiLinks.map((link) => (
<li key={link.href}>
<a href={link.href} className="font-medium text-primary-600 hover:underline" target="_blank" rel="noopener noreferrer">
{link.label}
</a>
<p className="mt-0.5 text-gray-600 dark:text-gray-400">{link.description}</p>
</li>
))}
<li>
<span className="font-medium text-gray-900 dark:text-white">Blockscout v2</span>
<p className="mt-0.5 text-gray-600 dark:text-gray-400">
Same-origin <code>/api/v2/stats</code>, blocks, transactions, tokens, and address endpoints proxied to Blockscout.
</p>
</li>
</ul>
</Card>
<Card title="Requesting higher limits or RPC keys">
<div className="space-y-3 text-sm leading-6 text-gray-600 dark:text-gray-400">
<p>
For managed RPC access, connect a wallet on{' '}
<Link href="/wallet" className="text-primary-600 hover:underline">
Wallet tools
</Link>{' '}
then open{' '}
<Link href="/access" className="text-primary-600 hover:underline">
Account access
</Link>{' '}
to create scoped keys with tier, product, expiry, and optional monthly quota.
</p>
<p>
For integrator questions about public JSON endpoints or future Blockscout key policy, email{' '}
<a href="mailto:support@d-bis.org" className="text-primary-600 hover:underline">
support@d-bis.org
</a>
.
</p>
</div>
</Card>
</div>
</div>
)
}

View File

@@ -265,21 +265,30 @@ export const accessApi = {
throw new Error('No EOA wallet detected. Please open the explorer with a browser wallet installed.')
}
const accounts = (await window.ethereum.request({
method: 'eth_requestAccounts',
})) as string[]
const address = accounts?.[0]
if (!address) {
throw new Error('Wallet connection was cancelled.')
}
return accessApi.connectWalletSessionWithSigner(async () => {
const accounts = (await window.ethereum!.request({
method: 'eth_requestAccounts',
})) as string[]
const address = accounts?.[0]
if (!address) {
throw new Error('Wallet connection was cancelled.')
}
return address
}, async (message, address) => {
return (await window.ethereum!.request({
method: 'personal_sign',
params: [message, address],
})) as string
})
},
async connectWalletSessionWithSigner(
resolveAddress: () => Promise<string>,
signMessage: (message: string, address: string) => Promise<string>,
): Promise<WalletAccessSession> {
const address = await resolveAddress()
const nonceResponse = await accessApi.createWalletNonce(address)
const message = buildWalletMessage(nonceResponse.nonce)
const signature = (await window.ethereum.request({
method: 'personal_sign',
params: [message, address],
})) as string
const signature = await signMessage(message, address)
return accessApi.authenticateWallet(address, signature, nonceResponse.nonce)
},
async getMe(): Promise<{ user: AccessUser; subscriptions?: AccessSubscription[] }> {

View File

@@ -1,4 +1,5 @@
import { fetchPublicJson } from '@/utils/publicExplorer'
import { getExplorerApiBase } from './blockscout'
const WALLETCONNECT_CONFIG_PATH = '/explorer-api/v1/walletconnect/config'
@@ -22,3 +23,23 @@ export async function getWalletConnectConfig(): Promise<WalletConnectConfigRespo
return null
}
}
export async function registerWalletConnectSession(input: {
sessionId: string
address: string
chainId?: number
}): Promise<void> {
const response = await fetch(`${getExplorerApiBase()}/explorer-api/v1/walletconnect/session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: input.sessionId,
address: input.address,
chainId: input.chainId,
}),
})
if (!response.ok) {
const text = await response.text().catch(() => '')
throw new Error(text || `WalletConnect session register failed (${response.status})`)
}
}

View File

@@ -0,0 +1,77 @@
import { getWalletConnectConfig, type WalletConnectConfigResponse } from '@/services/api/walletConnect'
export interface WalletConnectSessionResult {
address: string
chainId: number
}
let activeProvider: Awaited<ReturnType<typeof createEthereumProvider>> | null = null
async function createEthereumProvider(projectId: string, chains: number[]) {
const { default: EthereumProvider } = await import('@walletconnect/ethereum-provider')
const resolvedChains = (chains.length ? chains : [138, 1]) as [number, ...number[]]
return EthereumProvider.init({
projectId,
chains: [resolvedChains[0]],
optionalChains: resolvedChains,
showQrModal: true,
metadata: {
name: 'DBIS Explorer',
description: 'Chain 138 explorer by DBIS',
url: typeof window !== 'undefined' ? window.location.origin : 'https://explorer.d-bis.org',
icons: [`${typeof window !== 'undefined' ? window.location.origin : 'https://explorer.d-bis.org'}/favicon.ico`],
},
})
}
export async function loadWalletConnectConfig(): Promise<WalletConnectConfigResponse | null> {
return getWalletConnectConfig()
}
export function isWalletConnectClientReady(config: WalletConnectConfigResponse | null): config is WalletConnectConfigResponse {
return Boolean(config?.enabled && config.projectId)
}
export async function connectAndAuthenticateWalletConnect(
config: WalletConnectConfigResponse,
authenticate: (resolveAddress: () => Promise<string>, signMessage: (message: string, address: string) => Promise<string>) => Promise<{ address: string }>,
): Promise<{ address: string }> {
if (!config.projectId) {
throw new Error('WalletConnect project ID is not published.')
}
const chains = config.supportedChains?.length ? config.supportedChains : [138, 1]
const provider = await createEthereumProvider(config.projectId, chains)
activeProvider = provider
return authenticate(
async () => {
const accounts = (await provider.enable()) as string[]
const address = accounts?.[0]
if (!address) {
throw new Error('WalletConnect did not return an account.')
}
return address
},
async (message, address) => {
return (await provider.request({
method: 'personal_sign',
params: [message, address],
})) as string
},
)
}
export async function disconnectWalletConnect(): Promise<void> {
if (!activeProvider) return
try {
await activeProvider.disconnect()
} finally {
activeProvider = null
}
}
export function getActiveWalletConnectSessionId(): string | null {
const session = activeProvider?.session
return typeof session?.topic === 'string' && session.topic ? session.topic : null
}