From a99a30438c430499676f26f57c872dc6f9dbe683 Mon Sep 17 00:00:00 2001 From: carlhong Date: Fri, 27 Feb 2026 10:29:22 +0800 Subject: [PATCH 1/6] refactor: use resource api and update api path https://github.com/subscan-explorer/subscan/issues/5180 --- src/components/ExtrinsicRecords.tsx | 21 +++++++---- src/config/chains/assethub_kusama.json | 2 +- src/config/chains/assethub_paseo.json | 2 +- src/config/chains/assethub_polkadot.json | 2 +- src/config/chains/astar.json | 2 +- src/config/chains/bifrost.json | 2 +- src/config/chains/coretime_kusama.json | 2 +- src/config/chains/coretime_paseo.json | 2 +- src/config/chains/coretime_polkadot.json | 2 +- src/config/chains/hydration.json | 2 +- src/config/chains/pendulum.json | 2 +- src/config/chains/people_kusama.json | 2 +- src/config/chains/people_paseo.json | 2 +- src/config/chains/people_polkadot.json | 2 +- src/hooks/combineQuery.ts | 43 ++++++++++++++++++++++ src/hooks/subscan.ts | 45 +++++++++++++++++++++++- src/providers/multisig-provider.tsx | 23 ++++++------ 17 files changed, 124 insertions(+), 34 deletions(-) diff --git a/src/components/ExtrinsicRecords.tsx b/src/components/ExtrinsicRecords.tsx index 303dfba..d303ff8 100644 --- a/src/components/ExtrinsicRecords.tsx +++ b/src/components/ExtrinsicRecords.tsx @@ -3,7 +3,7 @@ import { ReloadOutlined } from '@ant-design/icons'; import { KeyringAddress } from '@polkadot/ui-keyring/types'; import { Space, Spin, Tabs } from 'antd'; import { isNumber } from 'lodash'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { useApi, useMultisigRecords } from '../hooks'; @@ -248,16 +248,23 @@ export function ExtrinsicRecords() { } }, [cancelledPage]); // eslint-disable-line react-hooks/exhaustive-deps + const fetchTimerRef = useRef | null>(null); + // eslint-disable-next-line complexity const handleChangeTab = (key: string) => { setTabKey(key); - if (key === 'inProgress') { - queryInProgress(); - } else if (key === 'confirmed') { - fetchConfirmed(); - } else if (key === 'cancelled') { - fetchCancelled(); + if (fetchTimerRef.current) { + clearTimeout(fetchTimerRef.current); } + fetchTimerRef.current = setTimeout(() => { + if (key === 'inProgress') { + queryInProgress(); + } else if (key === 'confirmed') { + fetchConfirmed(); + } else if (key === 'cancelled') { + fetchCancelled(); + } + }, 300); }; const refreshData = () => { diff --git a/src/config/chains/assethub_kusama.json b/src/config/chains/assethub_kusama.json index e30e0dc..209413c 100644 --- a/src/config/chains/assethub_kusama.json +++ b/src/config/chains/assethub_kusama.json @@ -4,7 +4,7 @@ "rpc": "wss://asset-hub-kusama-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://assethub-kusama.webapi.subscan.io" + "subscan": "https://assethub-kusama.api.subscan.io" }, "category": "kusama", "logo": "/image/assethub-kusama.png", diff --git a/src/config/chains/assethub_paseo.json b/src/config/chains/assethub_paseo.json index 921d501..89e10cb 100644 --- a/src/config/chains/assethub_paseo.json +++ b/src/config/chains/assethub_paseo.json @@ -4,7 +4,7 @@ "rpc": "wss://asset-hub-paseo-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://assethub-paseo.webapi.subscan.io" + "subscan": "https://assethub-paseo.api.subscan.io" }, "category": "paseo", "logo": "/image/assethub-paseo.png", diff --git a/src/config/chains/assethub_polkadot.json b/src/config/chains/assethub_polkadot.json index 5a0a34e..5edcbcf 100644 --- a/src/config/chains/assethub_polkadot.json +++ b/src/config/chains/assethub_polkadot.json @@ -4,7 +4,7 @@ "rpc": "wss://asset-hub-polkadot-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://assethub-polkadot.webapi.subscan.io" + "subscan": "https://assethub-polkadot.api.subscan.io" }, "category": "polkadot", "logo": "/image/assethub-polkadot.png", diff --git a/src/config/chains/astar.json b/src/config/chains/astar.json index a583c90..d05ae96 100644 --- a/src/config/chains/astar.json +++ b/src/config/chains/astar.json @@ -4,7 +4,7 @@ "rpc": "wss://astar-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://astar.webapi.subscan.io" + "subscan": "https://astar.api.subscan.io" }, "category": "polkadot", "logo": "/image/astar.png", diff --git a/src/config/chains/bifrost.json b/src/config/chains/bifrost.json index a744207..a8b69b6 100644 --- a/src/config/chains/bifrost.json +++ b/src/config/chains/bifrost.json @@ -4,7 +4,7 @@ "rpc": "wss://hk.p.bifrost-rpc.liebi.com/ws", "api": { "subql": "", - "subscan": "https://bifrost.webapi.subscan.io" + "subscan": "https://bifrost.api.subscan.io" }, "category": "polkadot", "logo": "/image/bifrost.png", diff --git a/src/config/chains/coretime_kusama.json b/src/config/chains/coretime_kusama.json index dca4f62..ef8d676 100644 --- a/src/config/chains/coretime_kusama.json +++ b/src/config/chains/coretime_kusama.json @@ -4,7 +4,7 @@ "rpc": "wss://coretime-kusama-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://coretime-kusama.webapi.subscan.io" + "subscan": "https://coretime-kusama.api.subscan.io" }, "category": "kusama", "logo": "/image/coretime-kusama.png", diff --git a/src/config/chains/coretime_paseo.json b/src/config/chains/coretime_paseo.json index 3293de5..d0d4835 100644 --- a/src/config/chains/coretime_paseo.json +++ b/src/config/chains/coretime_paseo.json @@ -4,7 +4,7 @@ "rpc": "wss://sys.ibp.network/coretime-paseo", "api": { "subql": "", - "subscan": "https://coretime-paseo.webapi.subscan.io" + "subscan": "https://coretime-paseo.api.subscan.io" }, "category": "paseo", "logo": "/image/coretime-paseo.png", diff --git a/src/config/chains/coretime_polkadot.json b/src/config/chains/coretime_polkadot.json index b3091df..a866121 100644 --- a/src/config/chains/coretime_polkadot.json +++ b/src/config/chains/coretime_polkadot.json @@ -4,7 +4,7 @@ "rpc": "wss://coretime-polkadot-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://coretime-polkadot.webapi.subscan.io" + "subscan": "https://coretime-polkadot.api.subscan.io" }, "category": "polkadot", "logo": "/image/coretime-polkadot.png", diff --git a/src/config/chains/hydration.json b/src/config/chains/hydration.json index 6cb6f8a..74d5f55 100644 --- a/src/config/chains/hydration.json +++ b/src/config/chains/hydration.json @@ -4,7 +4,7 @@ "rpc": "wss://hydration-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://hydration.webapi.subscan.io" + "subscan": "https://hydration.api.subscan.io" }, "category": "polkadot", "logo": "/image/hydration.png", diff --git a/src/config/chains/pendulum.json b/src/config/chains/pendulum.json index 3d5f006..7c38419 100644 --- a/src/config/chains/pendulum.json +++ b/src/config/chains/pendulum.json @@ -4,7 +4,7 @@ "rpc": "wss://rpc-pendulum.prd.pendulumchain.tech", "api": { "subql": "", - "subscan": "https://pendulum.webapi.subscan.io" + "subscan": "https://pendulum.api.subscan.io" }, "category": "polkadot", "logo": "/image/pendulum.png", diff --git a/src/config/chains/people_kusama.json b/src/config/chains/people_kusama.json index b28f135..25f65fc 100644 --- a/src/config/chains/people_kusama.json +++ b/src/config/chains/people_kusama.json @@ -4,7 +4,7 @@ "rpc": "wss://people-kusama-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://people-kusama.webapi.subscan.io" + "subscan": "https://people-kusama.api.subscan.io" }, "category": "kusama", "logo": "/image/people-kusama.png", diff --git a/src/config/chains/people_paseo.json b/src/config/chains/people_paseo.json index 1563cdc..a08e24f 100644 --- a/src/config/chains/people_paseo.json +++ b/src/config/chains/people_paseo.json @@ -4,7 +4,7 @@ "rpc": "wss://sys.ibp.network/people-paseo", "api": { "subql": "", - "subscan": "https://people-paseo.webapi.subscan.io" + "subscan": "https://people-paseo.api.subscan.io" }, "category": "paseo", "logo": "/image/people-paseo.png", diff --git a/src/config/chains/people_polkadot.json b/src/config/chains/people_polkadot.json index 6f7de55..0ad7f12 100644 --- a/src/config/chains/people_polkadot.json +++ b/src/config/chains/people_polkadot.json @@ -4,7 +4,7 @@ "rpc": "wss://people-polkadot-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://people-polkadot.webapi.subscan.io" + "subscan": "https://people-polkadot.api.subscan.io" }, "category": "polkadot", "logo": "/image/people-polkadot.png", diff --git a/src/hooks/combineQuery.ts b/src/hooks/combineQuery.ts index a7e3333..e44c806 100644 --- a/src/hooks/combineQuery.ts +++ b/src/hooks/combineQuery.ts @@ -11,6 +11,7 @@ import { useMultisigAccountDetail as useSubscanMultisigAccountDetail, useMultisigRecordCount as usSubscanMultisigRecordCount, useMultisigRecords as useSubscanMultisigRecords, + useResourceCount as useSubscanResourceCount, constants as subscanConstants, } from './subscan'; @@ -22,6 +23,12 @@ export interface MultisigRecordCountResult { multisigRecords: { totalCount: number }; } +export interface MultisigCountsResult { + approvalCount: number | undefined; + confirmedCount: number | undefined; + cancelledCount: number | undefined; +} + export function useMultisigAccountDetail(network: NetConfigV2 | undefined) { const subquery = useSubqueryMultisigAccountDetail(network); const subscan = useSubscanMultisigAccountDetail(network); @@ -97,6 +104,42 @@ export function useMultisigRecords( return { fetchData, data, loading }; } +// Fetch counts for all three statuses in a single request (subscan) or two fallback calls (subquery). +export function useMultisigResourceCount(network: NetConfigV2 | undefined) { + const subscan = useSubscanResourceCount(network); + const confirmedSubquery = usSubqueryMultisigRecordCount(network); + const cancelledSubquery = usSubqueryMultisigRecordCount(network); + + const useSubscan = !!network?.api?.subscan; + + const fetchData = useCallback( + (account: string) => { + if (!account) return; + if (useSubscan) { + subscan.fetchData(account); + } else { + confirmedSubquery.fetchData(account, 'confirmed'); + cancelledSubquery.fetchData(account, 'cancelled'); + } + }, + [useSubscan, subscan.fetchData, confirmedSubquery.fetchData, cancelledSubquery.fetchData] + ); + + if (useSubscan) { + return { fetchData, data: subscan.data, loading: subscan.loading }; + } + + return { + fetchData, + data: { + approvalCount: undefined, + confirmedCount: confirmedSubquery.data?.multisigRecords.totalCount, + cancelledCount: cancelledSubquery.data?.multisigRecords.totalCount, + } as MultisigCountsResult, + loading: confirmedSubquery.loading || cancelledSubquery.loading, + }; +} + export function useDataSourceTools(network: NetConfigV2 | undefined) { return { constants: network?.api?.subscan ? subscanConstants : subqueryConstants, diff --git a/src/hooks/subscan.ts b/src/hooks/subscan.ts index c4f14a5..337db8d 100644 --- a/src/hooks/subscan.ts +++ b/src/hooks/subscan.ts @@ -3,7 +3,7 @@ import { useCallback, useState } from 'react'; import { MultisigRecordsQueryRes } from 'src/components/ExtrinsicRecords'; import { NetConfigV2 } from 'src/model'; import axiosRequest from './AxiosRequest'; -import { MultisigAccountDetailResult, MultisigRecordCountResult } from './combineQuery'; +import { MultisigAccountDetailResult, MultisigCountsResult, MultisigRecordCountResult } from './combineQuery'; function subscanRecordsStatusConverter(status: string) { switch (status) { @@ -195,6 +195,49 @@ export const useMultisigRecords = (network: NetConfigV2 | undefined) => { return { fetchData, data: userInfo, loading }; }; +export const useResourceCount = (network: NetConfigV2 | undefined) => { + const [counts, setCounts] = useState({ + approvalCount: undefined, + confirmedCount: undefined, + cancelledCount: undefined, + }); + const [loading, setLoading] = useState(false); + + const fetchData = useCallback( + async (account: string) => { + if (!network || !account) return; + setLoading(true); + try { + const { data } = await axiosRequest.post<{ + list: { resource: string; count: number }[]; + }>(`${network.api?.subscan}/api/scan/resource_count`, { + resource: ['Multisig'], + extra: { + Multisig: { + status: ['Approval', 'Executed', 'Cancelled'], + }, + }, + account, + }); + const list = data.data.list || []; + const find = (name: string) => list.find((item) => item.resource === name)?.count; + setCounts({ + approvalCount: find('MultisigApproval'), + confirmedCount: find('MultisigExecuted'), + cancelledCount: find('MultisigCancelled'), + }); + } catch (error) { + // keep previous counts on error + } finally { + setLoading(false); + } + }, + [network] + ); + + return { fetchData, data: counts, loading }; +}; + export const constants = { approveType_initialize: 'Initialize', approveType_executed: 'Executed', diff --git a/src/providers/multisig-provider.tsx b/src/providers/multisig-provider.tsx index 9f9a769..fd1ab62 100644 --- a/src/providers/multisig-provider.tsx +++ b/src/providers/multisig-provider.tsx @@ -2,7 +2,7 @@ import { KeyringAddress } from '@polkadot/ui-keyring/types'; import { Spin } from 'antd'; import { createContext, useCallback, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { useApi, useMultisig, useMultisigRecordCount } from '../hooks'; +import { useApi, useMultisig, useMultisigResourceCount } from '../hooks'; import { Entry } from '../model'; import { empty } from '../utils'; @@ -44,30 +44,27 @@ export const EntriesProvider = ({ children }: React.PropsWithChildren) fetchInProgress(); }, [fetchInProgress]); - const { fetchData, data } = useMultisigRecordCount(networkConfig); + const { fetchData: fetchCounts, data: countsData } = useMultisigResourceCount(networkConfig); const refreshConfirmedAccount = useCallback(() => { - fetchData(account, 'confirmed'); - }, [account, fetchData]); - - const { fetchData: fetchCancelledData, data: cancelledData } = useMultisigRecordCount(networkConfig); + fetchCounts(account); + }, [account, fetchCounts]); const refreshCancelledAccount = useCallback(() => { - fetchCancelledData(account, 'cancelled'); - }, [account, fetchCancelledData]); + fetchCounts(account); + }, [account, fetchCounts]); useEffect(() => { - refreshConfirmedAccount(); - refreshCancelledAccount(); - }, [refreshConfirmedAccount, refreshCancelledAccount]); + fetchCounts(account); + }, [account, fetchCounts]); return ( Date: Mon, 2 Mar 2026 11:56:26 +0800 Subject: [PATCH 2/6] chore: add set subscan api key --- public/locales/en/translation.json | 12 +++- public/locales/zh/translation.json | 12 +++- src/App.tsx | 11 ++++ src/components/modals/ApiKeyModal.tsx | 81 +++++++++++++++++++++++++++ src/hooks/AxiosRequest.ts | 10 ++++ src/model/storage.ts | 1 + 6 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/components/modals/ApiKeyModal.tsx diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 6e4a2d4..770aa85 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -212,5 +212,15 @@ "Unfavorite": "Unfavorite", "share link": "Share Link", "Mulitsig Transaction Details": "Mulitsig Transaction Details", - "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.": "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving." + "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.": "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.", + "api_key": { + "title": "Subscan API Key", + "button": "API Key", + "description": "The key is stored locally in your browser.", + "label": "API Key", + "placeholder": "Enter your Subscan API key", + "current_set": "API key is currently set", + "saved": "API key saved", + "deleted": "API key deleted" + } } diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index c718a2e..f53268a 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -210,5 +210,15 @@ "unfavorite": "取消收藏", "share link": "分享链接", "Mulitsig Transaction Details": "多签交易详情", - "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.": "在批准前,请务必仔细检查多签交易进度和参数。" + "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.": "在批准前,请务必仔细检查多签交易进度和参数。", + "api_key": { + "title": "Subscan API Key", + "button": "API Key", + "description": "Key 保存在本地浏览器中。", + "label": "API Key", + "placeholder": "请输入 Subscan API Key", + "current_set": "当前已设置 API Key", + "saved": "API Key 已保存", + "deleted": "API Key 已删除" + } } diff --git a/src/App.tsx b/src/App.tsx index f70764d..a08be1c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import subscanLogo from 'src/assets/images/subscan_logo.png'; import { Footer } from './components/Footer'; import { HeadAccounts } from './components/HeadAccounts'; import { DownIcon } from './components/icons'; +import { ApiKeyModal } from './components/modals/ApiKeyModal'; import { SelectNetworkModal } from './components/modals/SelectNetworkModal'; import Status from './components/Status'; import { chains } from './config/chains'; @@ -54,6 +55,7 @@ function App() { }, [rpc]); const [selectNetworkModalVisible, setSelectNetworkModalVisible] = useState(false); + const [apiKeyModalVisible, setApiKeyModalVisible] = useState(false); const openExplorer = () => { if (networkConfig?.explorerHostName) { @@ -89,6 +91,14 @@ function App() { {networkStatus === 'success' && } + + + + {currentKey && ( + + )} + + + + ); +}; diff --git a/src/hooks/AxiosRequest.ts b/src/hooks/AxiosRequest.ts index f5702bc..0e93dbb 100644 --- a/src/hooks/AxiosRequest.ts +++ b/src/hooks/AxiosRequest.ts @@ -15,6 +15,7 @@ export class Request { this.instance = axios.create(Object.assign(this.baseConfig, config)); this.instance.interceptors.request.use( + // eslint-disable-next-line complexity (inConfig) => { const token = localStorage.getItem('token') as string; const organizationId = localStorage.getItem('organization:id') as string; @@ -27,6 +28,15 @@ export class Request { inConfig.headers!['OPEN-PLATFORM-ORGANIZATION'] = organizationId; } + try { + const storage = JSON.parse(localStorage.getItem('multisig') || '{}'); + if (storage.subscanApiKey) { + inConfig.headers!['X-API-Key'] = storage.subscanApiKey; + } + } catch { + // ignore storage parse errors + } + return inConfig; }, (err: any) => { diff --git a/src/model/storage.ts b/src/model/storage.ts index 8db95dd..56d2097 100644 --- a/src/model/storage.ts +++ b/src/model/storage.ts @@ -7,4 +7,5 @@ export interface StorageInfo { customNetwork?: NetConfigV2; addedCustomNetworks?: NetConfigV2[]; selectedRpc?: string; + subscanApiKey?: string; } From 9e07007bf4ae34174e202977d1b8951b4b20a0da Mon Sep 17 00:00:00 2001 From: carlhong Date: Mon, 2 Mar 2026 13:37:47 +0800 Subject: [PATCH 3/6] chore: update validate key --- public/locales/en/translation.json | 13 ++-- public/locales/zh/translation.json | 13 ++-- src/components/modals/ApiKeyModal.tsx | 89 ++++++++++++++++++++++----- 3 files changed, 92 insertions(+), 23 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 770aa85..368a922 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -214,13 +214,18 @@ "Mulitsig Transaction Details": "Mulitsig Transaction Details", "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.": "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.", "api_key": { - "title": "Subscan API Key", - "button": "API Key", - "description": "The key is stored locally in your browser.", + "title": "Set up API key", + "button": "Advanced settings", + "desc_storage": "Your API key is stored only in this browser (not uploaded to Subscan servers).", + "desc_security": "Don't save it on public/shared devices. Keep it secure.", + "desc_features": "After setup, you can enable features like History and auto-fetching Call Data.", + "desc_get_key": "Get an API key:", "label": "API Key", "placeholder": "Enter your Subscan API key", "current_set": "API key is currently set", "saved": "API key saved", - "deleted": "API key deleted" + "deleted": "API key deleted", + "invalid_key": "Invalid API key. Please check and try again.", + "invalid_key_or_network": "Invalid API key or unsupported network. Please check and try again." } } diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index f53268a..9bd70c6 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -212,13 +212,18 @@ "Mulitsig Transaction Details": "多签交易详情", "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.": "在批准前,请务必仔细检查多签交易进度和参数。", "api_key": { - "title": "Subscan API Key", - "button": "API Key", - "description": "Key 保存在本地浏览器中。", + "title": "配置 API Key", + "button": "高级设置", + "desc_storage": "API Key 仅保存在本地浏览器中(不上传 Subscan 服务器)。", + "desc_security": "请勿在公共设备上保存,并妥善保管以防泄露。", + "desc_features": "配置后可启用:历史记录、自动获取 Call Data 等功能。", + "desc_get_key": "获取 API Key:", "label": "API Key", "placeholder": "请输入 Subscan API Key", "current_set": "当前已设置 API Key", "saved": "API Key 已保存", - "deleted": "API Key 已删除" + "deleted": "API Key 已删除", + "invalid_key": "API Key 无效,请检查后重试。", + "invalid_key_or_network": "API Key 无效或网络不受支持,请检查后重试。" } } diff --git a/src/components/modals/ApiKeyModal.tsx b/src/components/modals/ApiKeyModal.tsx index 1e8e8ee..f1ab834 100644 --- a/src/components/modals/ApiKeyModal.tsx +++ b/src/components/modals/ApiKeyModal.tsx @@ -1,8 +1,9 @@ import { CloseOutlined } from '@ant-design/icons'; +import axios from 'axios'; import { Button, Input, Modal, message } from 'antd'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { getThemeColor } from 'src/config'; +import { getLinkColor, getThemeColor } from 'src/config'; import { useApi } from 'src/hooks'; import { readStorage, updateStorage } from 'src/utils/helper/storage'; @@ -11,29 +12,69 @@ interface ApiKeyModalProps { onCancel: () => void; } +const FALLBACK_SUBSCAN_URL = 'https://polkadot.api.subscan.io'; + +async function validateApiKey(subscanBaseUrl: string, apiKey: string): Promise { + const { data } = await axios.post( + `${subscanBaseUrl}/api/now`, + {}, + { headers: { 'X-API-Key': apiKey }, timeout: 10000 } + ); + // Subscan returns code 0 on success; non-zero means auth failure or error + if (data?.code !== 0) { + throw new Error(`code:${data?.code}`); + } +} + +// eslint-disable-next-line complexity export const ApiKeyModal = ({ visible, onCancel }: ApiKeyModalProps) => { const { t } = useTranslation(); - const { network } = useApi(); + const { network, networkConfig } = useApi(); const mainColor = useMemo(() => getThemeColor(network), [network]); + const linkColor = useMemo(() => getLinkColor(network), [network]); const [apiKey, setApiKey] = useState(() => readStorage().subscanApiKey || ''); + const [validating, setValidating] = useState(false); + const [validationError, setValidationError] = useState(''); - const handleSave = () => { + const subscanUrl = networkConfig?.api?.subscan || FALLBACK_SUBSCAN_URL; + const currentKey = readStorage().subscanApiKey; + + const handleSave = async () => { const trimmed = apiKey.trim(); - updateStorage({ subscanApiKey: trimmed || undefined }); - message.success(t('api_key.saved')); - onCancel(); + + // clearing the key — no need to validate + if (!trimmed) { + updateStorage({ subscanApiKey: undefined }); + message.success(t('api_key.saved')); + onCancel(); + return; + } + + setValidating(true); + setValidationError(''); + + try { + await validateApiKey(subscanUrl, trimmed); + updateStorage({ subscanApiKey: trimmed }); + message.success(t('api_key.saved')); + onCancel(); + } catch (err: any) { + const status = err?.response?.status; + setValidationError(t('api_key.invalid_key_or_network')); + } finally { + setValidating(false); + } }; const handleDelete = () => { updateStorage({ subscanApiKey: undefined }); setApiKey(''); + setValidationError(''); message.success(t('api_key.deleted')); onCancel(); }; - const currentKey = readStorage().subscanApiKey; - return (
@@ -46,31 +87,49 @@ export const ApiKeyModal = ({ visible, onCancel }: ApiKeyModalProps) => {
-
- {t('api_key.description')} -
+
    +
  • {t('api_key.desc_storage')}
  • +
  • {t('api_key.desc_security')}
  • +
  • {t('api_key.desc_features')}
  • +
  • + {t('api_key.desc_get_key')}{' '} + + pro.subscan.io + +
  • +
{t('api_key.label')}
setApiKey(e.target.value)} + onChange={(e) => { + setApiKey(e.target.value); + setValidationError(''); + }} + className={validationError ? 'ant-input-status-error' : ''} /> - {currentKey && ( + {validationError && ( +
+ {validationError} +
+ )} + + {!validationError && currentKey && (
{t('api_key.current_set')}
)}
- {currentKey && ( - )} From 86b98f3d41715ccd11069e89cd356d350737fb24 Mon Sep 17 00:00:00 2001 From: Carl Date: Mon, 2 Mar 2026 16:28:58 +0800 Subject: [PATCH 4/6] Update src/components/ExtrinsicRecords.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/ExtrinsicRecords.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/ExtrinsicRecords.tsx b/src/components/ExtrinsicRecords.tsx index d303ff8..b89d41f 100644 --- a/src/components/ExtrinsicRecords.tsx +++ b/src/components/ExtrinsicRecords.tsx @@ -267,6 +267,14 @@ export function ExtrinsicRecords() { }, 300); }; + useEffect(() => { + return () => { + if (fetchTimerRef.current) { + clearTimeout(fetchTimerRef.current); + fetchTimerRef.current = null; + } + }; + }, []); const refreshData = () => { queryInProgress(); fetchConfirmed(); From dceeebd0f33a44aec13fe9098c318b2f2c195c81 Mon Sep 17 00:00:00 2001 From: Carl Date: Mon, 2 Mar 2026 16:29:08 +0800 Subject: [PATCH 5/6] Update src/components/modals/ApiKeyModal.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/modals/ApiKeyModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/modals/ApiKeyModal.tsx b/src/components/modals/ApiKeyModal.tsx index f1ab834..7a965fd 100644 --- a/src/components/modals/ApiKeyModal.tsx +++ b/src/components/modals/ApiKeyModal.tsx @@ -60,7 +60,6 @@ export const ApiKeyModal = ({ visible, onCancel }: ApiKeyModalProps) => { message.success(t('api_key.saved')); onCancel(); } catch (err: any) { - const status = err?.response?.status; setValidationError(t('api_key.invalid_key_or_network')); } finally { setValidating(false); From 6cc221c3c2a0735084980e535988c428dd16eaf5 Mon Sep 17 00:00:00 2001 From: carlhong Date: Tue, 3 Mar 2026 09:58:34 +0800 Subject: [PATCH 6/6] refactor: update refreshcounts --- src/components/TxApprove.tsx | 15 +++------------ src/components/WalletState.tsx | 5 ++--- src/providers/multisig-provider.tsx | 15 ++++----------- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/components/TxApprove.tsx b/src/components/TxApprove.tsx index 0c5efb9..2b48f07 100644 --- a/src/components/TxApprove.tsx +++ b/src/components/TxApprove.tsx @@ -25,7 +25,7 @@ export function TxApprove({ const [getApproveTx] = useMultiApprove(); const { queueExtrinsic } = useContext(StatusContext); const [getUnapprovedInjectedList] = useUnapprovedAccounts(); - const { setIsPageLock, queryInProgress, refreshConfirmedAccount } = useMultisigContext(); + const { setIsPageLock, queryInProgress, refreshCounts } = useMultisigContext(); const unapprovedAddresses = getUnapprovedInjectedList(entry); const memberPairs = (account.meta?.addressPair ?? []) as AddressPair[]; const injectedMemberAccounts: string[] = memberPairs @@ -55,7 +55,7 @@ export function TxApprove({ makeSure(txSpy)(null); queryInProgress(); setTimeout(() => { - refreshConfirmedAccount(); + refreshCounts(); // eslint-disable-next-line no-magic-numbers }, 10000); }, @@ -73,16 +73,7 @@ export function TxApprove({ } ); }, - [ - availableAccounts, - getApproveTx, - onOperation, - queryInProgress, - queueExtrinsic, - refreshConfirmedAccount, - setIsPageLock, - txSpy, - ] + [availableAccounts, getApproveTx, onOperation, queryInProgress, queueExtrinsic, refreshCounts, setIsPageLock, txSpy] ); if (!entry.callHash || !entry.callData) { diff --git a/src/components/WalletState.tsx b/src/components/WalletState.tsx index 73b82a7..ed04eb6 100644 --- a/src/components/WalletState.tsx +++ b/src/components/WalletState.tsx @@ -32,8 +32,7 @@ export function WalletState(props: WalletStateProps) { const history = useHistory(); const { network, api, networkConfig } = useApi(); const { multisigAccount, changeMultisigAccount } = props; - const { inProgress, queryInProgress, confirmedAccount, refreshConfirmedAccount, fetchInProgress } = - useMultisigContext(); + const { inProgress, queryInProgress, confirmedAccount, refreshCounts, fetchInProgress } = useMultisigContext(); const [isAccountsDisplay, setIsAccountsDisplay] = useState(false); const [isExtrinsicDisplay, setIsExtrinsicDisplay] = useState(false); const [isTransferDisplay, setIsTransferDisplay] = useState(false); @@ -132,7 +131,7 @@ export function WalletState(props: WalletStateProps) { const id = setInterval(() => { tick(); - refreshConfirmedAccount(); + refreshCounts(); }, LONG_DURATION); return () => clearInterval(id); diff --git a/src/providers/multisig-provider.tsx b/src/providers/multisig-provider.tsx index fd1ab62..718312e 100644 --- a/src/providers/multisig-provider.tsx +++ b/src/providers/multisig-provider.tsx @@ -13,8 +13,7 @@ export const MultisigContext = createContext<{ cancelledAccount: number | undefined; setMultisigAccount: React.Dispatch> | null; queryInProgress: (silent?: boolean) => Promise; - refreshConfirmedAccount: () => void; - refreshCancelledAccount: () => void; + refreshCounts: () => void; setIsPageLock: (lock: boolean) => void; loadingInProgress: boolean; fetchInProgress: any; @@ -26,8 +25,7 @@ export const MultisigContext = createContext<{ cancelledAccount: 0, queryInProgress: () => Promise.resolve(), setIsPageLock: empty, - refreshConfirmedAccount: empty, - refreshCancelledAccount: empty, + refreshCounts: empty, loadingInProgress: false, // eslint-disable-next-line @typescript-eslint/no-empty-function fetchInProgress: () => {}, @@ -46,11 +44,7 @@ export const EntriesProvider = ({ children }: React.PropsWithChildren) const { fetchData: fetchCounts, data: countsData } = useMultisigResourceCount(networkConfig); - const refreshConfirmedAccount = useCallback(() => { - fetchCounts(account); - }, [account, fetchCounts]); - - const refreshCancelledAccount = useCallback(() => { + const refreshCounts = useCallback(() => { fetchCounts(account); }, [account, fetchCounts]); @@ -65,8 +59,7 @@ export const EntriesProvider = ({ children }: React.PropsWithChildren) setIsPageLock, confirmedAccount: countsData?.confirmedCount, cancelledAccount: countsData?.cancelledCount, - refreshConfirmedAccount, - refreshCancelledAccount, + refreshCounts, }} >